#!/usr/bin/python3
# OLD VERSION
# using 3 proxies between actual browser and actual server

import os, sys, subprocess

REPODIR = "/home/tuxmain/Documents/doc/tlsbench"
P2_SSH = "exp@p2"
P2_PSW = "exp"
P2_REPODIR = "/home/exp/exp"
EXPDIR = "/dev/shm/exp"
DOMAINS_ = [
	# Apple
	"apple.com", "www.apple.com", "graffiti-tags.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.google.com", "accounts.google.com", "yt3.ggpht.com", "www.gstatic.com", "fonts.googleapis.com", "www.googleadservices.com",
	# W3C
	"w3.org", "www.w3.org",
	# Amazon
	"amazon.com", "www.amazon.com",
	# JA3 fingerprint test
	"www.edgecomputing.live",
	# JA4 fingerprint test
	"ja4db.com",
	# Local test
	"foo.home",
	# Peertube
	"flim.txmn.tk",
]

#URL = "https://www.youtube.com/watch?v=IBWezPN4Ep8"
#URL = "https://www.edgecompute.live/tls/ja3"
#URL = "https://ja4db.com/id/ja4/"
#URL = "https://www.w3.org/"
#URL = "http://foo.home:8080/?hello=hi"
#URL = "https://www.apple.com/"
#URL = "https://www.amazon.com/"
URL = "https://flim.txmn.tk/w/4zpfvGB72oTL4hcSqdAwx8"

URLS = [
	# Heavy showcase website
	"https://www.apple.com/",
	# Light showcase website
	"https://librezo.fr/",
	# Long Wikipedia article
	"https://fr.wikipedia.org/wiki/Sp%C3%A9cial:Recherche?search=Victor+Hugo&sourceid=Mozilla-search",
	# Youtube video
	"https://www.youtube.com/watch?v=IBWezPN4Ep8",
	# Peertube video
	"https://videos.domainepublic.net/videos/watch/eaee7866-d209-4e5c-b7b0-443395b79c82",
	# Google search
	"https://www.google.com/search?q=where+do+birds+go+when+it+rains"
]

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 is aither 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",
]

# 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,
		"urls": [URL],
		"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,
		"urls": [URL],
		"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,
		"urls": [URL],
		"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"],
	},
}

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:str="", n:int=1, args:list=sys.argv) -> str:
	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:
			os.system(cmd)
	elif type(cmds) == str:
		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):
	if outdir[-1] != "/":
		outdir += "/"
	
	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])

RPXY_CONFIGS = {
	"p1_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}.p2:42002" }}]}}]
[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}.p2:42002" }}]}}]
"""
	},
	"p1_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}.p2:43002", tls = true }}], set_host = "{domain}.p2"}}]
[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}.p2:43002", tls = true }}], set_host = "{domain}.p2"}}]
"""
	},
	"p2_plain": {
		"listen_http": 42002,
		"listen_https": 43002,
		"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:42003" }}], set_host = "{domain}.p3"}}]
[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:42003" }}], set_host = "{domain}.p3"}}]
[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:42003" }}], set_host = "{domain}.p3"}}]
"""
	},
	"p2_tls": {
		"listen_http": 42002,
		"listen_https": 43002,
		"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:43003", tls = true }}], set_host = "{domain}.p3"}}]
[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:43003", tls = true }}], set_host = "{domain}.p3"}}]
[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:43003", tls = true }}], set_host = "{domain}.p3"}}]
"""
	},
	"p3": {
		"listen_http": 42003,
		"listen_https": 43003,
		"app": """[apps.{app}]
server_name = "{domain}"
tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }}
reverse_proxy = [{{ upstream = [{{ location = "{domain}", 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}", tls = true }}], set_host = "{domain}"}}]
[apps.{app}_p3]
server_name = "{domain}.p3"
tls = {{ tls_cert_path = "{cert}", tls_cert_key_path = "{key}", https_redirection = false }}
reverse_proxy = [{{ upstream = [{{ location = "{domain}", tls = true }}], set_host = "{domain}"}}]
"""
	},
}

SETUPS = {
	#"none": {
	#	"rpxy_configs": ["p1_plain", "p2_plain", "p3"],
	#},
	#"client": {
	#	"rpxy_configs": ["p1_plain", "p2_tls", "p3"],
	#},
	"server": {
		"rpxy_configs": ["p1_tls", "p2_plain", "p3"],
	},
}

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

def make_everything(expdir, domains):
	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)
	# this will be a symbolic link to the chosen certs directory
	cryptodir = expdir+"current_certs"
	for setup in SETUPS:
		setupdir = expdir+"setups/"+setup
		os.makedirs(setupdir, exist_ok=True)
		make_rpxy_config(setupdir, domains, cryptodir)

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_rpxy(expdir, repodir, setup, p, impl, ciphers=None, kexes=None):
	if expdir[-1] != "/":
		expdir += "/"
	repodir = repodir.removesuffix("/")
	env = {"RUST_LOG": "debug"}
	if ciphers:
		env["CIPHERS"] = ",".join(ciphers)
	if kexes:
		env["KEXES"] = ",".join(KEXES)
	print("RUN", [repodir+"/rpxy_rustls_"+impl, "--config", expdir+"setups/"+setup+"/"+p+".toml"])
	return subprocess.Popen([repodir+"/rpxy_rustls_"+impl, "--config", expdir+"setups/"+setup+"/"+p+".toml"], env=env)

# https://stackoverflow.com/questions/8775598/how-to-start-a-background-process-with-nohup-using-fabric
def runbg(ssh, cmd, vars={}):
	strvars = ""
	for var in vars:
		strvars += f"export {var}="+vars[var]+" && "
	return ssh.run(f"{strvars}dtach -n `mktemp -u /tmp/dtach.XXXX` {cmd}")

def run_exp(ssh, expdir, p2_path, exps):
	from selenium import webdriver
	import time
	import invoke
	
	logfile = open(expdir+"/log-"+str(int(time.time())), "w")
	
	driver = webdriver.Firefox()
	driver.get("about:config")
	driver.execute_script(SCRIPT_FIREFOX_HOSTS)
	time.sleep(1)
	for expname in exps:
		exp = exps[expname]
		for impl in exp["impls"]:
			sh(f"killall rpxy_rustls_{impl}")
			try:
				ssh.run(f"killall rpxy_rustls_{impl}")
			except invoke.exceptions.UnexpectedExit as e:
				pass
		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(f"python {p2_path}/exp.py cert {alg}")
						for setup in SETUPS:
							print(f"EXPERIMENT {expname}: {impl} {alg} {kex} {cipher} {setup}")
							setupdir = expdir+"setups/"+setup
							p1 = run_rpxy(expdir, REPODIR, setup, SETUPS[setup]["rpxy_configs"][0], impl)
							p3 = run_rpxy(expdir, REPODIR, setup, SETUPS[setup]["rpxy_configs"][2], impl)
							p2_rpxy_config = SETUPS[setup]["rpxy_configs"][1]
							vars = {"CIPHERS": cipher, "KEXES": kex}
							
							runbg(ssh, f"{p2_path}/rpxy_rustls_{impl} --config {expdir}/setups/{setup}/{p2_rpxy_config}.toml --log-dir /dev/shm", vars)
							
							start = time.time()
							for url in exp["urls"]:
								driver.get(url)
								#time.sleep(20)
								input("???")
							end = time.time()
							logfile.write(f"{expname} {impl} {alg} {kex} {cipher} {setup} {start} {end}\n")
							
							# DO the experiment
							
							p1.kill()
							p3.kill()
							ssh.run(f"killall rpxy_rustls_{impl}")
							ssh.run(f"killall dtach")
						logfile.flush()
	driver.quit()

def update_certs():
	import platform
	dist = platform.freedesktop_os_release()["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 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 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():
	import fabric
	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 != None:
		connect_kwargs["password"] = P2_PSW
	ssh = fabric.Connection(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             Create everything
  cert <alg>       Select cert signature algorithm
  send
  update-certs
  run              Run experiment

Signature algorithms:
  {sig_algs}

Implementations:
  {impls}

Run options:
  --passphrase  Prompt SSH key decryption passphrase (when using pubkey login)
""".format(
	sig_algs=" ".join(CERT_SIGN_ALGS),
	impls=" ".join(IMPLS),
))
		exit()
	
	opt = sys.argv[1]
	if opt == "make":
		make_everything(EXPDIR, DOMAINS)
	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":
		ssh = connect_ssh()
		upload_dir(ssh, EXPDIR, "/dev/shm")
	elif opt == "update-certs":
		update_certs()
	elif opt == "run":
		ssh = connect_ssh()
		run_exp(ssh, EXPDIR, P2_REPODIR, EXPERIMENTS)
	else:
		print("Unknown command, use help for help")
		exit(1)
