diff --git a/.gitmodules b/.gitmodules
index 7e946b0..a41077f 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -22,3 +22,6 @@
[submodule "terraform-modules/terraform-aws-tor-bridge"]
path = terraform-modules/terraform-aws-tor-bridge
url = https://github.com/sr2c/terraform-aws-tor-bridge.git
+[submodule "terraform-modules/terraform-aws-bc-eotk"]
+ path = terraform-modules/terraform-aws-bc-eotk
+ url = https://github.com/sr2c/terraform-aws-bc-eotk.git
diff --git a/app/portal/templates/tables.html.j2 b/app/portal/templates/tables.html.j2
index a80d885..254947d 100644
--- a/app/portal/templates/tables.html.j2
+++ b/app/portal/templates/tables.html.j2
@@ -98,7 +98,7 @@
Preview Configuration
{% endif %}
-
+
{{ icon("terminal") }}
diff --git a/app/terraform/__init__.py b/app/terraform/__init__.py
index 3710683..09837af 100644
--- a/app/terraform/__init__.py
+++ b/app/terraform/__init__.py
@@ -1,9 +1,42 @@
import os
+import stat
from typing import Tuple, Any, Optional
+from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED
import jinja2
+class DeterministicZip:
+ """
+ Create a zip file deterministically.
+
+ Heavily inspired by https://github.com/bboe/deterministic_zip.
+ """
+ zipfile: ZipFile
+
+ def __init__(self, filename: str):
+ self.zipfile = ZipFile(filename, "w")
+
+ def __enter__(self) -> "DeterministicZip":
+ return self
+
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
+ self.zipfile.close()
+
+ def add_file(self, path: str, contents: bytes) -> None:
+ permission = 0o555 if os.access(path, os.X_OK) else 0o444
+ zip_info = ZipInfo()
+ zip_info.filename = path
+ zip_info.date_time = (2022, 1, 1, 0, 0, 0)
+ zip_info.external_attr = (stat.S_IFREG | permission) << 16
+ self.zipfile.writestr(
+ zip_info,
+ contents,
+ compress_type=ZIP_DEFLATED,
+ compresslevel=9,
+ )
+
+
class BaseAutomation:
short_name: str = "base"
description: str = "Abstract base automation."
@@ -37,3 +70,13 @@ class BaseAutomation:
tmpl = jinja2.Template(template)
with open(os.path.join(self.working_dir, filename), 'w', encoding="utf-8") as tfconf:
tfconf.write(tmpl.render(**kwargs))
+
+ def bin_write(self, filename: str, data: bytes, group_id: Optional[int] = None) -> None:
+ if not self.working_dir:
+ raise RuntimeError("No working directory specified.")
+ try:
+ os.mkdir(os.path.join(self.working_dir, str(group_id)))
+ except FileExistsError:
+ pass
+ with open(os.path.join(self.working_dir, str(group_id) if group_id else "", filename), 'wb') as binfile:
+ binfile.write(data)
diff --git a/app/terraform/eotk/__init__.py b/app/terraform/eotk/__init__.py
index e69de29..474c0b1 100644
--- a/app/terraform/eotk/__init__.py
+++ b/app/terraform/eotk/__init__.py
@@ -0,0 +1,36 @@
+import jinja2
+from flask import current_app
+
+from app.models.base import Group
+
+EOTK_CONFIG_TEMPLATE = """
+set log_separate 1
+
+set nginx_resolver 127.0.0.53 ipv6=off
+
+set nginx_cache_seconds 60
+set nginx_cache_size 64m
+set nginx_tmpfile_size 8m
+
+set x_from_onion_value 1
+set inject_headers_upstream Bypass-Rate-Limit-Token,{{ bypass_token }}
+
+foreignmap facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion facebook.com
+foreignmap twitter3e4tixl4xyajtrzo62zg5vztmjuricljdp2c5kshju4avyoid.onion twitter.com
+
+set project sites
+{% for o in group.onions %}
+hardmap {{ o.onion_name }} {{ o.domain_name }}
+{%- endfor %}
+"""
+
+
+def eotk_configuration(group: Group) -> str:
+ """
+ Generate an EOTK project configuration for an origin group.
+
+ :param group: the origin group
+ :return: the configuration
+ """
+ tmpl = jinja2.Template(EOTK_CONFIG_TEMPLATE)
+ return tmpl.render(bypass_token=current_app.config["BYPASS_TOKEN"], group=group)
diff --git a/app/terraform/eotk/aws.py b/app/terraform/eotk/aws.py
index 34cb5de..1eaaa70 100644
--- a/app/terraform/eotk/aws.py
+++ b/app/terraform/eotk/aws.py
@@ -6,6 +6,8 @@ from app import app
from app.extensions import db
from app.models.base import Group
from app.models.onions import Eotk
+from app.terraform import DeterministicZip
+from app.terraform.eotk import eotk_configuration
from app.terraform.terraform import TerraformAutomation
@@ -54,31 +56,25 @@ class EotkAWSAutomation(TerraformAutomation):
region = "us-east-2"
}
- provider "aws" {
- access_key = "{{ aws_access_key }}"
- secret_key = "{{ aws_secret_key }}"
- region = "eu-central-1"
- alias = "second_region"
- }
-
{% for group in groups %}
module "eotk_{{ group.id }}" {
- providers = {
- aws = aws,
- aws.second_region = aws.second_region
- }
- source = "sr2c/aws/eotk"
- version = "0.0.6"
+ source = "{{ terraform_modules_path }}/terraform-aws-bc-eotk"
namespace = "{{ global_namespace }}"
tenant = "{{ group.group_name }}"
name = "eotk"
label_order = ["namespace", "tenant", "name", "attributes"]
- disable_api_termination = true
+ configuration_bundle = "{{ group.id }}.zip"
+ }
+
+ output "eotk_instances_{{ group.id }}" {
+ value = module.eotk_{{ group.id }}.instances
}
{% endfor %}
"""
def tf_generate(self) -> None:
+ if not self.working_dir:
+ raise RuntimeError("No working directory specified.")
self.tf_write(
self.template,
groups=Group.query.filter(
@@ -97,19 +93,28 @@ class EotkAWSAutomation(TerraformAutomation):
for k in self.template_parameters
}
)
+ for group in Group.query.filter(
+ Group.eotk.is_(True),
+ Group.destroyed.is_(None)
+ ).order_by(Group.id).all():
+ with DeterministicZip(os.path.join(self.working_dir, f"{group.id}.zip")) as dzip:
+ dzip.add_file("sites.conf", eotk_configuration(group).encode('utf-8'))
+ for onion in sorted(group.onions, key=lambda o: o.onion_name): # type: ignore[no-any-return]
+ dzip.add_file(f"{onion.onion_name}.v3pub.key", onion.onion_public_key)
+ dzip.add_file(f"{onion.onion_name}.v3sec.key", onion.onion_private_key)
+ dzip.add_file(f"{onion.onion_name[:20]}-v3.cert", onion.tls_public_key)
+ dzip.add_file(f"{onion.onion_name[:20]}-v3.pem", onion.tls_private_key)
def tf_posthook(self, *, prehook_result: Any = None) -> None:
- state = self.tf_show()
- for g in state["values"]["root_module"]["child_modules"]:
- if g["address"].startswith("module.eotk_"):
- group_id = int(g["address"][len("module.eotk_"):])
- for i in g["child_modules"]:
- if ".module.instance_" in i["address"]:
- instance = int(i["address"][-1])
- region = "us-east-2" if instance == 1 else "eu-central-1"
- for s in i["child_modules"]:
- if s["address"].endswith(".module.instance"):
- for x in s["resources"]:
- if x["address"].endswith(".module.instance.aws_instance.default[0]"):
- update_eotk_instance(group_id, region, x['values']['id'])
+ for e in Eotk.query.all():
+ db.session.delete(e)
+ outputs = self.tf_output()
+ for output in outputs:
+ if output.startswith("eotk_instances_"):
+ try:
+ group_id = int(output[len("eotk_instance_") + 1:])
+ for az in outputs[output]['value']:
+ update_eotk_instance(group_id, az, outputs[output]['value'][az])
+ except ValueError:
+ pass
db.session.commit()
diff --git a/app/terraform/terraform.py b/app/terraform/terraform.py
index 90bac45..b68c8d6 100644
--- a/app/terraform/terraform.py
+++ b/app/terraform/terraform.py
@@ -150,9 +150,9 @@ class TerraformAutomation(BaseAutomation):
"""
def tf_show(self) -> Any:
- # This subprocess call doesn't take any user input.
if not self.working_dir:
raise RuntimeError("No working directory specified.")
+ # This subprocess call doesn't take any user input.
terraform = subprocess.run( # nosec
['terraform', 'show', '-json'],
cwd=self.working_dir,
diff --git a/terraform-modules/terraform-aws-bc-eotk b/terraform-modules/terraform-aws-bc-eotk
new file mode 160000
index 0000000..fbbf804
--- /dev/null
+++ b/terraform-modules/terraform-aws-bc-eotk
@@ -0,0 +1 @@
+Subproject commit fbbf804da7528470558d3e887a6bd44fed5990c4
diff --git a/terraform-modules/terraform-aws-eotk b/terraform-modules/terraform-aws-eotk
deleted file mode 160000
index 4bd8d9b..0000000
--- a/terraform-modules/terraform-aws-eotk
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 4bd8d9b7a72d6aab918f210358c183943ee2d64d