#
# Расширение пакета test для OpenSSL
#
package require http
# Путь поиска пакета test
if {[info exists env(TOOLDIR)]} {
	lappend auto_path $env(TOOLDIR)
} {
	lappend auto_path "[file dirname [info script]]/../../maketool"
}


# outputs specified environment variables into log

proc log_vars {args} {
	foreach var $args {
		if [info exists ::env($var)] {
			log $var=$::env($var)
		} else {
			log "$var is not set"
		}	
	}
}	
# Проверка наличия необходимых переменных окружения
foreach var {OPENSSL_APP} {
if {![info exists env($var)]} {
	puts stderr "Environment variable $var not defined"
	exit 100
} else {
	set $var [file normalize $env($var)]
}
}

if {[info exists env(OPENSSL_CONF)]} {
	set OPENSSL_CONF $env(OPENSSL_CONF)
} else {
	if {[regexp {OPENSSLDIR: "([^\"]+)"} [exec $OPENSSL_APP version -d] => openssl_dir]} {
		set OPENSSL_CONF $openssl_dir/openssl.cnf
	} else {	
		puts stderr "Cannot find out default openssl config"
		exit 100
	}
}	

if {![file exists $OPENSSL_CONF]} {
	puts "Configuration file $OPENSSL_CONF doesn't exist"
	exit 100
}	

if {$::tcl_platform(platform) != "windows"} {
  proc kill {signal pid} {
  exec kill -$signal $pid
  }
} else {
  proc kill {signal pid} {
  exec taskkill /pid $pid /f
  }
}
	
package require test
set test::suffix ""
package require base64

#
# set  up test::src variable
#

if {[info exists env(TESTSRC)]} {
	set ::test::src [file normalize $env(TESTSRC)]
} else {
	set ::test::src [pwd]
}	

#
# set  up test::dir variable
#

if {[info exists env(TESTDIR)]} {
	set ::test::dir [file normalize $env(TESTDIR)]
} else {
	set ::test::dir [file join [pwd] z]
}	

#
# Фильтрует вывод полученный в виде длинной строки, разбивая на строки
# по \n. Возвращает строки, удовлетворяющие регулярному выражениу
# pattern
#

proc grep {pattern data} {
	set out ""
	foreach line [split $data "\n"] {
		if {[regexp $pattern $line]} {
			append out $line "\n"
		}
	}	
	return $out
}	
proc check_builtin_engine {} {
	global OPENSSL_APP
	set found [regexp Cryptocom [exec $OPENSSL_APP engine 2> /dev/null]]
	if {$found} {
		puts "Using statically compiled engine"
	} else {
		puts "Using dynamically loaded engine"
	}
	return $found
}	

# Получение случайно подвыборки из доступного списка
proc get_sample {list count} {
    set result {}
    set n [llength $list]
    if {$count >= $n} {
        return $list
    }
    unset -nocomplain seen;
    while {[llength $result] < $count} {
        set idx [expr {int(rand() * $n)}]
        if {![info exists seen($idx)]} {
            lappend result [lindex $list $idx]
            set seen($idx) 1
        }
    }
    return $result
}

# Вызывает команду openssl.
# Посылает в лог вывод на stdout и на stderr, возвращает его же.
proc openssl {cmdline} {
	global ENGINE_PATH OPENSSL_APP
	log_vars OPENSSL_CONF CRYPT_PARAMS RNG RNG_PARAMS CCENGINE_LICENSE
	if {[info exists ::test::engine]} {
		set cmdline [concat [lrange $cmdline 0 0] [list -engine $::test::engine] [lrange $cmdline 1 end]]
	}	
	log "OpenSSL cmdline: $OPENSSL_APP $cmdline"
	set f [open "|$OPENSSL_APP $cmdline" r]
	set output [read $f]
	if {[catch {close $f} msg]} {
		append output "STDERR CONTENTS:\n$msg"
		log $output
		if {[lindex $::errorCode 0]!="NONE"} {
			return -code error -errorcode $::errorCode $output
		}
	}	
	return $output
}	


proc getConfig {args} {
	global OPENSSL_CONF
	if {![info exists OPENSSL_CONF]} {
	  if {![regexp "OPENSSLDIR: \"\[^\"\]+\"" [openssl version -d] => openssl_dir]} {
	  	puts stderr "Cannot find out openssl directory"
		exit 1
	  }
	 set OPENSSL_CONF  "$openssl_dir/openssl.cnf"
	}
	set f [open $OPENSSL_CONF r]
	set out ""
	set mode copy
	while {[gets $f line]>=0} {
		if {[regexp	"\\s*\\\[\\s*(\\S+)\\s*\\\]" $line => section]} {
			if {[lsearch -exact $args $section]!=-1} {
				set mode skip
			} else {
				set mode copy
			}
		}
		if {$mode eq "copy"} {
			append out $line \n
		}	
	 }	
	 return $out
}	 
#
# Создает тестовый CA
# Допустимые параметры: 
# CAname - директория, в которой создается CA (testCA по умолчанию)
# алгоритм с параметрами в формате команды req
#

proc makeCA {{CAname {}} {algor_with_par gost2012_512:A}} {
	global OPENSSL_CONF
	if {![string length $CAname]} {
		set CAname [file rootname [file tail $::argv0]]CA-2012
	}	
	set test::ca $CAname
	file delete -force $CAname
	file mkdir $CAname
	makeFile $CAname/ca.conf "
\[ ca \]
default_ca      = CA_default            # The default ca section

\[ CA_default \]

dir            = [file join [pwd] $CAname]              # top dir
database       = \$dir/index.txt        # index file.
new_certs_dir  = \$dir/newcerts         # new certs dir

certificate    = \$dir/cacert.pem       # The CA cert
serial         = \$dir/serial           # serial no file
private_key    = \$dir/private/cakey.pem# CA private key
RANDFILE       = \$dir/private/.rand    # random number file

default_days   = 3650                  # how long to certify for
default_crl_days= 30                   # how long before next CRL
default_md     = default               # use digest corresponding the algorithm
default_startdate = 060101000000Z

policy         = policy_any            # default policy
email_in_dn    = yes                   #  add the email into cert D


nameopt        = ca_default            # Subject name display option
certopt        = ca_default            # Certificate display option
copy_extensions = copy                 # Copy extensions from requ


\[ policy_any \]
countryName            = supplied
stateOrProvinceName    = optional
organizationName       = optional
organizationalUnitName = optional
commonName             = supplied
emailAddress           = supplied

"	
	makeFile $CAname/req.conf "
\[req\]
prompt=no
distinguished_name = req_dn
\[ req_dn \]
C = RU
L = Moscow
CN=Test CA $algor_with_par
O=Cryptocom
OU=OpenSSL CA
emailAddress = openssl@cryptocom.ru
\[ v3_ca \]
# Extensions for a typical CA
# PKIX recommendation.
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = critical,CA:true

# Key usage: this is typical for a CA certificate. However since it will
# prevent it being used as an test self-signed certificate it is best
# left out by default.
# keyUsage = cRLSign, keyCertSign

# Include email address in subject alt name: another PKIX recommendation
# subjectAltName=email:copy
# Copy issuer details
# issuerAltName=issuer:copy

# DER hex encoding of an extension: beware experts only!
# obj=DER:02:03
# Where 'obj' is a standard or added object
# You can even override a supported extension:
# basicConstraints= critical, DER:30:03:01:01:FF
"
	file mkdir $CAname/private
	file mkdir $CAname/newcerts
	generate_key [keygen_params $algor_with_par] $CAname/private/cakey.pem
	openssl "req -new  -x509 -key $CAname/private/cakey.pem -nodes -out $CAname/cacert.pem -config $CAname/req.conf -reqexts v3_ca -set_serial 0x11E"
	makeFile ./$CAname/.rand 1234567890
	makeFile ./$CAname/serial 011E
	makeFile ./$CAname/index.txt ""
	return [file isfile $CAname/cacert.pem]
}

proc extract_oids {filename {format PEM} {offset 0}} {
	set out ""
	if {$offset} {
		set miscargs "-offset $offset "
	} else {
		set miscargs ""
	}	
	foreach line [split [openssl "asn1parse $miscargs-in $filename -inform $format -oid oidfile"] "\n"] {
		if {([regexp {Gost\d+} $line]||[regexp "GostR" $line]||[regexp "GOST" $line]||[regexp "sha1" $line]) && ![regexp ^Loaded: $line]} {
			regsub {[^:]+:[^:]+:} $line "" line
			append out $line "\n"
		}
	}
	return $out
}
# 
# Формирует список параметров для openssl req необходимый для формирования 
# ключа c указанным алгоритмом и параметрами
#  
proc keygen_params {alg} {	
	return [split $alg :] 
}	

proc generate_key {params filename} {
	set alg [lindex $params 0]
	set param [lindex $params 1]
	set keyname $alg
	set keyname [append keyname _ $param .pem] 
	switch -glob $alg {
	rsa { 
		if {![string length $param]} {
			set param 1024
			set keyname "rsa_1024.pem"
		}
		set optname "-algorithm rsa -pkeyopt rsa_keygen_bits:$param"
		}
	ec {set optname "-paramfile $param"}
	dsa {set optname "-paramfile $param" }
	gost* { set optname "-algorithm $alg -pkeyopt paramset:$param" }
	}	
	if {$::tcl_platform(platform) eq "windows"} {
		set exesuffix ".exe"
	} else {
		set exesuffix ""
	}
	log "Keyname is $keyname"
#	if {[test_target_name] eq "open"} {
		log "Calling openssl cmd to create private key"
		openssl "genpkey  $optname -out $filename"
#	} elseif {[info exists ::env(OBJ)] && [file executable ../$::env(OBJ)/keytest$exesuffix]&& $alg eq "gost2001"} {
#		log "keytest$exesuffix $alg $param $filename"
#		exec ../$::env(OBJ)/keytest$exesuffix $alg $param $filename >&@ stdout
#	} elseif {[info exists ::env(OBJ)] && [file executable ../$::env(OBJ)/keytest$exesuffix]&& $alg eq "gost2012_256"} {
#		log "keytest$exesuffix $alg $param $filename"
#		exec ../$::env(OBJ)/keytest$exesuffix $alg $param $filename >&@ stdout
#	} elseif {[info exists ::env(OBJ)] && [file executable ../$::env(OBJ)/keytest$exesuffix]&& $alg eq "gost2012_512"} {
#		log "keytest$exesuffix $alg $param $filename"
#		exec ../$::env(OBJ)/keytest$exesuffix $alg $param $filename >&@ stdout
#	} elseif {[info exists ::env(PRIVATEKEYSDIR)] && [file exists $::env(PRIVATEKEYSDIR)/$keyname]} {
#		log "Copying file $keyname"
#		file copy $::env(PRIVATEKEYSDIR)/$keyname $filename
#	} else {
#		log "Calling openssl cmd to create private key"
#		openssl "genpkey  $optname -out $filename"
#	}
}

#
# Создает тестового пользователя с одним ключом подписи и одной заявкой
# на сертификат. 
# Параметры 
# username Имя директории, куда складывать файлы этого пользователя
# alg Параметр для опции -newkey команды openssl req, задающий алгоритм
#  ключа и параметры этого алгоритма
# Последующие параметры имеют вид списка ключ значение и задают поля
# Distinguished Name 
# FIXME Процедуру надо поправить, чтобы работала с новой версией openssl
proc makeUser {username alg args} {
	file delete -force $username
	file mkdir $username
	if {[lsearch $args CN]==-1} {
		lappend args CN $username
	}	
	makeFile $username/req.conf [eval makeConf $args]
	log "req.conf --------\n[getFile $username/req.conf]-------------"
	
	generate_key [keygen_params $alg] $username/seckey.pem
	openssl "req -new -key $username/seckey.pem -nodes -out $username/req.pem -config $username/req.conf"
	return [expr {[file size $username/req.pem] > 0}]
}

proc makeSecretKey {username alg} {
	file delete -force $username
	file mkdir $username
	generate_key [keygen_params $alg] $username/seckey.pem	
	return [expr {[file size $username/seckey.pem] > 0}]
}

#
# Создает пользователя с помощью makeUser и подписывает его сертификат
# ключом ранее созданного testCA. 
# Параметр CAname обрабатывается специальным образом: он не попадает в DN
#
proc makeRegisteredUser {username alg args } {
	if {![info exists params(CAname)]&&![info exists ::test::ca]} {
		return -code error "Default CA name is not known. Have you called makeCA earlier in this script?"
	}	
	set CAname $test::ca
	array set params $args
	if {[info exist params(CAname)]} {
		set CAname $params(CAname)
		unset params(CAname)
	}
	if {![file isdirectory $CAname]||![file exists $CAname/cacert.pem]} {
		return -code error "CA $CAname doesn't exists"
	}	
	eval makeUser [list $username $alg] [array get params]
	openssl "ca -config $CAname/ca.conf -in $username/req.pem -out $username/cert.pem -batch -notext" 
	return [file isfile $username/cert.pem]
}

proc makeConf {args} {
	global OPENSSL_CONF
	array set dn_attrs [list C  RU\
	L  Moscow\
	CN "Dummy user"\
	O Cryptocom\
	OU "OpenSSL Team"\
	emailAddress  "openssl@cryptocom.ru"\
	]
	array set dn_attrs $args
	if {[info exists dn_attrs(extensions)]} {
		set extensions $dn_attrs(extensions)
		unset dn_attrs(extensions)
	}	
	set out ""
	append out {[req]
prompt=no
distinguished_name = req_dn
}
if {[info exists extensions]} {
	append out "req_extensions = req_exts\n\[ req_exts \]\n" $extensions "\n"
}	
append out "\[ req_dn \]\n"
	foreach {key val} [array get dn_attrs] {
		append out "$key=$val\n"
	}
	return $out
}	
#
# Выполняет замену регулярного выражения re на строку s в указанном
# PEM-документе.
#
proc hackPem {re pem s} {
	set out ""
	foreach {whole_pem start_line coded_body end_line} [regexp -inline -all "(-----BEGIN \[^\n\]+-----\n)(.*?)(\n-----END \[^\n\]+-----\n)" $pem] {
		set der [::base64::decode $coded_body]
		set der [regsub -all $re $der $s]
		append out $start_line [::base64::encode $der] $end_line
	}
	return $out
}	

#
# Handling of OIDs
#

source [file dirname  [info script]]/name2oid.tcl
foreach {name oid} [array get name2oid] {
	set oid2name($oid) $name
}

proc long_name_by_id {id} {
	variable name2oid
	variable oid2name
	if {[regexp {^\d+(\.\d+)+$} $id]} {
	return "GOST $oid2name($id) $id"
	}
	return "GOST $id $name2oid($id)"
}

proc alg_id {alg} {
	switch -glob $alg {
		gost94cc {return pk_sign94_cc}
		gost94cc:* {return pk_sign94_cc}
		gost94:* {return pk_sign94_cp}
		gost2001cc:* {return pk_sign01_cc}
		gost2001cc {return pk_sign01_cc}
		gost2001:* {return pk_sign01_cp}
		gost2012_256:* {return pk_sign12_256}
		gost2012_512:* {return pk_sign12_512}
	}
}

proc alg_with_digest {alg} {
	variable name2oid
	switch -glob $alg {
		gost94cc {return hash_with_sign94_cc}
		gost94cc:* {return hash_with_sign94_cc}
		gost94:* {return hash_with_sign94_cp}
		gost2001cc:* {return hash_with_sign01_cc}
		gost2001cc {return hash_with_sign01_cc}
		gost2001:* {return hash_with_sign01_cp}
		gost2012_256:* {return hash_with_sign12_256}
		gost2012_512:* {return hash_with_sign12_512}
		
	}
}

proc alg_long_name {alg} {
	variable name2oid
	switch -glob $alg {
		#gost94cc {return hash_with_sign94_cc}
		#gost94cc:* {return hash_with_sign94_cc}
		#gost94:* {return hash_with_sign94_cp}
		#gost2001cc:* {return hash_with_sign01_cc}
		#gost2001cc {return hash_with_sign01_cc}
		gost2001:* {return "GOST R 34.10-2001"}
		gost2012_256:* {return "GOST R 34.10-2012 with 256 bit modulus"}
		gost2012_512:* {return "GOST R 34.10-2012 with 512 bit modulus"}
	}
}

# Returns hash algorithm corresponded to sign algorithm
proc alg_hash {alg} {
    switch -glob $alg {
        gost2012_256:* {return hash_12_256}
        gost2012_512:* {return hash_12_512}
        * {return hash_94}
   }
}

# Returns short name of hash algorithm
proc hash_short_name {hash_alg} {
    switch -glob $hash_alg {
        *hash_94 {return md_gost94}
        hash_12_256 {return md_gost12_256}
        hash_12_512 {return md_gost12_512}
        default {return $hash_alg}
    }
}

proc ts_hash_long_name {hash_alg} {
    switch -glob $hash_alg {
        *hash_94 {return md_gost94}
        hash_12_256 {return md_gost12_256}
        hash_12_512 {return md_gost12_512}
        default {return $hash_alg}
    }
}

# Returns long name of hash algorithm
proc hash_long_name {hash_alg} {
    switch -glob $hash_alg {
		*hash_94* {return "GOST R 34.11-94"}
		gost2001* {return "GOST R 34.11-94"}
        *12_256* {return "GOST R 34.11-2012 with 256 bit hash"}
        *12_512* {return "GOST R 34.11-2012 with 512 bit hash"}
        default {return $hash_alg}
    }
}

# Returns long name of hash_with_sign algorithm
proc hash_with_sign_long_name {alg} {
    switch -glob $alg {
        gost2001:* {return "GOST R 34.11-94 with GOST R 34.10-2001"}
        gost2012_256:* {return "GOST R 34.10-2012 with GOST R 34.11-2012 (256 bit)"}
        gost2012_512:* {return "GOST R 34.10-2012 with GOST R 34.11-2012 (512 bit)"}
        default {return $alg}
    }
}

proc smime_hash_with_sign_long_name {alg} {
    switch -glob $alg {
        hash_with_sign01_cp {return "GOST R 34.11-94 with GOST R 34.10-2001"}
        hash_with_sign12_256 {return "GOST R 34.10-2012 with GOST R 34.11-2012 (256 bit)"}
        hash_with_sign12_512 {return "GOST R 34.10-2012 with GOST R 34.11-2012 (512 bit)"}
        default {return $alg}
    }
}

proc micalg {hash_alg} {
    switch -exact $hash_alg {
        hash_94 {return "gostr3411-94"}
        hash_12_256 {return "gostr3411-2012-256"}
        hash_12_512 {return "gostr3411-2012-512"}
    }
}

proc param_pubkey {alg} {
	variable name2oid
	switch -exact $alg {
		gost94cc: {return param_pubkey94_cpa}
		gost94cc {return param_pubkey94_cpa}
		gost94:A {return param_pubkey94_cpa}
		gost94:B {return param_pubkey94_cpb}
		gost94:C {return param_pubkey94_cpc}
		gost94:D {return param_pubkey94_cpd}
		gost94:XA {return param_pubkey94_cpxcha}
		gost94:XB {return param_pubkey94_cpxchb}
		gost94:XC {return param_pubkey94_cpxchc}
		gost2001cc: {return param_pubkey01_cc}
		gost2001cc {return param_pubkey01_cc}
		gost2001:0 {return param_pubkey01_cptest}
		gost2001:A {return param_pubkey01_cpa}
		gost2001:B {return param_pubkey01_cpb}
		gost2001:C {return param_pubkey01_cpc}
		gost2001:XA {return param_pubkey01_cpxcha}
		gost2001:XB {return param_pubkey01_cpxchb}
		gost2012_256:0 {return param_pubkey01_cptest}
		gost2012_256:A {return param_pubkey01_cpa}
		gost2012_256:B {return param_pubkey01_cpb}
		gost2012_256:C {return param_pubkey01_cpc}
		gost2012_256:XA {return param_pubkey01_cpxcha}
		gost2012_256:XB {return param_pubkey01_cpxchb}
		gost2012_512:0 {return param_pubkey12_512_0}
		gost2012_512:A {return param_pubkey12_512_A}
		gost2012_512:B {return param_pubkey12_512_B}
	}
}


proc param_hash_long_name {hash_alg {pk_alg {}}} {
    # R 1323565.1.023-2018 (5.2.1.2) not recommends or forbids encoding
    # hash oid into TC26 (2012) parameters in AlgorithmIdentifier, so
    # this is removed.
    # Note:
    # Commit d47b346 reverts this behavior for 512-bit 0,A,B parameters
    switch -glob $pk_alg {
	gost2012_256:TC* {return}
	gost2012_512:C {return}
    }
    switch -glob $hash_alg {
        *hash_94 {return "id-GostR3411-94-CryptoProParamSet"}
        hash_12_256 {return "GOST R 34.11-2012 with 256 bit hash"}
        hash_12_512 {return "GOST R 34.11-2012 with 512 bit hash"}
    }
}

proc pubkey_long_name {alg} {
	variable name2oid
	switch -glob $alg {
		
		#gost2001cc: {return param_pubkey01_cc}
		#gost2001cc {return param_pubkey01_cc}
		#gost2001:0 {return param_pubkey01_cptest}
		gost2001:A {return "id-GostR3410-2001-CryptoPro-A-ParamSet"}
		gost2001:B {return "id-GostR3410-2001-CryptoPro-B-ParamSet"}
		gost2001:C {return "id-GostR3410-2001-CryptoPro-C-ParamSet"}
		gost2001:XA {return "id-GostR3410-2001-CryptoPro-XchA-ParamSet"}
		gost2001:XB {return "id-GostR3410-2001-CryptoPro-XchB-ParamSet"}
		gost2012_256:0 {return "id-GostR3410-2001-TestParamSet"}
		gost2012_256:A {return "id-GostR3410-2001-CryptoPro-A-ParamSet"}
		gost2012_256:B {return "id-GostR3410-2001-CryptoPro-B-ParamSet"}
		gost2012_256:C {return "id-GostR3410-2001-CryptoPro-C-ParamSet"}
		gost2012_256:XA {return "id-GostR3410-2001-CryptoPro-XchA-ParamSet"}
		gost2012_256:XB {return "id-GostR3410-2001-CryptoPro-XchB-ParamSet"}
		gost2012_256:TCA {return "GOST R 34.10-2012 (256 bit) ParamSet A"}
		gost2012_256:TCB {return "GOST R 34.10-2012 (256 bit) ParamSet B"}
		gost2012_256:TCC {return "GOST R 34.10-2012 (256 bit) ParamSet C"}
		gost2012_256:TCD {return "GOST R 34.10-2012 (256 bit) ParamSet D"}
		#gost2012_512:0 {return param_pubkey12_512_0}
		gost2012_512:A {return 	"GOST R 34.10-2012 (512 bit) ParamSet A"}
		gost2012_512:B {return 	"GOST R 34.10-2012 (512 bit) ParamSet B"}
		gost2012_512:C {return  "GOST R 34.10-2012 (512 bit) ParamSet C"}
	}
}

proc mkObjList {args} {
	set out ""
	foreach name $args {
		if {$name eq {}} continue
		append out " OBJECT            :$name\n"
	}
	return $out
}

proc structured_obj_list {args} {
	variable name2oid
	set out {}
	foreach {path name} $args {
		if {$name != {}} {set oid $name2oid($name)} {set oid {}}
		lappend out "$path=$oid"
	}
	return $out
}

proc param_hash {alg} {
    switch -glob $alg {
        gost2012_256:* {return hash_12_256}
        gost2012_512:* {return hash_12_512}
        * {return param_hash_94}
    }
}


proc param_encr {short_name} {
	variable name2oid
	if {[regexp {^\d+(\.\d+)+$} $short_name]} {
	return "$short_name"
	}
	switch -exact $short_name {
		cc_cipher_param {return param_encr_cc}
		{} {return param_encr_tc}
		cp_cipher_param_a {return param_encr_cpa}
		cp_cipher_param_b {return param_encr_cpb}
		cp_cipher_param_c {return param_encr_cpc}
		cp_cipher_param_d {return param_encr_cpd}
	}
}

proc encr_long_name {short_name} {
	variable name2oid
	switch -exact $short_name {
		"1.2.643.2.2.31.1" {return "id-Gost28147-89-CryptoPro-A-ParamSet"}
		"1.2.643.2.2.31.2" {return "id-Gost28147-89-CryptoPro-B-ParamSet"}
		"1.2.643.2.2.31.3" {return "id-Gost28147-89-CryptoPro-C-ParamSet"}
		"1.2.643.2.2.31.4" {return "id-Gost28147-89-CryptoPro-D-ParamSet"}
		"1.2.643.7.1.2.5.1.1" {return "GOST 28147-89 TC26 parameter set"}
		{} {return "GOST 28147-89 TC26 parameter set"}
	}
}



#
# Функции для управления клиентом и сервером при тестировании
# SSL-соединения
#

#  Параметры
#    Список аргументов командной строки клиента
#    список аргументов командной строки сервера
#    строка, которую надо передать на stdin клиенту
#
# Запускает openssl s_server и пытается приконнектиться к нему openssl
# s_client-ом. Возвращает список stdout  клиента, stderr клиента, кода
# завершения клиента, stdout
# сервера stderr сервера и кода завершения сервера.
# 
# Если процесс убит сигналом, возвращает в качестве кода завершения имя
# сигнала, иначе - числовое значение кода завершения ОС
# 
proc client_server {client_args server_args client_stdin} {
	log "CLIENT ARGS\n$client_args\n"
	log "SERVER ARGS\n$server_args\n"
	flush [test_log]
	set server [open_server $server_args]
	set client [open_client $client_args $client_stdin]
	log "server = $server client = $client"
	log "Both client and server started"
	flush [test_log]
	global finished
	log "Waitng for client to termintate"
	flush [test_log]
#	if {$::tcl_platform(platform) == "windows"} {
#		exec ../kbstrike [pid $client] 0x20
#	}
	vwait finished($client) 
	catch {stop_server $server}
	set list [concat [stop $client] [stop $server]]
	foreach channel {"CLIENT STDOUT" "CLIENT STDERR" "CLIENT EXIT CODE"  "SERVER STDOUT"
	"SERVER STDERR" "SERVER EXIT CODE"} data $list {
		log "$channel\n$data\n"
	}
	return $list
}
#
# Устанавливает командную строку для вызова клиента,
# в системный openssl на указанном хосте
#
proc remote_client {host} {
	if {[info hostname] == "$host"} {
		set ::test::client_unset {OPENSSL_CONF}
		set ::test::client_app "openssl s_client"
	} else {
		set ::test::client_unset {LD_LIBRARY_PATH OPENSSL_CONF}
		set ::test::client_app "ssh build@$host openssl s_client"
	}
}	
#
# Устанавливает командную строку для вызова клиента в указанную команду
# Необязательный параметр указывает список переменных окружения, которые
# НЕ НАДО передавать в эту команду
#
proc custom_client {command {forbidden_vars {}}} {
	set ::test::client_app $command
	set ::test::client_unset $forbidden_vars

}
#
# Восстанавливает станадртую клиентскую команду
#
proc our_client {} {
	catch {unset ::test::client_app}
	catch {unset ::test::client_unset}
}	

#
# Закрывает файл, указанный в соответствующем file_id, возвращает
# элемент глобального массива output, содержимое error message от close
# и код завершения процесса (имя сигнала)
proc stop {file_id} {
	global output
	fconfigure $file_id -blocking yes
	if {[catch {close $file_id} msg]} {
		if {[string match CHILD* [lindex $::errorCode 0]]} {
			set status [lindex $::errorCode 2]
		} else {
			set status 0
		}	
	}  else {
		set status 0
	}	
	return [list $output($file_id) $msg $status]
}	
#
# Завершает работу сервера
#
proc stop_server {file_id} {
#	puts $file_id "Q\n" 
#	catch {set xx [socket localhost 4433]}
	log "Interrupting process [pid $file_id]"
	flush [test_log]
	kill INT [pid $file_id]
	#puts -nonewline stderr "Waiting for server termination.."
	vwait finished($file_id)
	if [info exists xx] {close $xx}
#	puts stderr "Ok"
}	

#
# Запускает процесс с указанной командной строкой. Возвращает дескриптор
# файла в nonblocking mode с повешенным туда fileevent
# Очищает соответствующие элементы массивов output и finished
proc start_process {cmd_line read_event {mode "r"}} {
	set f [open "|$cmd_line" $mode]
	global output finished
	catch {unset finished($f)}
	fconfigure $f -buffering none -blocking n
	set output($f) ""
	fileevent $f readable [list $read_event $f]
	return $f
}	
#
# Обработчик fileevent-ов на чтение. Записывает считанные данные в
# элемент массива output соответствущий файлхендлу. В случае если
# достигнут eof, выставляет элемент массива finished. (элемент output
# при этом тоже трогается, чтобы vwait завершился)
#
proc process_read {f} {
	global output
	if {[eof $f]} {
		global finished
		fconfigure $f -blocking y
		set finished($f) 1
		append output($f) ""
		return
	}	
	append output($f) [read $f]
}	

#
#  Запускает openssl s_server с указанными аргументами и дожидается пока
#  он скажет на stdout ACCEPT. Возвращает filehandle, открытый на
#  чтение/запись
#
proc open_server {server_args} {
	global OPENSSL_APP
	global ENGINE_PATH
	if {[info exists ::test::server_conf]} {
		global env
		set save_conf $env(OPENSSL_CONF)
		set env(OPENSSL_CONF) $::test::server_conf
	}
	if {[info exists ::test::server_app]} {
		set server $::test::server_app
	} else {
		set server [list $OPENSSL_APP s_server]
	}
	if {[info exists ::test::server_unset]} {
		save_env $::test::server_unset
	}	
	set server [start_process [concat $server $server_args] process_read "r+"]
	restore_env
	if {[info exists save_conf]} {
		set env(OPENSSL_CONF) $save_conf
	}	

	global output finished
	#puts -nonewline stderr  "Waiting for server startup..."
	while {![regexp "ACCEPT" $output($server)]} {
		vwait output($server)
		if {[info exists finished($server)]} {
			#puts stderr "error"
			return -code error [lindex  [stop $server] 1]
		}	
	}		
	#puts stderr "Ok"
	after 100
	return $server
}
#
# Сохраняет указанные переменные среды для последующего восстановления
# restore_env
#
proc save_env {var_list} {
	catch {array unset ::test::save_env}
	foreach var $var_list {
		if {[info exist ::env($var)]} {
			set ::test::save_env($var) $::env($var)
			unset ::env($var)
		}	
	}

}
proc restore_env {} {
	if {[array exists ::test::save_env]} {
		array set ::env [array get ::test::save_env]
		array unset ::test::save_env
	}	
	
}
#
# Сохраняет указанные переменные среды для последующего восстановления
# restore_env2. В отличие от save_env, не делает unset сохраненной переменной.
#
proc save_env2 {var_list} {
	catch {array unset ::test::save_env2}
	foreach var $var_list {
		if {[info exist ::env($var)]} {
			set ::test::save_env2($var) $::env($var)
		}	
	}

}
#
# Восстанавливает переменные среды, ранее сохраненные функцией save_env2 
# В отличие от функции restore_env, требует списка переменных и 
# восстанавливает только переменные из данного списка. Второе отличие -
# если переменная из списка не была сохранена, делает ей unset.
#
proc restore_env2 {var_list} {
	foreach var $var_list {
		if {[info exist ::test::save_env2($var)]} {
			set ::env($var) $::test::save_env2($var)
		} else {
			catch {unset ::env($var)}
		}
	}
	array unset ::test::save_env2
}


#
# Запускает s_client с указанными аргументами, передавая на stdin
# указанную строку
#
proc open_client {client_args client_stdin} {
	global OPENSSL_APP
	if [info exists ::test::client_app] {
		set client $::test::client_app
	} else {
		set client [list $OPENSSL_APP s_client]
	}
	if {[info exists ::test::client_unset]} {
		save_env $::test::client_unset
	}	
	if {[info exists ::test::client_conf]}  {
		set save_env(OPENSSL_CONF) $::env(OPENSSL_CONF)
		set ::env(OPENSSL_CONF) $::test::client_conf
	}
	set client [start_process [concat $client $client_args [list << $client_stdin]] process_read]
	restore_env
	return $client
}	
#
# Зачитывает список хостов из ../../ssl-ciphers
#
proc get_hosts {file} {
	set ::test::suffix "-$file"
	if [file readable $file.ciphers] {
		set f [open $file.ciphers]
	} else {	
		set f [open ../../ssl-ciphers/$file.ciphers r]
	}
	while {[gets $f line]>=0} {
		if {[regexp {^\s*#} $line]} continue
		append data "$line\n"
	}
	close $f
	global hosts
	array set hosts $data
}	
#
# Регистрирует пользователся (возможно удаленном) тестовом CA, используя
# скрипт testca установленный в PATH на CAhost.
#

proc registerUserAtCA {userdir CAhost CAprefix CApath} {
		global OPENSSL_APP
		log "registerUserAtCA $userdir $CAhost $CAprefix $CApath"
		set f [open  $userdir/req.pem]
		set request [read $f]
		close $f
		set token [::http::geturl http://$CAhost/$CAprefix/$CApath\
		-query [::http::formatQuery request $request startdate [clock\
		format [expr [clock seconds]-3600] -format "%y%m%d%H%M%SZ" -gmt y]]]
		if {[::http::ncode $token]!=200} {
			return -code error "Error certifying request [::http::data $token]"
		}
		log "Got a certificate. Saving"
		saveCertFromPKCS7 $userdir/cert.pem [::http::data $token]
}
proc saveCertFromPKCS7 {file pkcs7} {
		global OPENSSL_APP
		log saveCertFromPCS7
		log "$OPENSSL_APP pkcs7 -print_certs $pkcs7"
		set f [open "|[list $OPENSSL_APP pkcs7 -print_certs << $pkcs7]" r]
		set out [open $file w]
		set mode 0
		while {[gets $f line]>=0} {
			if {$mode==1} {
				puts $out $line
				if {$line eq "-----END CERTIFICATE-----"} {
					set mode 2
				}
			} elseif {$mode==0 && $line eq "-----BEGIN CERTIFICATE-----"} {
				set mode 1
				puts $out $line
			}
		}	
		close $f
		close $out
		if {$mode !=2 } {
			return -code error "Cannot get certificate from PKCS7 output"
		}	
}
#
# Invokes scp and discards stderr output if exit code is 0
#
proc scp {args} {
	if {[info exists env(SCP)]} {
		set scp $env(SCP)
	} else {
		set scp scp
	}	
	if {[catch [concat exec $scp $args] msg]} {
		if {[string match CHIDLD* [lindex $::errorCode 0]]} {
			return -code error -errorcode $::errorCode  $msg
		}
	}
}	

proc getCAAlgParams {CAhost CAprefix alg} {
 	if {$alg == "ec" || $alg == "dsa"} {
		set token [::http::geturl http://$CAhost/$CAprefix/$alg?algparams=1]
		if {[::http::ncode $token]!=200} {
			return -code error "Error getting algorithm parameters [::http::data $token]"
		}
		set f [open ${alg}params.pem w]
		puts $f [::http::data $token]
		close $f
	}
}	
#
# Copies CA certificate from specified CA into ca_$alg.pem
# Returns name of the ca certificate or empty line if something goes
# wrong and error wasn't properly detected
#
proc getCAcert {CAhost CApath alg} {
	set token [::http::geturl http://$CAhost$CApath/$alg?getroot=1]
	if {[::http::ncode $token]!=200} {
		return -code error "Error getting root cert for $alg: [::http::data $token]"
	}
	saveCertFromPKCS7 ca_$alg.pem [::http::data $token]	
	return ca_$alg.pem
}
#
# Returns decoded version of first pem object in the given file
#
proc readpem {filename} {
	set f [open $filename]
	fconfigure $f -translation binary
	set data [read $f]
	close $f
	if {[regexp -- "-----BEGIN \[^\n\]+-----\r?\n(.*\n)-----END" $data => b64]} {
		set data [::base64::decode $b64]
	}  
	return $data

}
	
proc der_from_pem {pem} {
	if {[regexp -- {^-----BEGIN ([^\n]*)-----\r?\n(.*)\r?\n-----END \1-----} $pem => => base64]} {
		::base64::decode $base64
	} {
		error "Not a PEM:\n$pem"
	}
}

proc test_target_name {} {
	global env
	if {[info exists env(TEST_TARGET_NAME)]} {
		switch -exact $env(TEST_TARGET_NAME) {
			"open" {return "open"}
			"gost" {return "open"}
			"gostprov" {return "openprov"}
			"cryptocom" {return "ccore"}
			"ccore" {return "ccore"}
			default {error "Unknown engine '$env(TEST_TARGET_NAME)'"}
		}
	} else {
		return "ccore"
	}
}

proc openssl_remote {files host cmdlinex suffix} {
		set hostname [exec hostname]
		set workpath /tmp/$hostname/$suffix
		save_env {LD_LIBRARY_PATH OPENSSL_CONF ENGINE_DIR}
		exec ssh build@$host mkdir -p $workpath
		foreach file $files {
			exec scp -r $file build@$host:$workpath
		}
		exec scp ../opnssl.sh build@$host:$workpath
		exec ssh build@$host chmod +x $workpath/opnssl.sh
		set cmdline [string map "TESTPATH $workpath" $cmdlinex]
		log "hstname: $hostname OpenSSL cmdline: $host remote_openssl $cmdline"
		set f [open "| ssh build@$host $workpath/opnssl.sh $cmdline" r]
		set output [read $f]
		restore_env
		if {[catch {close $f} msg]} {
			append output "STDERR CONTENTS:\n$msg"
			log $output
			if {[lindex $::errorCode 0]!="NONE"} {
				return -code error -errorcode $::errorCode $output
			}
		}
		return $output
}

package provide ossltest 0.7
