#!/usr/bin/python3
import os, sys, subprocess

CONFIGS = {
	"pi": {
		"experiments": [
			"impl-cipher-ver",
			"impl-cert-ver",
			"impl-kex-ver",
		],
		"setups": [
			"none",
			"client",
			"server",
		],
		"p2_hostname": "p2",
		"p2_addr": "192.168.3.14",
		"p2_ssh": "exp@p2",
		"p2_psw": "exp",
		"p2_repodir": "/home/exp/exp",
		"wattmeter": True,
		"perf": False,
		"p3_suffix": "",
		"p3_port_plain": 80,
		"p3_port_tls": 443,
	},
	"local": {
		"experiments": [
			"impl-cipher-ver",
			"impl-cert-ver",
			"impl-kex-ver",
		],
		"setups": [
			"none-local",
			"client-local",
			"server-local",
		],
		"p2_hostname": "localhost",
		"p2_addr": "127.0.0.1",
		"p2_repodir": "/home/tuxmain/reps/tlsbench",
		"wattmeter": False,
		"perf": True,
		"perf_dir": "/home/tuxmain/.cache/exp",
		"p3_suffix": ".localhost",
		"p3_port_plain": 8080,
		"p3_port_tls": 8443,
	}
}

REPODIR = "/home/tuxmain/reps/tlsbench"
P2_REPODIR = "/home/exp/exp"
EXPDIR = "/dev/shm/exp"
LOG_BACKUP_DIR = "/home/tuxmain"
DOMAINS_ = [
	# Apple
	"apple.com", "www.apple.com", "graffiti-tags.apple.com", "securemetrics.apple.com",
	"store.storeimages.cdn-apple.com",
	"mzstatic.com", "is1-ssl.mzstatic.com",
	# Youtube video
	"youtube.com", "www.youtube.com",
	"i.ytimg.com",
	"fonts.gstatic.com", "www.gstatic.com",
	"www.google.com", "accounts.google.com",
	"yt3.ggpht.com",
	"rr1---sn-gxo5uxg-jqbl.googlevideo.com",
	"rr2---sn-gxo5uxg-jqbl.googlevideo.com",
	"rr4---sn-q4fl6nds.googlevideo.com",
	# Amazon
	"amazon.com", "www.amazon.com",
	# Wikipedia article
	"fr.wikipedia.org", "upload.wikimedia.org",
	# Google search
	"www.google.com", "www.googleadservices.com", "www.gstatic.com", "csp.withgoogle.com", "ogads-pa.clients6.google.com", "play.google.com", "ssl.gstatic.com", "fonts.gstatic.com", "ogs.google.com",
	# Peertube video
	"videos.domainepublic.net",
]

RECORDS = [
	#{ "filename": "youtube", "repeat": 100 },
	#{ "filename": "peertube", "repeat": 10 },
	{ "filename": "wikipedia", "repeat": 100 },
	#{ "filename": "apple", "repeat": 1000 },
	#{ "filename": "google", "repeat": 1000 },
]
CERT_SIGN_ALGS = [
	"prime256v1", # widely used
	"secp384r1", # rarely used but supported by browsers because it's NIST standard
	#"secp521r1", # not supported by browsers because NIST said it was not needed
	"rsa2048", "rsa3072", "rsa4096", # widely used
]
IMPLS = [
	"aws_lc_rs", # Amazon's Rust crypto widely used in Rust stuff
	"boring", # Google's fork of OpenSSL used in Chrome and Android
	"openssl", # widely used
	"ring", # used in most Rust stuff
	"symcrypt", # Microsoft's crypto
	#"wolfcrypt" # used in embedded (won't build with rpxy for now)
]
# Symmetric ciphers
# They also allow to choose the TLS version.
CIPHERS = [
	# TLS 1.3
	"AES_256_GCM_SHA384",
	"AES_128_GCM_SHA256",
	"CHACHA20_POLY1305_SHA256",
	# TLS 1.2
	# ECDSA vs RSA refers to the certificate signature algorithm.
	# DH is EC in either case, using the group specified below.
	"ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,ECDHE_RSA_WITH_AES_256_GCM_SHA384",
	"ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256",
	"ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
]
KEXES = [
	"X25519",
	"SECP256R1",
	"SECP384R1",
]
IDLE = "idle - - - - - - 600.000081539154 0.0 896 4792 0.5399999999999991"

# Testing all combinations would be too much. Instead we isolate independent parts.
EXPERIMENTS = {
	# Compare ciphers among implementations and TLS versions
	"impl-cipher-ver": {
		"impls": IMPLS,
		"records": RECORDS,
		"ciphers": [
			"AES_128_GCM_SHA256",
			"AES_256_GCM_SHA384",
			"CHACHA20_POLY1305_SHA256",
			#"ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256",
			#"ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,ECDHE_RSA_WITH_AES_256_GCM_SHA384",
			#"ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
		],
		"kexes": ["X25519"],
		"cert": ["prime256v1"],
	},
	# Compare signatures among implementations and TLS versions
	"impl-cert-ver": {
		"impls": IMPLS,
		"records": RECORDS,
		"ciphers": [
			"AES_128_GCM_SHA256",
			#"ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256",
		],
		"kexes": ["X25519"],
		"cert": [
			"prime256v1",
			"secp384r1",
			"rsa2048",
			"rsa3072",
			"rsa4096",
		],
	},
	# Compare key exchange groups among implementations and TLS versions
	"impl-kex-ver": {
		"impls": IMPLS,
		"records": RECORDS,
		"ciphers": [
			"AES_128_GCM_SHA256",
			#"ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256",
		],
		"kexes": ["X25519", "SECP256R1", "SECP384R1"],
		"cert": ["prime256v1"],
	},
	"debug": {
		"impls": IMPLS,
		"records": RECORDS,
		"ciphers": [
			"ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256",
			#"ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
		],
		"kexes": ["X25519"],
		"cert": ["prime256v1"],
	},
}

DOMAINS = []
for domain in DOMAINS_:
	if not domain in DOMAINS:
		DOMAINS.append(domain)

# JS to redirect the target domains to local (bypass DNS without altering system's config or webpages or packets)
SCRIPT_FIREFOX_HOSTS = """const gOverride = Cc["@mozilla.org/network/native-dns-override;1"].getService(Ci.nsINativeDNSResolverOverride);
gOverride.clearOverrides();
var names = """+str(DOMAINS)+""";
for(var i in names) {
	gOverride.addIPOverride(names[i], "127.0.0.1");
}
"""

def getargv(arg:str, default="", n:int=1, args:list=sys.argv):
	if arg in args and len(args) > args.index(arg)+n:
		return args[args.index(arg)+n]
	else:
		return default

def sh(cmds):
	if type(cmds) == list:
		for cmd in cmds:
			print(cmd)
			os.system(cmd)
	elif type(cmds) == str:
		print(cmds)
		os.system(cmds)
	else:
		raise TypeError

def make_sk(outpath, alg):
	sh({
		"ed25519": [
			f"openssl genpkey -out {outpath}.sec1 -algorithm ed25519",
			f"openssl pkcs8 -topk8 -nocrypt -in {outpath}.sec1 -out {outpath} -outform PEM"
		],
		"prime256v1": [
			f"openssl ecparam -genkey -name prime256v1 -noout -out {outpath}.sec1",
			f"openssl pkcs8 -topk8 -nocrypt -in {outpath}.sec1 -out {outpath} -outform PEM"
		],
		"rsa2048": [
			f"openssl genrsa -out {outpath} 2048"
		],
		"rsa3072": [
			f"openssl genrsa -out {outpath} 3072"
		],
		"rsa4096": [
			f"openssl genrsa -out {outpath} 4096"
		],
		"secp384r1": [
			f"openssl ecparam -genkey -name secp384r1 -noout -out {outpath}.sec1",
			f"openssl pkcs8 -topk8 -nocrypt -in {outpath}.sec1 -out {outpath} -outform PEM"
		],
	}[alg])

def make_ca_cert(outpath, skpath):
	sh(f"openssl req -x509 -new -key {skpath} -sha256 -days 730 -out {outpath} -subj '/CN=Foobar Root CA/C=AT/ST=Lyon/L=Lyon/O=Foobar'")

def make_cert(outpath, skpath, capath, caskpath, name, domains):
	sh(f"openssl req -new -key {skpath} -sha256 -out /tmp/tmp.csr -subj '/CN={name}/C=AT/ST=Lyon/L=Lyon/O={name}'")
	ext = open("/tmp/tmp.v3.ext", "w")
	ext.write("""authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
""")
	i = 1
	for domain in domains:
		ext.write(f"DNS.{i} = {domain}\n")
		i += 1
		ext.write(f"DNS.{i} = {domain}.localhost\n")
		i += 1
		ext.write(f"DNS.{i} = {domain}.p2\n")
		i += 1
		ext.write(f"DNS.{i} = {domain}.p3\n")
		i += 1
	ext.close()
	
	sh(f"openssl x509 -req -in /tmp/tmp.csr -CA {capath} -CAkey {caskpath} -CAcreateserial -out {outpath} -days 365 -sha256 -extfile /tmp/tmp.v3.ext")

def get_domain_root(domain):
	last_dot = domain.rfind(".")
	penultimate_dot = domain.rfind(".", 0, last_dot)
	return domain[penultimate_dot+1:]

# Issue secret keys, CA cert and signed certs for given domains
# All using the same algorithm
def make_certs(outdir, domains, alg, make_ca):
	if outdir[-1] != "/":
		outdir += "/"
	
	if make_ca:
		make_sk(outdir+"ca.key", alg)
		make_ca_cert(outdir+"ca.crt", outdir+"ca.key")
	
	# Only make certs for root domains, and include subdomains
	roots = {}
	for domain in domains:
		root = domain
		if domain.count(".") > 1:
			root = get_domain_root(domain)
		if root in roots:
			roots[root].append(domain)
		else:
			roots[root] = [domain]
	for root in roots:
		make_sk(outdir+root+".key", alg)
		make_cert(outdir+root+".crt", outdir+root+".key", outdir+"ca.crt", outdir+"ca.key", root, roots[root])
	
	# Make a cert for all domains because choosing a certificate as a proxy is a real pain
	make_sk(outdir+"all.key", alg)
	make_cert(outdir+"all.crt", outdir+"all.key", outdir+"ca.crt", outdir+"ca.key", "wikipedia.org", domains)

RPXY_CONFIGS = {
	"plain": {
		"listen_http": 80,
		"listen_https": 443,
		"app": """[apps.{app}]
server_name = "{domain}"
tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }}
reverse_proxy = [{{ upstream = [{{ location = "{domain}{p3_suffix}:{p3_port_plain}" }}], set_host = "{domain}"}}]
[apps.{app}_localhost]
server_name = "{domain}.localhost"
tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }}
reverse_proxy = [{{ upstream = [{{ location = "{domain}{p3_suffix}:{p3_port_plain}" }}], set_host = "{domain}"}}]
[apps.{app}_p2]
server_name = "{domain}.p2"
tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }}
reverse_proxy = [{{ upstream = [{{ location = "{domain}{p3_suffix}:{p3_port_plain}" }}], set_host = "{domain}"}}]
"""
	},
	"tls": {
		"listen_http": 80,
		"listen_https": 443,
		"app": """[apps.{app}]
server_name = "{domain}"
tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }}
reverse_proxy = [{{ upstream = [{{ location = "{domain}{p3_suffix}:{p3_port_tls}", tls = true }}], set_host = "{domain}"}}]
[apps.{app}_localhost]
server_name = "{domain}.localhost"
tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }}
reverse_proxy = [{{ upstream = [{{ location = "{domain}{p3_suffix}:{p3_port_tls}", tls = true }}], set_host = "{domain}"}}]
[apps.{app}_p2]
server_name = "{domain}.p2"
tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }}
reverse_proxy = [{{ upstream = [{{ location = "{domain}{p3_suffix}:{p3_port_tls}", tls = true }}], set_host = "{domain}"}}]
"""
	},
}

SETUPS = {
	"none": {
		"rpxy_config": "plain",
		"netreplay_tls_mode": "none",
		"p2_port": 80,
		"listen_port": 80,
		"tls_invariant": True,
	},
	"client": {
		"rpxy_config": "tls",
		"netreplay_tls_mode": "server",
		"p2_port": 80,
		"listen_port": 443,
		"tls_invariant": False,
	},
	"server": {
		"rpxy_config": "plain",
		"netreplay_tls_mode": "client",
		"p2_port": 443,
		"listen_port": 80,
		"tls_invariant": False,
	},
	"both": {
		"rpxy_config": "tls",
		"netreplay_tls_mode": "both",
		"p2_port": 443,
		"listen_port": 443,
		"tls_invariant": False,
	},
	"none-local": {
		"rpxy_config": "plain",
		"netreplay_tls_mode": "none",
		"p2_port": 80,
		"listen_port": 8080,
		"tls_invariant": True,
	},
	"client-local": {
		"rpxy_config": "tls",
		"netreplay_tls_mode": "server",
		"p2_port": 80,
		"listen_port": 8443,
		"tls_invariant": False,
	},
	"server-local": {
		"rpxy_config": "plain",
		"netreplay_tls_mode": "client",
		"p2_port": 443,
		"listen_port": 8080,
		"tls_invariant": False,
	},
	"both-local": {
		"rpxy_config": "tls",
		"netreplay_tls_mode": "both",
		"p2_port": 443,
		"listen_port": 8443,
		"tls_invariant": False,
	},
}

def make_rpxy_config(outdir, domains, cryptodir, config_name, p3_suffix, p3_port_plain, p3_port_tls):
	if outdir[-1] != "/":
		outdir += "/"
	if cryptodir[-1] != "/":
		cryptodir += "/"
	
	rpxy_config = RPXY_CONFIGS[config_name]
	f = open(outdir+config_name+".toml", "w")
	f.write("listen_port = {}\nlisten_port_tls = {}\n".format(rpxy_config["listen_http"], rpxy_config["listen_https"]))
	for domain in domains:
		app = domain.replace(".", "_")
		root = get_domain_root(domain)
		f.write(rpxy_config["app"].format(
			app=app,
			domain=domain,
			cert=cryptodir+root+".crt",
			key=cryptodir+root+".key",
			p3_suffix=p3_suffix,
			p3_port_plain=p3_port_plain,
			p3_port_tls=p3_port_tls,
		))
	f.close()

def make_everything(expdir, domains, make_ca, p3_suffix, p3_port_plain, p3_port_tls):
	os.makedirs(expdir, exist_ok=True)
	if expdir[-1] != "/":
		expdir += "/"
	for alg in CERT_SIGN_ALGS:
		algdir = expdir+"certs/"+alg
		os.makedirs(algdir, exist_ok=True)
		make_certs(algdir, domains, alg, make_ca)
	# this will be a symbolic link to the chosen certs directory
	cryptodir = expdir+"current_certs"
	configdir = expdir+"configs/"
	os.makedirs(configdir, exist_ok=True)
	for config_name in RPXY_CONFIGS:
		make_rpxy_config(configdir, domains, cryptodir, config_name, p3_suffix, p3_port_plain, p3_port_tls)

def choose_cert_alg(expdir, alg):
	if expdir[-1] != "/":
		expdir += "/"
	try:
		os.unlink(expdir+"current_certs")
	except FileNotFoundError as e:
		pass
	# WHY is dst pointing to src?????
	os.symlink(expdir+"certs/"+alg, expdir+"current_certs", True)

def choose_impl(expdir, p, impl):
	if expdir[-1] != "/":
		expdir += "/"
	os.symlink(os.getcwd()+"/rpxy_rustls_"+impl, expdir+str(p)+"_rpxy", False)

def run_netreplay(expdir, repodir, record, p2_addr, p2_port, listen_port, tls_mode, only_record=None, ciphers=None, kexes=None):
	if expdir[-1] != "/":
		expdir += "/"
	repodir = repodir.removesuffix("/")
	env = {"RUST_LOG": "warning"}
	if ciphers:
		env["CIPHERS"] = ciphers
	if kexes:
		env["KEXES"] = kexes
	cmd = [repodir+"/netreplay", repodir+"/records/"+record["filename"], "play", p2_addr, str(p2_port), str(listen_port), expdir+"current_certs", tls_mode, "-r", str(record["repeat"])]
	if only_record != None:
		cmd += ["--record", only_record]
	print(" ".join(cmd))
	return subprocess.Popen(cmd, env=env)

# Run with or without SSH
def ssh_run(ssh, cmd, env={}, **kwargs):
	if ssh == None:
		# As long as there is no argument containing a space (escaped or quoted), we're fine
		proc = subprocess.Popen(cmd.split(" "), stdout=subprocess.PIPE, env=env)
		proc.wait()
		return proc.stdout.read().decode()
	else:
		strvars = ""
		for var in env:
			strvars += var+"="+env[var]+" "
		return ssh.run(strvars+cmd, **kwargs).stdout

# Run with or without SSH
def ssh_run_bg(ssh, cmd, env={}, **kwargs):
	if ssh == None:
		# As long as there is no argument containing a space (escaped or quoted), we're fine
		print(cmd)
		return subprocess.Popen(cmd.split(" "), env=env)
	else:
		# https://stackoverflow.com/questions/8775598/how-to-start-a-background-process-with-nohup-using-fabric
		strvars = ""
		for var in env:
			strvars += "export "+var+"="+env[var].replace(" ", "\\ ")+" && "
		return ssh.run(f"{strvars}dtach -n `mktemp -u /tmp/dtach.XXXX` {cmd}", **kwargs)

def get_cpu_stat(ssh):
	res = ssh_run(ssh, "/sbin/sa --list-all-names", hide=True)
	for line in res.split("\n"):
		if "rpxy" in line:
			return float(re.finditer("\\s(\\d+\\.\\d+)cp\\s", line).__next__().group(1))
	return 0.0

def get_net_stat(ssh):
	res = ssh_run(ssh, "cat /proc/net/netstat", hide=True)
	items = res.split("\n")[3].split(" ")
	bytes_in = int(items[7])
	bytes_out = int(items[8])
	return (bytes_in, bytes_out)

def run_exp(expdir, config, only_record=None, idle=False):
	ssh = None
	if "p2_ssh" in config:
		ssh = connect_ssh(config)

	p2_path = config["p2_repodir"]
	wattmeter = None
	if config["wattmeter"]:
		errmsg = YRefParam()
		if YAPI.RegisterHub("usb", errmsg) != YAPI.SUCCESS:
			sys.exit("init error" + errmsg.value)
		wattmeter = YPower.FirstPower()
		if wattmeter is None or not wattmeter.isOnline():
			print("No YoctoWatt connected")
			exit(1)
	
	sh("killall netreplay")
	for expname in config["experiments"]:
		exp = EXPERIMENTS[expname]
		for impl in exp["impls"]:
			try:
				ssh_run(ssh, f"killall rpxy_rustls_{impl}")
			except invoke.exceptions.UnexpectedExit as e:
				pass
	timestr = str(int(time.time()))
	logfile_name = "log-"+timestr
	logfile_path = expdir+"/"+logfile_name
	logfile = open(logfile_path, "w")
	logfile.write("exp impl alg kex cipher setup record time cpu bytes_in bytes_out Wh prof\n")
	logfile.close()

	perf_dir = ""
	if config["perf"]:
		perf_dir = config["perf_dir"]
		os.makedirs(perf_dir, exist_ok=True)

	if idle:
		print("Measuring idle...")
		rpxy_cpu = get_cpu_stat(ssh)
		p2_bytes_in, p2_bytes_out = get_net_stat(ssh)
		energy = 0
		if config["wattmeter"]:
			energy = wattmeter.get_meter()
		start = time.time()

		time.sleep(600)

		end = time.time()
		new_energy = 0
		if config["wattmeter"]:
			new_energy = wattmeter.get_meter()
		new_p2_bytes_in, new_p2_bytes_out = get_net_stat(ssh)
		new_rpxy_cpu = get_cpu_stat(ssh)
		rpxy_cpu_diff = new_rpxy_cpu - rpxy_cpu
		p2_bytes_in_diff = new_p2_bytes_in - p2_bytes_in
		p2_bytes_out_diff = new_p2_bytes_out - p2_bytes_out
		energy_diff = new_energy - energy
		time_diff = end - start
		while True:
			try:
				with open(logfile_path, "a") as logfile:
					logfile.write(f"idle - - - - - - {time_diff} {rpxy_cpu_diff} {p2_bytes_in_diff} {p2_bytes_out_diff} {energy_diff} -\n")
					logfile.close()
					break
			except Exception as e:
				print("Can't open log file:", e)
				time.sleep(1)
	else:
		while True:
			try:
				with open(logfile_path, "a") as logfile:
					logfile.write(IDLE+"\n")
					logfile.close()
					break
			except Exception as e:
				print("Can't open log file:", e)
				time.sleep(1)
	sh(f"cp {logfile_path} {LOG_BACKUP_DIR}/{logfile_name}")

	run_id = 0
	for expname in config["experiments"]:
		exp = EXPERIMENTS[expname]
		first_set = True
		for impl in exp["impls"]:
			for alg in exp["cert"]:
				for kex in exp["kexes"]:
					for cipher in exp["ciphers"]:
						choose_cert_alg(expdir, alg)
						ssh_run(ssh, f"python {p2_path}/exp.py cert {alg}")
						for setup in config["setups"]:
							if SETUPS[setup]["tls_invariant"] and not first_set:
								continue
							setupdir = expdir+"setups/"+setup
							for record in exp["records"]:
								print(f"EXPERIMENT {expname}: {impl} {alg} {kex} {cipher} {setup}")
								p2_rpxy_config = SETUPS[setup]["rpxy_config"]
								vars = {"CIPHERS": cipher, "KEXES": kex, "RUST_LOG": "warning"}
								cmd = f"{p2_path}/rpxy_rustls_{impl} --config {expdir}/configs/{p2_rpxy_config}.toml --log-dir /dev/shm"
								#cmd = f"{p2_path}/rpxy_rustls_{impl} --config {expdir}/configs/{p2_rpxy_config}.toml"
								ssh_run_bg(ssh, cmd, env=vars)
								time.sleep(1)

								prof_filename = "-"
								if config["perf"]:
									prof_filename = f"{perf_dir}/perf-{timestr}-{run_id}.data"
									rpxy_pid = ssh_run(ssh, f"pidof rpxy_rustls_{impl}").removesuffix("\n")
									ssh_run_bg(ssh, f"perf record -F 997 --call-graph dwarf,64000 -g -o {prof_filename} -p {rpxy_pid}")
								
								run_id += 1
								
								rpxy_cpu = get_cpu_stat(ssh)
								p2_bytes_in, p2_bytes_out = get_net_stat(ssh)
								energy = 0
								if config["wattmeter"]:
									energy = wattmeter.get_meter()
								
								start = time.time()
								netreplay = run_netreplay(expdir, REPODIR, record, config["p2_addr"], SETUPS[setup]["p2_port"], SETUPS[setup]["listen_port"], SETUPS[setup]["netreplay_tls_mode"], only_record=only_record, ciphers=cipher, kexes=kex)
								
								# TODO detect when netreplay has finished
								try:
									netreplay.wait()
								except KeyboardInterrupt:
									netreplay.kill()
									try:
										ssh_run(ssh, f"killall rpxy_rustls_{impl}")
									except invoke.exceptions.UnexpectedExit as e:
										pass
									try:
										ssh_run(ssh, f"killall dtach")
									except invoke.exceptions.UnexpectedExit as e:
										pass
									exit(0)
								
								#time.sleep(30)
								#sh("killall netreplay")
								try:
									ssh_run(ssh, f"rm /dev/shm/access.log /dev/shm/rpxy.log")
									pass
								except invoke.exceptions.UnexpectedExit as e:
									pass
								try:
									ssh_run(ssh, f"killall rpxy_rustls_{impl}")
								except invoke.exceptions.UnexpectedExit as e:
									pass
								try:
									ssh_run(ssh, f"killall dtach")
								except invoke.exceptions.UnexpectedExit as e:
									pass
								end = time.time()
								
								new_energy = 0
								if config["wattmeter"]:
									new_energy = wattmeter.get_meter()
								new_p2_bytes_in, new_p2_bytes_out = get_net_stat(ssh)
								new_rpxy_cpu = get_cpu_stat(ssh)
								record_filename = record["filename"]
								rpxy_cpu_diff = new_rpxy_cpu - rpxy_cpu
								p2_bytes_in_diff = new_p2_bytes_in - p2_bytes_in
								p2_bytes_out_diff = new_p2_bytes_out - p2_bytes_out
								energy_diff = new_energy - energy
								time_diff = end - start
								while True:
									try:
										with open(logfile_path, "a") as logfile:
											logfile.write(f"{expname} {impl} {alg} {kex} {cipher} {setup} {record_filename} {time_diff} {rpxy_cpu_diff} {p2_bytes_in_diff} {p2_bytes_out_diff} {energy_diff} {prof_filename}\n")
											logfile.close()
											break
									except Exception as e:
										print("Can't open log file:", e)
										time.sleep(1)
								sh(f"cp {logfile_path} {LOG_BACKUP_DIR}/{logfile_name}")
							first_set = False
	if config["wattmeter"]:
		YAPI.FreeAPI()

def update_certs():
	info = platform.freedesktop_os_release()
	dist = info.get("ID_LIKE", info["ID"])
	if dist == "debian":
		for alg in CERT_SIGN_ALGS:
			sh([
				f"sudo cp {EXPDIR}/certs/{alg}/ca.crt /usr/local/share/ca-certificates/ca-{alg}.crt",
				f"sudo chmod 644 /usr/local/share/ca-certificates/ca-{alg}.crt",
				f"sudo chown root:root /usr/local/share/ca-certificates/ca-{alg}.crt"
			])
		sh("sudo update-ca-certificates")
	elif dist == "arch":
		for alg in CERT_SIGN_ALGS:
			sh([
				f"sudo cp {EXPDIR}/certs/{alg}/ca.crt /etc/ca-certificates/trust-source/anchors/ca-{alg}.crt",
				f"sudo chmod 644 /etc/ca-certificates/trust-source/anchors/ca-{alg}.crt",
				f"sudo chown root:root /etc/ca-certificates/trust-source/anchors/ca-{alg}.crt"
			])
		sh("sudo update-ca-trust extract")

# copy local dir src to remote parent dir dst (src's copy will be a subdir of dst)
def upload_dir(ssh, src, dst):
	src = src.removesuffix("/")
	src_name = src.split("/")[-1]
	os.chdir(f"{src}/..")
	os.system(f"tar -czf {src}/../tmp.tar.gz {src_name}")
	print(ssh.put(src+"/../tmp.tar.gz", dst))
	print(ssh.run(f"cd {dst} && tar -xf tmp.tar.gz"))

def connect_ssh(config):
	ssh_passphrase = "--passphrase" in sys.argv
	connect_kwargs = {}
	if ssh_passphrase:
		import getpass
		connect_kwargs["passphrase"] = getpass.getpass("Enter passphrase for SSH key: ")
	if "p2_psw" in config:
		connect_kwargs["password"] = config["p2_psw"]
	ssh = fabric.Connection(config["p2_ssh"], connect_kwargs=connect_kwargs)
	return ssh

if __name__ == "__main__":
	if len(sys.argv) < 2 or sys.argv[1] in ["h", "help", "?", "-h", "-help", "--help", "/?"]:
		print("""Options:
  make [-c]        Create everything
  cert <alg>       Select cert signature algorithm
  send <config>    Send configs and certs to p2
  update-certs     Update system's certs
  run <config>     Run experiment
  script           Print Firefox script to override DNS

Make options:
  -c  Make CA cert (otherwise use already existing one)

Cert options:
  <alg>            One of: {sig_algs}

Send options:
  <config>         One of: {configs}

Run options:
  <config>         One of: {configs}
  --passphrase     Prompt SSH key decryption passphrase (when using pubkey login)
  --count          Do not run experiments but display number of experiments
  --record <id>    Only play this record
  --idle           Also measure when idle
""".format(
	sig_algs = " ".join(CERT_SIGN_ALGS),
	configs = " ".join([i for i in CONFIGS]),
))
		exit()
	
	opt = sys.argv[1]
	if opt == "make":
		config = CONFIGS[sys.argv[2]]
		make_ca = "-c" in sys.argv
		make_everything(EXPDIR, DOMAINS, make_ca, config["p3_suffix"], config["p3_port_plain"], config["p3_port_tls"])
	elif opt == "cert":
		alg = sys.argv[2]
		if not alg in CERT_SIGN_ALGS:
			print("Error: alg must be in", CERT_SIGN_ALGS)
			exit(1)
		choose_cert_alg(EXPDIR, alg)
	elif opt == "send":
		config = CONFIGS[sys.argv[2]]
		import fabric
		ssh = connect_ssh(config)
		upload_dir(ssh, EXPDIR, "/dev/shm")
	elif opt == "update-certs":
		import platform
		update_certs()
	elif opt == "run":
		config = CONFIGS[sys.argv[2]]
		if "--count" in sys.argv:
			exps = 0
			for expname in config["experiments"]:
				exp = config["experiments"][expname]
				exps += len(exp["impls"]) * len(exp["cert"]) * len(exp["kexes"]) * len(exp["ciphers"]) * len(exp["records"])
			print("Experiments to make:", exps * len(config["setups"]))
			exit(0)
		
		import time
		import invoke
		import re
		import fabric
		
		if config["wattmeter"]:
			import yoctopuce
			from yoctopuce.yocto_api import *
			from yoctopuce.yocto_power import *

		run_exp(EXPDIR, config, only_record=getargv("--record", None), idle="--idle" in sys.argv)
	elif opt == "script":
		print(SCRIPT_FIREFOX_HOSTS)
	else:
		print("Unknown command, use help for help")
		exit(1)
