├── .bazelignore ├── .bazelrc ├── .gitignore ├── .style.yapf ├── .yapfignore ├── BUILD ├── LICENSE ├── README.md ├── WORKSPACE ├── challenges └── BUILD ├── config.bzl ├── infra ├── BUILD ├── ctfd │ ├── BUILD.bazel │ ├── challenge.libsonnet │ ├── flaganizer │ │ ├── __init__.py │ │ └── assets │ │ │ ├── flaganizer.js │ │ │ └── submitform.html │ └── run.py ├── ctfproxy │ ├── BUILD.bazel │ ├── README.md │ ├── access.go │ ├── access.jsonnet │ ├── alias.jsonnet │ ├── auth.go │ ├── certs │ │ └── .keep │ ├── config.go │ ├── defaultfiles │ │ ├── BUILD │ │ ├── README.md │ │ └── quocca.ico │ ├── error.go │ ├── http.go │ ├── ip.go │ ├── logging.go │ ├── login.go │ ├── main.go │ ├── network.go │ ├── proxy.go │ ├── ssl.go │ ├── statusz.go │ ├── templates │ │ ├── BUILD.bazel │ │ ├── error.html │ │ └── login.html │ └── websocket.go ├── dns │ ├── BUILD.bazel │ ├── README.md │ └── main.go ├── elk │ ├── BUILD.bazel │ ├── challenge.libsonnet │ ├── elasticsearch-docker │ ├── elasticsearch.yml │ ├── kibana-docker │ └── kibana.yml ├── flaganizer │ ├── BUILD.bazel │ ├── README.md │ ├── challenge.libsonnet │ ├── flags.jsonnet │ └── main.go ├── gaia │ ├── .gitignore │ ├── BUILD.bazel │ ├── README.md │ ├── auth.go │ ├── challenge.libsonnet │ ├── main.go │ └── template.go ├── isodb │ ├── BUILD.bazel │ ├── README.md │ ├── auth.go │ ├── challenge.libsonnet │ ├── main.go │ └── python │ │ ├── BUILD.bazel │ │ └── __init__.py ├── jsonnet │ ├── BUILD │ ├── README.md │ ├── cli-static-sffe.jsonnet │ ├── docker-compose.jsonnet │ ├── k8s.jsonnet │ ├── route53.jsonnet │ ├── services.libsonnet │ └── utils.libsonnet ├── requestz │ ├── BUILD.bazel │ ├── README.md │ ├── challenge.libsonnet │ └── main.go ├── whoami │ ├── BUILD.bazel │ ├── README.md │ ├── challenge.libsonnet │ └── main.go └── xssbot │ ├── BUILD │ ├── README.md │ ├── challenge.libsonnet │ └── server.js ├── jwtkeys ├── BUILD ├── README.md └── generate.go ├── package.json ├── pylintrc ├── requirements.txt ├── third_party ├── BUILD ├── autocertcache │ ├── BUILD.bazel │ └── autocertcache.go ├── chromium.BUILD ├── ctfd │ ├── BUILD │ ├── ctfd.BUILD │ └── manage.py ├── easfs │ ├── BUILD │ ├── BookParser.go │ ├── FooterParser.go │ ├── LICENSE │ ├── MDParser.go │ ├── ProjectParser.go │ ├── README.md │ ├── YAMLParser.go │ ├── error.go │ ├── go.mod │ ├── go.sum │ ├── helper.go │ ├── load.go │ ├── main.go │ ├── page.go │ ├── redirector.go │ ├── static │ │ ├── images │ │ │ ├── credentials-spinner.svg │ │ │ └── redesign-14 │ │ │ │ ├── button-down-black.svg │ │ │ │ ├── button-down-grey.svg │ │ │ │ └── nav-status-experimental.svg │ │ ├── scripts │ │ │ ├── devsite-dev.js │ │ │ ├── framebox.js │ │ │ ├── jquery-bundle.js │ │ │ ├── prettify-bundle.js │ │ │ ├── script_foot.js │ │ │ └── script_foot_closure.js │ │ └── styles │ │ │ ├── devsite-google-blue.css │ │ │ ├── easfs.css │ │ │ └── empty.css │ └── templates │ │ ├── framebox.html │ │ ├── includes │ │ ├── analytics.html │ │ ├── announcement-banner.html │ │ ├── article-license.html │ │ ├── devsite-collapsible-section.html │ │ ├── devsite-footer-banner.html │ │ ├── devsite-footer-linkboxes.html │ │ ├── devsite-footer-promos.html │ │ ├── devsite-nav-responsive.html │ │ ├── devsite-top-logo-row-wrapper-wrapper.html │ │ ├── dogfood-banner.html │ │ ├── landing-page-content-row.html │ │ ├── landing-page-content.html │ │ ├── lower-tabs.html │ │ ├── page-head.html │ │ ├── page-nav-responsive-sidebar.html │ │ ├── page-nav.html │ │ ├── page-nav.html.bak │ │ ├── ratings-container.html │ │ ├── searchbar.html │ │ ├── section-nav.html │ │ ├── survey.html │ │ └── upper-tabs.html │ │ ├── page-article.html │ │ ├── page-base.html │ │ └── page-landing.html ├── eddsa │ ├── BUILD.bazel │ └── ed25519.go └── python │ └── html │ ├── BUILD.bazel │ └── __init__.py ├── tools ├── BUILD ├── PRESUBMIT.sh ├── challenge.bzl ├── challenges_list.bzl ├── ctflark │ ├── BUILD.bazel │ └── main.go ├── format.sh ├── format_jsonnet.sh ├── format_jsonnet_check.sh ├── gcr_delete_all.sh ├── gcr_delete_untagged.sh ├── generatechallengeslist.py ├── lint_jsonnet.sh ├── nogoconfig.json ├── rsync.sh ├── sffe.bzl ├── tarscript.bzl ├── unsed_go_repos.sh └── workspace-status.sh └── yarn.lock /.bazelignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.bazelrc: -------------------------------------------------------------------------------- 1 | build --stamp --workspace_status_command=./tools/workspace-status.sh 2 | run --stamp --workspace_status_command=./tools/workspace-status.sh 3 | 4 | # Don't create bazel-* symlinks in the WORKSPACE directory, except `bazel-out`, 5 | # which is mandatory. 6 | # These require .gitignore and may scare users. 7 | # 8 | # Instead, the output will appear in `dist/bin`. You'll need to ignore the 9 | # `bazel-out` directory that is created in the workspace root. 10 | build --symlink_prefix=dist/ 11 | 12 | # Turn off legacy external runfiles 13 | # This prevents accidentally depending on this feature, which Bazel will remove. 14 | build --nolegacy_external_runfiles 15 | 16 | # Disable sandbox on Mac OS for performance reason. 17 | build --spawn_strategy=local 18 | run --spawn_strategy=local 19 | test --spawn_strategy=local 20 | 21 | # The following flags are set to test use of new features for python toolchains 22 | build --incompatible_use_python_toolchains 23 | build --host_force_python=PY2 24 | test --incompatible_use_python_toolchains 25 | test --host_force_python=PY2 26 | run --incompatible_use_python_toolchains 27 | run --host_force_python=PY2 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | # dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Custom .gitignore additons 132 | .vscode 133 | Pipfile 134 | Pipfile.lock 135 | 136 | # annoying mac 137 | .DS_Store 138 | 139 | ### BEGIN: Copied from Geegle3 ### 140 | *.swp 141 | *.pyc 142 | .gdb_history 143 | core 144 | *.o 145 | bazel-* 146 | node_modules 147 | /dist 148 | yarn-error.log 149 | 150 | # NOTES(adamyi): those keep appearing in my environment and are annoying 151 | # i think it won't cause any problems 152 | /go.mod 153 | /go.sum 154 | ### END: Copied from Geegle3 ### 155 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = yapf 3 | -------------------------------------------------------------------------------- /.yapfignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /BUILD: -------------------------------------------------------------------------------- 1 | load("@bazel_gazelle//:def.bzl", "gazelle") 2 | load("@com_github_bazelbuild_buildtools//buildifier:def.bzl", "buildifier") 3 | load("@io_bazel_rules_docker//container:container.bzl", "container_bundle") 4 | load("@io_bazel_rules_docker//contrib:push-all.bzl", "container_push") 5 | load("@io_bazel_rules_go//go:def.bzl", "TOOLS_NOGO", "nogo") 6 | load("@rules_python//python:defs.bzl", "py_library") 7 | load("//:config.bzl", "CTF_DOMAIN") 8 | 9 | # exports_files(["tsconfig.json"]) 10 | 11 | # gazelle:exclude dist 12 | # gazelle:exclude node_modules 13 | # gazelle:prefix github.com/adamyi/CTFProxy 14 | gazelle(name = "gazelle") 15 | 16 | buildifier( 17 | name = "buildifier", 18 | exclude_patterns = [ 19 | "./dist/*", 20 | "./node_modules/*", 21 | ], 22 | lint_mode = "fix", 23 | lint_warnings = ["all"], 24 | ) 25 | 26 | buildifier( 27 | name = "buildifier_check", 28 | exclude_patterns = [ 29 | "./dist/*", 30 | "./node_modules/*", 31 | ], 32 | lint_mode = "warn", 33 | lint_warnings = ["all"], 34 | mode = "check", 35 | ) 36 | 37 | nogo( 38 | name = "nogo", 39 | config = "//tools:nogoconfig.json", 40 | visibility = ["//visibility:public"], 41 | deps = TOOLS_NOGO, 42 | ) 43 | 44 | container_bundle( 45 | name = "all_containers", 46 | images = { 47 | "gcr.io/ctfproxy/elk/elasticsearch:latest": "//infra/elk:elasticsearch", #ctflark: keep 48 | "gcr.io/ctfproxy/elk/kibana:latest": "//infra/elk:kibana", #ctflark:keep 49 | "gcr.io/ctfproxy/infra/ctfd:latest": "//infra/ctfd:image", 50 | "gcr.io/ctfproxy/infra/ctfproxy:latest": "//infra/ctfproxy:image", 51 | "gcr.io/ctfproxy/infra/dns:latest": "//infra/dns:image", 52 | "gcr.io/ctfproxy/infra/flaganizer:latest": "//infra/flaganizer:image", 53 | "gcr.io/ctfproxy/infra/gaia:latest": "//infra/gaia:image", 54 | "gcr.io/ctfproxy/infra/isodb:latest": "//infra/isodb:image", 55 | "gcr.io/ctfproxy/infra/requestz:latest": "//infra/requestz:image", 56 | "gcr.io/ctfproxy/infra/whoami:latest": "//infra/whoami:image", 57 | "gcr.io/ctfproxy/infra/xssbot:latest": "//infra/xssbot:image", 58 | }, 59 | ) 60 | 61 | container_push( 62 | name = "all_containers_push", 63 | bundle = ":all_containers", 64 | format = "Docker", 65 | ) 66 | 67 | genrule( 68 | name = "ctf_domain_py", 69 | srcs = [], 70 | outs = ["ctf_domain.py"], 71 | cmd = "echo CTF_DOMAIN=\\\"" + CTF_DOMAIN + "\\\"> \"$@\"", 72 | ) 73 | 74 | py_library( 75 | name = "python_ctf_domain", 76 | srcs = [":ctf_domain.py"], 77 | visibility = ["//visibility:public"], 78 | ) 79 | -------------------------------------------------------------------------------- /challenges/BUILD: -------------------------------------------------------------------------------- 1 | load("//tools:challenges_list.bzl", "challenges_list") 2 | 3 | challenges_list( 4 | name = "challenges_jsonnet", 5 | visibility = ["//visibility:public"], 6 | deps = [], 7 | ) 8 | -------------------------------------------------------------------------------- /config.bzl: -------------------------------------------------------------------------------- 1 | """ 2 | THIS IS THE CONFIGURATION FILE FOR CTFPROXY INFRASTRUCTURE 3 | 4 | Example: 5 | ``` 6 | CTF_DOMAIN = "myctf.com" 7 | CONTAINER_REGISTRY = "gcr.io/ctfproxy" 8 | ``` 9 | """ 10 | 11 | # FIXME: configure me 12 | -------------------------------------------------------------------------------- /infra/BUILD: -------------------------------------------------------------------------------- 1 | load("//tools:challenges_list.bzl", "challenges_list") 2 | 3 | challenges_list( 4 | name = "infra_jsonnet", 5 | visibility = ["//visibility:public"], 6 | deps = [ 7 | "//infra/ctfd:challenge", 8 | "//infra/elk:challenge", 9 | "//infra/flaganizer:challenge", 10 | "//infra/gaia:challenge", 11 | "//infra/isodb:challenge", 12 | "//infra/requestz:challenge", 13 | "//infra/whoami:challenge", 14 | "//infra/xssbot:challenge", 15 | ], 16 | ) 17 | -------------------------------------------------------------------------------- /infra/ctfd/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@ctfd_pip//:requirements.bzl", "requirement") 2 | load("@io_bazel_rules_docker//container:container.bzl", "container_image") 3 | load("@io_bazel_rules_docker//python:image.bzl", "py_image") 4 | load("@rules_python//python:defs.bzl", "py_library") 5 | load("//:config.bzl", "CTF_DOMAIN") 6 | load("//tools:challenge.bzl", "ctf_challenge") 7 | 8 | ctf_challenge() 9 | 10 | py_image( 11 | name = "base", 12 | srcs = ["run.py"], 13 | base = "@python2-base//image", 14 | main = "run.py", 15 | deps = [ 16 | ":flaganizer_lib", 17 | "@ctfd//:app_lib", 18 | requirement("gunicorn"), 19 | requirement("setuptools"), 20 | ], 21 | ) 22 | 23 | py_library( 24 | name = "flaganizer_lib", 25 | srcs = glob([ 26 | "flaganizer/**/*.py", 27 | ]), 28 | data = glob( 29 | [ 30 | "flaganizer/**", 31 | ], 32 | exclude = [ 33 | "flaganizer/**/*.py", 34 | ], 35 | ), 36 | ) 37 | 38 | container_image( 39 | name = "image", 40 | base = ":base", 41 | env = { 42 | "CACHE_DIR": "/data/cache", 43 | "CTF_DOMAIN": CTF_DOMAIN, 44 | # FIXME: configure DATABASE_URL and SECRET_KEY here 45 | "LOG_FOLDER": "/data/log", 46 | "PYTHONPATH": "/app/infra/ctfd/base.binary.runfiles/ctfd_pip_pypi__SQLAlchemy_1_3_11/SQLAlchemy-1.3.11.data/purelib", # hack 47 | "UPLOAD_FOLDER": "/data/upload", 48 | }, 49 | files = [ 50 | "//jwtkeys:jwt.pub", 51 | "//third_party/ctfd:manage.py", 52 | ], 53 | symlinks = { 54 | "/app/infra/ctfd/base.binary.runfiles/ctfproxy/migrations": "/app/infra/ctfd/base.binary.runfiles/ctfd/migrations", 55 | "/app/infra/ctfd/base.binary.runfiles/ctfd/CTFd/plugins/flaganizer": "/app/infra/ctfd/base.binary.runfiles/ctfproxy/infra/ctfd/flaganizer", 56 | }, 57 | visibility = ["//visibility:public"], 58 | ) 59 | -------------------------------------------------------------------------------- /infra/ctfd/challenge.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | services: [ 3 | { 4 | name: 'ctfd', 5 | replicas: 1, 6 | category: 'infra', 7 | clustertype: 'master', 8 | persistent: '100M', 9 | access: ||| 10 | def checkAccess(): 11 | # NOTES: admin apis don't have specific separate endpoint so we only protect admin frontend here 12 | # we'll rely on CTFd's own permission settings for fine-grained api access control 13 | if path.startswith("/admin") and ("ctfd-admin@groups." + corpDomain) not in groups: 14 | denyAccess() 15 | grantAccess() 16 | checkAccess() 17 | |||, 18 | }, 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /infra/ctfd/flaganizer/assets/flaganizer.js: -------------------------------------------------------------------------------- 1 | if ($('#challenges-board').length) { 2 | $('
').insertBefore('#challenges-board'); 3 | $.get('plugins/flaganizer/assets/submitform.html', function (data) { 4 | $('#flaganizer').html(data); 5 | 6 | var flaganizer_submit = $('#flaganizer-submit'); 7 | var flaganizer_flag = $('#flaganizer-flag'); 8 | flaganizer_submit.unbind('click'); 9 | flaganizer_submit.click(function (e) { 10 | e.preventDefault(); 11 | 12 | $.post("/flaganizer-submit" , { 13 | submission: flaganizer_flag.val(), 14 | nonce: $('#nonce').val() 15 | }, function (data) { 16 | console.log(data); 17 | var result = $.parseJSON(JSON.stringify(data)); 18 | 19 | var result_message = $('#flaganizer-message'); 20 | var result_notification = $('#flaganizer-notification'); 21 | result_notification.removeClass(); 22 | result_message.text(result.data.message); 23 | 24 | if (result.data.status == "authentication_required") { 25 | window.location = script_root + "/login?next=" + script_root + window.location.pathname + window.location.hash 26 | return 27 | } 28 | else if (result.data.status == "incorrect") { 29 | result_notification.addClass('alert alert-danger alert-dismissable text-center'); 30 | result_notification.slideDown(); 31 | 32 | flaganizer_flag.removeClass("correct"); 33 | flaganizer_flag.addClass("wrong"); 34 | setTimeout(function () { 35 | flaganizer_flag.removeClass("wrong"); 36 | }, 3000); 37 | } 38 | else if (result.data.status == "correct") { 39 | result_notification.addClass('alert alert-success alert-dismissable text-center'); 40 | result_notification.slideDown(); 41 | 42 | flaganizer_flag.val(""); 43 | flaganizer_flag.removeClass("wrong"); 44 | flaganizer_flag.addClass("correct"); 45 | } 46 | else if (result.data.status == "already_solved") { 47 | result_notification.addClass('alert alert-info alert-dismissable text-center'); 48 | result_notification.slideDown(); 49 | 50 | flaganizer_flag.addClass("correct"); 51 | } 52 | 53 | setTimeout(function () { 54 | $('.alert').slideUp(); 55 | flaganizer_submit.removeClass("disabled-button"); 56 | flaganizer_submit.prop('disabled', false); 57 | }, 4500); 58 | }); 59 | }); 60 | 61 | flaganizer_flag.keyup(function(event){ 62 | if(event.keyCode == 13){ 63 | flaganizer_submit.click(); 64 | } 65 | }); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /infra/ctfd/flaganizer/assets/submitform.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 9 |
10 |
11 |
12 |
13 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /infra/ctfd/run.py: -------------------------------------------------------------------------------- 1 | # hacky script to start gunicorn 2 | 3 | import re 4 | import sys 5 | import os 6 | import logging 7 | 8 | from gunicorn.app.wsgiapp import run 9 | 10 | logging.basicConfig() 11 | 12 | os.system("python -m compileall /app/infra/ctfd/image.binary.runfiles/") 13 | os.system("python /manage.py db upgrade") 14 | 15 | sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) 16 | 17 | sys.argv.append("CTFd:create_app()") 18 | sys.argv.append("--workers=1") 19 | sys.argv.append("--worker-class=gevent") 20 | sys.argv.append("--bind") 21 | sys.argv.append("0.0.0.0:80") 22 | 23 | os.chdir("../ctfd/CTFd/") 24 | 25 | sys.exit(run()) 26 | -------------------------------------------------------------------------------- /infra/ctfproxy/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar") 2 | load("@io_bazel_rules_docker//container:container.bzl", "container_image") 3 | load("@io_bazel_rules_docker//go:image.bzl", "go_image") 4 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 5 | load("@io_bazel_rules_jsonnet//jsonnet:jsonnet.bzl", "jsonnet_to_json") 6 | load("//:config.bzl", "CTF_DOMAIN") 7 | 8 | container_image( 9 | name = "image", 10 | base = ":image_base", 11 | files = [ 12 | "//jwtkeys:jwt.key", 13 | "//jwtkeys:jwt.pub", 14 | ], 15 | ports = [ 16 | "80", 17 | "443", 18 | ], 19 | tars = [ 20 | ":certs", 21 | ], 22 | visibility = ["//visibility:public"], 23 | ) 24 | 25 | pkg_tar( 26 | name = "certs", 27 | srcs = glob([ 28 | "certs/*.pem", 29 | "certs/*.key", 30 | ]), 31 | mode = "0755", 32 | strip_prefix = ".", 33 | ) 34 | 35 | go_image( 36 | name = "image_base", 37 | args = [ 38 | "-jwt_private_key", 39 | "/jwt.key", 40 | "-jwt_public_key", 41 | "/jwt.pub", 42 | # FIXME: configure ssl_cert, ssl_key, mtls_ca here. See config.go for more optional configuration 43 | "-corp_domain", 44 | CTF_DOMAIN, 45 | "-login_subdomain", 46 | "login", 47 | "-clirelay_subdomain", 48 | "cli-relay", 49 | "-root_service_domain", 50 | "www", 51 | "-access_config_path", 52 | "infra/ctfproxy/access.json", 53 | "-alias_config_path", 54 | "infra/ctfproxy/alias.json", 55 | # FIXME: configure cert_gcs_bucket gcp_service_account gcp_project here 56 | ], 57 | data = [ 58 | ":access", 59 | ":alias", 60 | ], 61 | embed = [":go_default_library"], 62 | ) 63 | 64 | go_library( 65 | name = "go_default_library", 66 | srcs = [ 67 | "access.go", 68 | "auth.go", 69 | "config.go", 70 | "error.go", 71 | "http.go", 72 | "ip.go", 73 | "logging.go", 74 | "login.go", 75 | "main.go", 76 | "network.go", 77 | "proxy.go", 78 | "ssl.go", 79 | "statusz.go", 80 | "websocket.go", 81 | ], 82 | importpath = "github.com/adamyi/CTFProxy/infra/ctfproxy", 83 | visibility = ["//visibility:private"], 84 | x_defs = { 85 | "github.com/adamyi/CTFProxy/infra/ctfproxy.BuildTimestamp": "{BUILD_TIMESTAMP}", 86 | "github.com/adamyi/CTFProxy/infra/ctfproxy.Builder": "{STABLE_BUILDER}", 87 | "github.com/adamyi/CTFProxy/infra/ctfproxy.GitCommit": "{STABLE_GIT_COMMIT}", 88 | "github.com/adamyi/CTFProxy/infra/ctfproxy.GitVersion": "{STABLE_GIT_VERSION}", 89 | }, 90 | deps = [ 91 | "//infra/ctfproxy/defaultfiles:go_default_library", 92 | "//infra/ctfproxy/templates:go_default_library", 93 | "//third_party/autocertcache:go_default_library", 94 | "//third_party/eddsa:go_default_library", 95 | "@com_github_adamyi_hotconfig//:go_default_library", 96 | "@com_github_dgrijalva_jwt_go//:go_default_library", 97 | "@com_github_go_sql_driver_mysql//:go_default_library", 98 | "@com_github_google_uuid//:go_default_library", 99 | "@com_github_gorilla_websocket//:go_default_library", 100 | "@com_github_ulule_limiter_v3//:go_default_library", 101 | "@com_github_ulule_limiter_v3//drivers/store/memory:go_default_library", 102 | "@com_google_cloud_go_storage//:go_default_library", 103 | "@net_starlark_go//starlark:go_default_library", 104 | "@org_golang_google_api//option:go_default_library", 105 | "@org_golang_x_crypto//acme/autocert:go_default_library", 106 | "@org_golang_x_oauth2//google:go_default_library", 107 | ], 108 | ) 109 | 110 | go_binary( 111 | name = "ctfproxy", 112 | embed = [":go_default_library"], 113 | visibility = ["//visibility:public"], 114 | ) 115 | 116 | jsonnet_to_json( 117 | name = "access", 118 | src = "access.jsonnet", 119 | outs = ["access.json"], 120 | deps = [ 121 | "//infra/jsonnet:services", 122 | "//infra/jsonnet:utils", 123 | ], 124 | ) 125 | 126 | jsonnet_to_json( 127 | name = "alias", 128 | src = "alias.jsonnet", 129 | outs = ["alias.json"], 130 | deps = [ 131 | "//infra/jsonnet:services", 132 | "//infra/jsonnet:utils", 133 | ], 134 | ) 135 | -------------------------------------------------------------------------------- /infra/ctfproxy/README.md: -------------------------------------------------------------------------------- 1 | # ctfproxy 2 | Edge Endpoint Proxy and Inter-Service Communication Agent 3 | 4 | Centralized authentication + logging 5 | 6 | ## LICENSE 7 | 8 | Modified from https://github.com/adamyi/Geegle3, original license: 9 | 10 | Copyright (c) 2019 [Adam Yi](mailto:i@adamyi.com), [Adam Tanana](mailto:adam@tanana.io), [Lachlan Jones](mailto:contact@lachjones.com) 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | -------------------------------------------------------------------------------- /infra/ctfproxy/access.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "go.starlark.net/starlark" 10 | ) 11 | 12 | var ( 13 | aclAstCache map[string]*starlark.Program 14 | aclAstCacheTime int64 15 | predefinedSet map[string]bool 16 | ) 17 | 18 | func aclAstExpireCache(lutime int64) { 19 | if aclAstCacheTime != lutime { 20 | aclAstCacheTime = lutime 21 | aclAstCache = make(map[string]*starlark.Program) 22 | } 23 | } 24 | 25 | func init() { 26 | predefinedSet = make(map[string]bool) 27 | pdv := []string{"host", "method", "path", "rawpath", "query", "ip", "user", "corpDomain", "groups", "timestamp", "grantAccess", "openAccess", "denyAccess"} 28 | for _, v := range pdv { 29 | predefinedSet[v] = true 30 | } 31 | } 32 | 33 | func isPredeclared(token string) bool { 34 | return predefinedSet[token] 35 | } 36 | 37 | func hasAccess(servicename, username string, groups []string, req *http.Request) *CPError { 38 | var err error 39 | for _, g := range groups { 40 | if g == "break-glass-access@groups."+_configuration.CorpDomain { 41 | return nil 42 | } 43 | } 44 | aclAstExpireCache(_configuration.AccessPolicies.LastUpdated().Unix()) 45 | prog := aclAstCache[servicename] 46 | 47 | // lazy compile AST 48 | if prog == nil { 49 | code := _configuration.AccessPolicies.ConfigOrNil().(map[string]string)[servicename] 50 | if code == "" { 51 | return NewCPError(http.StatusBadRequest, "Could not resolve the IP address for host "+req.Host, "Your client has issued a malformed or illegal request.", "", "_configuration.AccessPolicies["+servicename+"] not found") 52 | } 53 | log.Printf("lazy compiling %s_access.star", servicename) 54 | _, prog, err = starlark.SourceProgram(servicename+"_access.star", code, isPredeclared) 55 | if err != nil { 56 | return NewCPError(http.StatusInternalServerError, "Error happened while determining access rights", "contact staff if you believe this shouldn't happen", "", err.Error()) 57 | } 58 | aclAstCache[servicename] = prog 59 | } 60 | 61 | thread := &starlark.Thread{ 62 | Name: "access", 63 | Print: func(_ *starlark.Thread, msg string) { 64 | log.Printf("access starlark print: " + msg) 65 | }, 66 | } 67 | ret := make(chan *CPError) 68 | done := make(chan bool) 69 | defer close(done) 70 | openAccess := func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 71 | select { 72 | case ret <- nil: 73 | case <-done: 74 | } 75 | return nil, errors.New("ctfproxy: returned") 76 | } 77 | denyAccess := func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 78 | var code int = http.StatusForbidden 79 | var title string = "403 Forbidden" 80 | var description string = "Contact staff if you believe you should have access" 81 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "code?", &code, "title?", &title, "description?", &description); err != nil { 82 | return nil, err 83 | } 84 | select { 85 | case ret <- NewCPError(code, title, description, "", "denyAccess() called in "+servicename+"_access.star call stack:\n"+thread.CallStack().String()): 86 | case <-done: 87 | } 88 | return nil, errors.New("ctfproxy: returned") 89 | } 90 | grantAccess := func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 91 | if username == "anonymous@anonymous."+_configuration.CorpDomain { 92 | return denyAccess(thread, b, args, kwargs) 93 | } 94 | return openAccess(thread, b, args, kwargs) 95 | } 96 | sgroups := starlark.NewList(nil) 97 | for _, group := range groups { 98 | sgroups.Append(starlark.String(group)) 99 | } 100 | predeclared := starlark.StringDict{ 101 | "host": starlark.String(req.Host), 102 | "method": starlark.String(req.Method), 103 | "path": starlark.String(req.URL.Path), 104 | "rawpath": starlark.String(req.URL.EscapedPath()), 105 | "query": starlark.String(req.URL.RawQuery), 106 | "ip": starlark.String(req.RemoteAddr), 107 | "user": starlark.String(username), 108 | "corpDomain": starlark.String(_configuration.CorpDomain), 109 | "groups": sgroups, 110 | "timestamp": starlark.MakeInt64(time.Now().Unix()), 111 | "grantAccess": starlark.NewBuiltin("grantAccess", grantAccess), 112 | "openAccess": starlark.NewBuiltin("openAccess", openAccess), 113 | "denyAccess": starlark.NewBuiltin("denyAccess", denyAccess), 114 | } 115 | go func() { 116 | g, err := prog.Init(thread, predeclared) 117 | g.Freeze() 118 | e := NewCPError(http.StatusForbidden, "403 Forbidden", "Contact staff if you believe you should have access", "", servicename+"_access.star returned without granting access, default denial") 119 | if err != nil { 120 | if err.Error() == "ctfproxy: returned" { 121 | return 122 | } 123 | estr := err.Error() 124 | if evalerr, ok := err.(*starlark.EvalError); ok { 125 | estr = evalerr.Backtrace() 126 | } 127 | e = NewCPError(http.StatusInternalServerError, "Error happened while determining access rights", "contact staff if you believe this shouldn't happen", "", estr) 128 | } 129 | select { 130 | case ret <- e: 131 | case <-done: 132 | } 133 | }() 134 | 135 | select { 136 | case e := <-ret: 137 | return e 138 | case <-time.After(1 * time.Second): 139 | return NewCPError(http.StatusInternalServerError, "Error happened while determining access rights", "contact staff if you believe this shouldn't happen", "", servicename+"_access.star timed out during evaluation") 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /infra/ctfproxy/access.jsonnet: -------------------------------------------------------------------------------- 1 | local services = import 'infra/jsonnet/services.libsonnet'; 2 | local utils = import 'infra/jsonnet/utils.libsonnet'; 3 | 4 | local defaultaccess = 'denyAccess()'; 5 | 6 | { 7 | 'kubernetes-dashboard': ||| 8 | def checkAccess(): 9 | if ("k8s-admin@groups." + corpDomain) in groups: 10 | grantAccess() 11 | checkAccess() 12 | |||, 13 | } 14 | { 15 | [service.name]: if 'access' in service then service.access else defaultaccess 16 | for service in services 17 | } 18 | -------------------------------------------------------------------------------- /infra/ctfproxy/alias.jsonnet: -------------------------------------------------------------------------------- 1 | local services = import 'infra/jsonnet/services.libsonnet'; 2 | local utils = import 'infra/jsonnet/utils.libsonnet'; 3 | 4 | local aliases = std.flattenArrays([ 5 | if 'alias' in service then [ 6 | { 7 | alias: alias, 8 | origin: service.name, 9 | } 10 | for alias in service.alias 11 | ] else [] 12 | for service in services 13 | ]); 14 | 15 | { 16 | [alias.alias]: alias.origin 17 | for alias in aliases 18 | } 19 | -------------------------------------------------------------------------------- /infra/ctfproxy/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/x509" 5 | "crypto/x509/pkix" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "regexp" 12 | "strings" 13 | 14 | "github.com/dgrijalva/jwt-go" 15 | "github.com/adamyi/CTFProxy/third_party/eddsa" 16 | ) 17 | 18 | type Claims struct { 19 | Username string `json:"username"` 20 | Displayname string `json:"displayname"` 21 | Service string `json:"service"` 22 | Groups []string `json:"groups"` 23 | jwt.StandardClaims 24 | } 25 | 26 | var SubAccValid = regexp.MustCompile(`^[a-zA-Z.0-9\-_]+$`).MatchString 27 | 28 | // username++impersonation+subacc@domain 29 | // then displayname 30 | func getUsername(req *http.Request) (string, string, error) { 31 | username, displayname := getMainUsername(req) 32 | // disable impersonation/subacc for players. Only allow services to do this for now 33 | if strings.Split(username, "@")[1] == _configuration.CorpDomain { 34 | return username, displayname, nil 35 | } 36 | var impersonateToken string 37 | if impersonateToken = req.Header.Get(_configuration.ImpersonateTokenHeader); impersonateToken != "" { 38 | impUsername, _ := getUsernameFromJWT(impersonateToken, username) 39 | if impUsername != "anonymous@anonymous."+_configuration.CorpDomain { 40 | s := strings.Split(username, "@") 41 | username = s[0] + "++" + strings.Split(strings.Split(impUsername, "@")[0], "+")[0] + "@" + s[1] 42 | } 43 | } 44 | subacc := req.Header.Get(_configuration.SubAccHeader) 45 | if subacc != "" { 46 | if !(SubAccValid(subacc) && 47 | (_configuration.SubAccLengthLimit == -1 || len(subacc) < _configuration.SubAccLengthLimit)) { 48 | return "", "", errors.New("invalid subacc") 49 | } 50 | s := strings.Split(username, "@") 51 | username = s[0] + "+" + subacc + "@" + s[1] 52 | } 53 | return username, displayname, nil 54 | } 55 | 56 | func getGroups(username string) (ret []string) { 57 | r, err := http.Get(_configuration.GAIAEndpoint + "/api/getgroups?ldap=" + username) 58 | if err != nil { 59 | fmt.Println(err) 60 | return 61 | } 62 | defer r.Body.Close() 63 | json.NewDecoder(r.Body).Decode(&ret) 64 | return 65 | } 66 | 67 | func getEmailFromRDN(name *pkix.Name) (email string, err error) { 68 | // most likely the last one so we loop in reverse 69 | for i := len(name.Names) - 1; i >= 0; i -= 1 { 70 | t := name.Names[i].Type 71 | if t[0] == 1 && t[1] == 2 && t[2] == 840 && t[3] == 113549 && t[4] == 1 && t[5] == 9 && t[6] == 1 { 72 | var ok bool 73 | email, ok = name.Names[i].Value.(string) 74 | if !ok { 75 | return "", errors.New("email not string") 76 | } 77 | return 78 | } 79 | } 80 | return "", errors.New("can't find email in cert") 81 | } 82 | 83 | // return handle, real name, and error 84 | func verifyCert(cert *x509.Certificate) (string, string, error) { 85 | // log.Println("verifyCert") 86 | opts := x509.VerifyOptions{ 87 | Roots: authCA, 88 | } 89 | _, err := cert.Verify(opts) 90 | if err != nil { 91 | log.Println(err) 92 | return "", "", err 93 | } 94 | email, err := getEmailFromRDN(&cert.Subject) 95 | if err != nil { 96 | return "", "", err 97 | } 98 | return strings.Split(email, "@")[0], cert.Subject.CommonName, nil 99 | } 100 | 101 | func getMainUsername(req *http.Request) (string, string) { 102 | certs := req.TLS.PeerCertificates 103 | // log.Println(certs) 104 | if len(certs) > 0 { 105 | name, displayname, err := verifyCert(certs[0]) 106 | if err == nil { 107 | return name + "@" + _configuration.CorpDomain, displayname 108 | } 109 | } 110 | c, err := req.Cookie(_configuration.AuthCookieKey) 111 | var tknStr string 112 | if err != nil { 113 | if tknStr = req.Header.Get(_configuration.InternalJWTHeader); tknStr == "" { 114 | sn, err := getServiceNameFromIP(strings.Split(req.RemoteAddr, ":")[0]) 115 | if err != nil { 116 | return "anonymous@anonymous." + _configuration.CorpDomain, "anonymous" 117 | } 118 | return sn + "@services." + _configuration.CorpDomain, sn 119 | } 120 | } else { 121 | tknStr = c.Value 122 | } 123 | return getUsernameFromJWT(tknStr, _configuration.ServiceName+"@services."+_configuration.CorpDomain) 124 | } 125 | 126 | func getUsernameFromJWT(tknStr, service string) (string, string) { 127 | claims := &Claims{} 128 | 129 | p := jwt.Parser{ValidMethods: []string{eddsa.SigningMethodEdDSA.Alg()}} 130 | tkn, err := p.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) { 131 | return _configuration.VerifyKey, nil 132 | }) 133 | 134 | if err != nil { 135 | if err == jwt.ErrSignatureInvalid { 136 | log.Println("Signature Invalid") 137 | return "anonymous@anonymous." + _configuration.CorpDomain, "anonymous" 138 | } 139 | log.Println("JWT Error") 140 | log.Println(err.Error()) 141 | return "anonymous@anonymous." + _configuration.CorpDomain, "anonymous" 142 | } 143 | 144 | if !tkn.Valid { 145 | log.Println("JWT Invalid") 146 | return "anonymous@anonymous." + _configuration.CorpDomain, "anonymous" 147 | } 148 | 149 | if claims.Service != service { 150 | log.Printf("JWT not correct service - %v vs %v", claims.Service, service) 151 | return "anonymous@anonymous." + _configuration.CorpDomain, "anonymous" 152 | } 153 | 154 | return claims.Username, claims.Displayname 155 | } 156 | -------------------------------------------------------------------------------- /infra/ctfproxy/certs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamyi/CTFProxy/9a2979d925a339d1018e69b883512a18d7d69afb/infra/ctfproxy/certs/.keep -------------------------------------------------------------------------------- /infra/ctfproxy/defaultfiles/BUILD: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//extras:embed_data.bzl", "go_embed_data") 2 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 3 | 4 | go_embed_data( 5 | name = "defaultfiles_data", 6 | srcs = glob( 7 | ["*"], 8 | exclude = [ 9 | "BUILD", 10 | "README.md", 11 | ], 12 | ), 13 | flatten = True, 14 | package = "defaultfiles", 15 | visibility = ["//visibility:public"], 16 | ) 17 | 18 | # keep 19 | go_library( 20 | name = "go_default_library", 21 | srcs = [":defaultfiles_data"], 22 | importpath = "github.com/adamyi/CTFProxy/infra/ctfproxy/defaultfiles", 23 | visibility = ["//visibility:public"], 24 | ) 25 | -------------------------------------------------------------------------------- /infra/ctfproxy/defaultfiles/README.md: -------------------------------------------------------------------------------- 1 | # defaultfiles 2 | 3 | This is the folder you put in general files that will be served if upstream service returns a 404 4 | 5 | This is a nice place for a global fallback robots.txt or favicon.ico if the upstream service does not provide their own. 6 | -------------------------------------------------------------------------------- /infra/ctfproxy/defaultfiles/quocca.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamyi/CTFProxy/9a2979d925a339d1018e69b883512a18d7d69afb/infra/ctfproxy/defaultfiles/quocca.ico -------------------------------------------------------------------------------- /infra/ctfproxy/error.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "html" 7 | "html/template" 8 | "log" 9 | "net/http" 10 | "runtime/debug" 11 | "strings" 12 | 13 | "github.com/adamyi/CTFProxy/infra/ctfproxy/defaultfiles" 14 | "github.com/adamyi/CTFProxy/infra/ctfproxy/templates" 15 | ) 16 | 17 | type CPError struct { 18 | Title string 19 | Description string 20 | PublicDebug string 21 | InternalDebug string 22 | Type string 23 | Debug template.HTML 24 | Code int 25 | } 26 | 27 | var errorTemplate *template.Template 28 | 29 | func init() { 30 | errorTemplate = template.Must(template.New("error").Parse(templates.Data["error.html"])) 31 | } 32 | 33 | func (e *CPError) SetType(t string) { 34 | e.Type = t 35 | } 36 | 37 | func NewCPError(code int, title, description, publicDebug, internalDebug string) *CPError { 38 | ret := &CPError{Code: code, Title: title, Description: description, PublicDebug: publicDebug, InternalDebug: internalDebug} 39 | ret.InternalDebug += "\n\n===CTFProxy Stack Trace===\n" + string(debug.Stack()) 40 | ret.Type = "cp" 41 | return ret 42 | } 43 | 44 | func (e CPError) Write(w http.ResponseWriter, r *http.Request) { 45 | if e.Code >= 300 && e.Code < 400 { 46 | w.Header().Add("Location", e.Description) 47 | } else if e.Code == 404 && len(defaultfiles.Data[r.URL.Path[1:]]) > 0 { 48 | w.Write(defaultfiles.Data[r.URL.Path[1:]]) 49 | return 50 | } 51 | w.WriteHeader(e.Code) 52 | dbgstr := e.PublicDebug 53 | if w.Header().Get("X-CTFProxy-I-Debug") == "1" { 54 | dbgstr += "\n===Internal Debug Info because you're in ctfproxy-debug@===\n" + e.InternalDebug 55 | } 56 | e.Debug = template.HTML(strings.Replace(html.EscapeString(dbgstr), "\n", "
\n", -1)) 57 | errorTemplate.Execute(w, e) 58 | } 59 | 60 | func WrapHandlerWithRecovery(wrappedHandler http.Handler) http.Handler { 61 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 62 | defer func() { 63 | err := recover() 64 | if err != nil { 65 | log.Println("[PANIC RECOVERY TRIGGERED] something terrible happened.") 66 | log.Println(err) 67 | NewCPError(http.StatusInternalServerError, "Server Internal Error", "Something went terribly wrong...", "", fmt.Sprintf("===panic recovery===\n%v", err)).Write(w, r) 68 | } 69 | }() 70 | wrappedHandler.ServeHTTP(w, r) 71 | }) 72 | } 73 | 74 | func handleUpstreamCPError(rw http.ResponseWriter, resp *http.Response, req *http.Request) { 75 | defer resp.Body.Close() 76 | var e CPError 77 | err := json.NewDecoder(resp.Body).Decode(&e) 78 | if err != nil { 79 | e = *NewCPError(http.StatusInternalServerError, "Server Internal Error", "Something went wrong with the service, please try again later", "", "upstream service returned ctfproxy/uperror but failed to decode CPError json "+err.Error()) 80 | } 81 | e.SetType("s") 82 | e.Write(rw, req) 83 | } 84 | -------------------------------------------------------------------------------- /infra/ctfproxy/http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "io" 6 | "net/http" 7 | "path" 8 | "strings" 9 | ) 10 | 11 | func initUPRsp(rsp http.ResponseWriter) { 12 | rsp.Header().Add("Server", "CTFProxy/"+GitVersion) 13 | rsp.Header().Add("X-CTFProxy-Trace-Context", uuid.New().String()) 14 | } 15 | 16 | func copyHeader(dst, src http.Header) { 17 | for k, vv := range src { 18 | for _, v := range vv { 19 | if k == "Server" { 20 | dst.Set(k, v) 21 | } else { 22 | dst.Add(k, v) 23 | } 24 | } 25 | } 26 | } 27 | 28 | func copyResponse(rw http.ResponseWriter, resp *http.Response) error { 29 | copyHeader(rw.Header(), resp.Header) 30 | rw.WriteHeader(resp.StatusCode) 31 | defer resp.Body.Close() 32 | 33 | _, err := io.Copy(rw, resp.Body) 34 | return err 35 | } 36 | 37 | // cleanPath returns the canonical path for p, eliminating . and .. elements. 38 | func cleanPath(p string) string { 39 | if p == "" { 40 | return "/" 41 | } 42 | if p[0] != '/' { 43 | p = "/" + p 44 | } 45 | np := path.Clean(p) 46 | // path.Clean removes trailing slash except for root; 47 | // put the trailing slash back if necessary. 48 | if p[len(p)-1] == '/' && np != "/" { 49 | // Fast path for common case of p being the string we want: 50 | if len(p) == len(np)+1 && strings.HasPrefix(p, np) { 51 | np = p 52 | } else { 53 | np += "/" 54 | } 55 | } 56 | return np 57 | } 58 | -------------------------------------------------------------------------------- /infra/ctfproxy/ip.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | var privateIPBlocks []*net.IPNet 12 | 13 | func init() { 14 | for _, cidr := range []string{ 15 | "10.0.0.0/8", // RFC1918 16 | "172.16.0.0/12", // RFC1918 17 | "192.168.0.0/16", // RFC1918 18 | "100.64.0.0/10", // RFC6598 19 | } { 20 | _, block, err := net.ParseCIDR(cidr) 21 | if err != nil { 22 | panic(fmt.Errorf("parse error on %q: %v", cidr, err)) 23 | } 24 | privateIPBlocks = append(privateIPBlocks, block) 25 | } 26 | } 27 | 28 | func isDockerIP(ip net.IP) bool { 29 | for _, block := range privateIPBlocks { 30 | if block.Contains(ip) { 31 | return true 32 | } 33 | } 34 | return false 35 | } 36 | 37 | // hacky string breakdown for ptr record to look for docker network name 38 | // TODO: there's probably a more elegant way way to do this but ceebs 39 | func getServiceNameFromIP(ip string) (string, error) { 40 | pip := net.ParseIP(ip) 41 | if pip == nil || !isDockerIP(pip) { 42 | return "", errors.New("not ctfproxy service") 43 | } 44 | rdns, err := net.LookupAddr(ip) 45 | if err != nil { 46 | return "", err 47 | } 48 | if len(rdns) == 0 { 49 | return "", errors.New("no ptr record") 50 | } 51 | // fmt.Println(rdns[0]) 52 | parts := strings.Split(rdns[0], ".") 53 | if os.Getenv("CTFPROXY_CLUSTER") == "k8s" { 54 | return parts[1], nil 55 | } 56 | dockernet := parts[len(parts)-2] 57 | bcp := strings.Split(dockernet, "beyondcorp_") 58 | if len(bcp) != 2 { 59 | return "", errors.New("not beyondcorp service") 60 | } 61 | return bcp[1], nil 62 | } 63 | -------------------------------------------------------------------------------- /infra/ctfproxy/logging.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "net" 13 | "net/http" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | type uplogEntry struct { 19 | ClientIP string `json:"clientIP"` 20 | Host string `json:"host"` 21 | RequestTime string `json:"requestTime"` 22 | Latency int64 `json:"latency"` 23 | Request struct { 24 | URI string `json:"uri"` 25 | Method string `json:"method"` 26 | Version string `json:"version"` 27 | Body string `json:"body"` 28 | Header http.Header `json:"headers"` 29 | } `json:"request"` 30 | Response struct { 31 | StatusCode int `json:"statusCode"` 32 | Body string `json:"body"` 33 | Header http.Header `json:"headers"` 34 | } `json:"response"` 35 | } 36 | 37 | func WrapHandlerWithLogging(wrappedHandler http.Handler) http.Handler { 38 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 39 | // log.Printf("%v - %v %v%v", req.RemoteAddr, req.Method, req.Host, req.RequestURI) 40 | // don't log k8s health check for ctfproxy 41 | if req.Host == "ctfproxyz."+_configuration.CorpDomain && req.RequestURI == "/healthz" { 42 | wrappedHandler.ServeHTTP(w, req) 43 | return 44 | } 45 | // don't log kibana/elasticsearch requests 46 | if req.Host == "kibana."+_configuration.CorpDomain || req.Host == "elasticsearch."+_configuration.CorpDomain { 47 | wrappedHandler.ServeHTTP(w, req) 48 | return 49 | } 50 | entry := uplogEntry{} 51 | t := time.Now() 52 | entry.RequestTime = t.UTC().Format(time.RFC3339) 53 | entry.ClientIP = strings.Split(req.RemoteAddr, ":")[0] // we don't trust any proxy except ourselves 54 | entry.Host = req.Host 55 | entry.Request.URI = req.RequestURI 56 | entry.Request.Method = req.Method 57 | entry.Request.Version = fmt.Sprintf("%d.%d", req.ProtoMajor, req.ProtoMinor) 58 | entry.Request.Header = req.Header 59 | // http.MaxBytesReader might be better but let's just use io.LimitedReader since we are doing the wrapped logger. 60 | limitedReader := &io.LimitedReader{R: req.Body, N: 10485760} 61 | reqcontent, lrerr := ioutil.ReadAll(limitedReader) 62 | req.Body = ioutil.NopCloser(bytes.NewReader(reqcontent)) 63 | entry.Request.Body = string(reqcontent) 64 | buf := new(bytes.Buffer) 65 | lrw := newUplogResponseWriter(buf, w, &entry) 66 | if lrerr != nil { 67 | NewCPError(http.StatusBadRequest, "You issued a malformed request", "Entity Too Large", "", "").Write(lrw, req) 68 | } else if limitedReader.N < 1 { 69 | NewCPError(http.StatusRequestEntityTooLarge, "You issued a malformed request", "Entity Too Large", "", "").Write(lrw, req) 70 | } else if len(reqcontent) > 0 && req.Method == "GET" { 71 | NewCPError(http.StatusBadRequest, "You issued a malformed request", "No Body for GET", "", "").Write(lrw, req) 72 | } else { 73 | wrappedHandler.ServeHTTP(lrw, req) 74 | } 75 | entry.Response.Body = buf.String() 76 | // entry.Response.Header = lrw.Header() 77 | entry.Latency = (time.Now().UnixNano() - t.UnixNano()) / (int64(time.Millisecond) / int64(time.Nanosecond)) 78 | log.Printf("%v %v - %v - %v %v%v [%vms]\n", entry.ClientIP, entry.Response.Header.Get("X-CTFProxy-I-User"), entry.Response.StatusCode, entry.Request.Method, entry.Host, entry.Request.URI, entry.Latency) 79 | go func() { 80 | estr, _ := json.Marshal(entry) 81 | rsp, err := http.Post("http://elasticsearch."+_configuration.ResolvingDomain+"/ctfproxy/_doc", "application/json", bytes.NewBuffer(estr)) 82 | if err == nil { 83 | rsp.Body.Close() 84 | } else { 85 | log.Printf("Error logging: %v", err.Error()) 86 | } 87 | }() 88 | }) 89 | } 90 | 91 | type uplogResponseWriter struct { 92 | file io.Writer 93 | resp http.ResponseWriter 94 | multi io.Writer 95 | entry *uplogEntry 96 | wroteHeader bool 97 | } 98 | 99 | func newUplogResponseWriter(file io.Writer, resp http.ResponseWriter, entry *uplogEntry) http.ResponseWriter { 100 | multi := io.MultiWriter(file, resp) 101 | return &uplogResponseWriter{ 102 | file: file, 103 | resp: resp, 104 | multi: multi, 105 | entry: entry, 106 | wroteHeader: false, 107 | } 108 | } 109 | 110 | // implement http.ResponseWriter 111 | // https://golang.org/pkg/net/http/#ResponseWriter 112 | func (w *uplogResponseWriter) Header() http.Header { 113 | return w.resp.Header() 114 | } 115 | 116 | func (w *uplogResponseWriter) Write(b []byte) (int, error) { 117 | // this ensures we remove debug headers 118 | if !w.wroteHeader { 119 | _, hasContentType := w.resp.Header()["Content-Type"] 120 | // if Content-Encoding is non-blank, we shouldn't sniff the body. 121 | if len(w.resp.Header().Get("Content-Encoding")) == 0 && !hasContentType && len(b) > 0 { 122 | w.resp.Header().Set("Content-Type", http.DetectContentType(b)) 123 | } 124 | w.WriteHeader(http.StatusOK) 125 | } 126 | return w.multi.Write(b) 127 | } 128 | 129 | func (w *uplogResponseWriter) WriteHeader(i int) { 130 | if !w.wroteHeader { 131 | w.wroteHeader = true 132 | w.entry.Response.StatusCode = i 133 | w.entry.Response.Header = w.resp.Header().Clone() 134 | if w.resp.Header().Get("X-CTFProxy-I-Debug") == "" { 135 | for k := range w.resp.Header() { 136 | if strings.HasPrefix(strings.ToLower(k), "x-ctfproxy-i-") { 137 | w.resp.Header().Del(k) 138 | } 139 | } 140 | } 141 | w.resp.WriteHeader(i) 142 | } 143 | } 144 | 145 | // websocket needs this 146 | func (w *uplogResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 147 | if hj, ok := w.resp.(http.Hijacker); ok { 148 | return hj.Hijack() 149 | } 150 | return nil, nil, errors.New("Error in hijacker") 151 | } 152 | -------------------------------------------------------------------------------- /infra/ctfproxy/login.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "html/template" 8 | "net/http" 9 | "strings" 10 | "time" 11 | 12 | "github.com/dgrijalva/jwt-go" 13 | _ "github.com/go-sql-driver/mysql" 14 | "github.com/adamyi/CTFProxy/infra/ctfproxy/templates" 15 | "github.com/adamyi/CTFProxy/third_party/eddsa" 16 | ) 17 | 18 | func verifyPassword(email, password string) bool { 19 | 20 | data, err := json.Marshal(struct { 21 | Username string `json:"username"` 22 | Password string `json:"password"` 23 | }{email, password}) 24 | if err != nil { 25 | fmt.Println(err) 26 | return false 27 | } 28 | 29 | r, err := http.Post(_configuration.GAIAEndpoint+"/api/login", "application/json", bytes.NewBuffer(data)) 30 | if err != nil { 31 | fmt.Println(err) 32 | return false 33 | } 34 | r.Body.Close() 35 | 36 | return r.StatusCode == 200 37 | } 38 | 39 | var loginTemplate *template.Template 40 | 41 | func init() { 42 | loginTemplate = template.Must(template.New("login").Parse(templates.Data["login.html"])) 43 | } 44 | 45 | func handleLogin(rsp http.ResponseWriter, req *http.Request) { 46 | if req.Method == "GET" { 47 | loginTemplate.Execute(rsp, "") 48 | } else if req.Method == "POST" { 49 | req.ParseForm() 50 | username := strings.ToLower(req.Form.Get("username")) 51 | if !strings.HasSuffix(username, "@"+_configuration.CorpDomain) { 52 | username = username + "@" + _configuration.CorpDomain 53 | } 54 | password := req.Form.Get("password") 55 | 56 | if !verifyPassword(username, password) { 57 | loginTemplate.Execute(rsp, "Incorrect password") 58 | return 59 | } 60 | 61 | expirationTime := time.Now().Add(24 * 30 * time.Hour) 62 | pclaims := Claims{ 63 | Username: username, 64 | Displayname: strings.Split(username, "@")[0], 65 | Service: _configuration.ServiceName + "@services." + _configuration.CorpDomain, 66 | StandardClaims: jwt.StandardClaims{ 67 | ExpiresAt: expirationTime.Unix(), 68 | }, 69 | } 70 | ptoken := jwt.NewWithClaims(eddsa.SigningMethodEdDSA, pclaims) 71 | ptstr, err := ptoken.SignedString(_configuration.SignKey) 72 | if err != nil { 73 | NewCPError(http.StatusInternalServerError, "Internal Server Error", "Something went wrong while generating JWT", "", err.Error()).Write(rsp, req) 74 | return 75 | } 76 | authcookie := &http.Cookie{Name: _configuration.AuthCookieKey, Value: ptstr, HttpOnly: true, Domain: _configuration.CorpDomain} 77 | http.SetCookie(rsp, authcookie) 78 | if req.URL.Query().Get("return_url") == "" { 79 | http.Redirect(rsp, req, "https://whoami."+_configuration.CorpDomain, http.StatusFound) 80 | } else { 81 | http.Redirect(rsp, req, req.URL.Query().Get("return_url"), http.StatusFound) 82 | } 83 | } else { 84 | NewCPError(http.StatusMethodNotAllowed, "Method Not Allowed", "Only GET and POST are supported", "", "").Write(rsp, req) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /infra/ctfproxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | func main() { 10 | rand.Seed(time.Now().UnixNano()) 11 | readConfig() 12 | initRateLimit() 13 | 14 | server := buildSSLServer() 15 | go http.ListenAndServe(_configuration.ListenAddress, certManager.HTTPHandler(http.HandlerFunc(redirectSSL))) 16 | server.ListenAndServeTLS("", "") 17 | } 18 | -------------------------------------------------------------------------------- /infra/ctfproxy/network.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/gorilla/websocket" 7 | "net" 8 | "net/http" 9 | "os" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | var ( 15 | dialer = &net.Dialer{ 16 | Timeout: 30 * time.Second, 17 | KeepAlive: 30 * time.Second, 18 | DualStack: false, 19 | } 20 | ) 21 | 22 | func upDialContext(ctx context.Context, network, address string) (net.Conn, error) { 23 | if v := ctx.Value("up_real_addr"); v != nil { 24 | address = v.(string) 25 | } 26 | // fmt.Println("updialContext", address) 27 | return dialer.DialContext(ctx, network, address) 28 | } 29 | 30 | func getServiceNameFromDomain(domain string) (string, error) { 31 | host := strings.ToLower(domain) 32 | if !strings.HasSuffix(host, "."+_configuration.CorpDomain) { 33 | return "", errors.New("not corp domain") 34 | } 35 | 36 | sn := strings.ReplaceAll(host[:len(host)-len(_configuration.CorpDomain)-1], ".", "-dot-") 37 | 38 | if tsn, ok := _configuration.ServiceAliases.ConfigOrNil().(map[string]string)[sn]; ok { 39 | return tsn, nil 40 | } 41 | return sn, nil 42 | } 43 | 44 | func getRealAddr(host string) (string, error) { 45 | sn, err := getServiceNameFromDomain(host) 46 | if err != nil { 47 | return "", err 48 | } 49 | host = sn + "." + _configuration.ResolvingDomain 50 | 51 | ips, err := net.LookupIP(host) 52 | if err != nil { 53 | return "", err 54 | } 55 | 56 | for _, ip := range ips { 57 | if !isDockerIP(ip) { 58 | return "", errors.New("not internal ip") 59 | } 60 | } 61 | 62 | return ips[0].String() + ":80", nil 63 | } 64 | 65 | func getL2Addr(player string) (string, error) { 66 | if os.Getenv("CTFPROXY_CLUSTER") == "all" { 67 | return "", errors.New("levelshift disabled on all server") 68 | } 69 | if os.Getenv("CTFPROXY_CLUSTER") != "master" { 70 | player = "master" 71 | } 72 | host := player + ".prod." + _configuration.CorpDomain 73 | ips, err := net.LookupIP(host) 74 | // fmt.Println("getL2", ips, err, host) 75 | if err != nil || len(ips) == 0 { 76 | return "", errors.New("not valid internal") 77 | } 78 | 79 | return ips[0].String() + ":443", nil 80 | } 81 | 82 | func getNetworkContext(req *http.Request, username string) (context.Context, bool, error) { 83 | addr, err := getRealAddr(req.Host) 84 | if err == nil { 85 | return context.WithValue(context.Background(), "up_real_addr", addr), false, nil 86 | } 87 | 88 | if req.Header.Get("X-CTFProxy-LevelShift") == "1" { 89 | return context.Background(), false, errors.New("domain not present in two-level UP infra") 90 | } 91 | 92 | players := strings.Split(strings.Split(username, "@")[0], "+") 93 | hp := req.Header.Get("X-CTFProxy-Player") 94 | if hp != "" { 95 | players = append(players, hp) 96 | } 97 | // fmt.Println(players) 98 | for _, player := range players { 99 | addr, err = getL2Addr(player) 100 | if err == nil { 101 | return context.WithValue(context.Background(), "up_real_addr", addr), true, nil 102 | } 103 | } 104 | return context.Background(), false, errors.New("not found anywhere") 105 | } 106 | 107 | func init() { 108 | http.DefaultTransport.(*http.Transport).DialContext = upDialContext 109 | websocket.DefaultDialer.NetDialContext = upDialContext 110 | } 111 | -------------------------------------------------------------------------------- /infra/ctfproxy/ssl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "errors" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/adamyi/CTFProxy/third_party/autocertcache" 14 | 15 | "golang.org/x/crypto/acme/autocert" 16 | ) 17 | 18 | func redirectSSL(rsp http.ResponseWriter, req *http.Request) { 19 | target := "https://" + req.Host + req.URL.Path 20 | if len(req.URL.RawQuery) > 0 { 21 | target += "?" + req.URL.RawQuery 22 | } 23 | http.Redirect(rsp, req, target, 24 | http.StatusTemporaryRedirect) 25 | } 26 | 27 | var authCA *x509.CertPool 28 | var certManager *autocert.Manager 29 | 30 | func buildSSLServer() *http.Server { 31 | authCA = x509.NewCertPool() 32 | 33 | cacert, err := ioutil.ReadFile(_configuration.MTLSCA) 34 | cfg := &tls.Config{} 35 | if err != nil && len(cacert) > 0 { 36 | log.Println("Error reading mTLS CA or CA file empty. mTLS Auth disabled.") 37 | log.Println(err) 38 | } else { 39 | log.Println("mTLS auth enabled") 40 | authCA.AppendCertsFromPEM(cacert) 41 | cfg = &tls.Config{ 42 | ClientAuth: tls.RequestClientCert, 43 | ClientCAs: authCA, 44 | } 45 | } 46 | 47 | cfg.Certificates = make([]tls.Certificate, len(_configuration.SSLCertificates)) 48 | for i, cert := range _configuration.SSLCertificates { 49 | cfg.Certificates[i], err = tls.LoadX509KeyPair(cert, _configuration.SSLPrivateKeys[i]) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | } 54 | 55 | // i'm lazy 56 | cfg.BuildNameToCertificate() 57 | ntc := cfg.NameToCertificate 58 | cfg.NameToCertificate = nil 59 | 60 | hostPolicy := func(ctx context.Context, host string) error { 61 | sn, err := getServiceNameFromDomain(host) 62 | if err != nil { 63 | return err 64 | } 65 | if _, ok := _configuration.AccessPolicies.ConfigOrNil().(map[string]string)[sn]; ok { 66 | return nil 67 | } 68 | return errors.New("non-existing host") 69 | } 70 | 71 | certManager = &autocert.Manager{ 72 | Prompt: autocert.AcceptTOS, 73 | HostPolicy: hostPolicy, 74 | Cache: autocertcache.NewGoogleCloudStorageCache(gcsClient, _configuration.CertBucket), 75 | } 76 | 77 | cfg.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { 78 | name := strings.ToLower(hello.ServerName) 79 | if cert, ok := ntc[name]; ok { 80 | return cert, nil 81 | } 82 | if len(name) > 0 { 83 | labels := strings.Split(name, ".") 84 | labels[0] = "*" 85 | wildcardName := strings.Join(labels, ".") 86 | if cert, ok := ntc[wildcardName]; ok { 87 | return cert, nil 88 | } 89 | } 90 | return certManager.GetCertificate(hello) 91 | } 92 | 93 | server := &http.Server{ 94 | Addr: _configuration.SSLListenAddress, 95 | Handler: WrapHandlerWithLogging(WrapHandlerWithRecovery(http.HandlerFunc(handleUP))), 96 | //Handler: http.HandlerFunc(handleUP), 97 | TLSConfig: cfg, 98 | } 99 | return server 100 | } 101 | -------------------------------------------------------------------------------- /infra/ctfproxy/templates/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//extras:embed_data.bzl", "go_embed_data") 2 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 3 | 4 | go_embed_data( 5 | name = "templates", 6 | srcs = glob(["*.html"]), 7 | flatten = True, 8 | package = "templates", 9 | string = True, 10 | ) 11 | 12 | # keep 13 | go_library( 14 | name = "go_default_library", 15 | srcs = [":templates"], 16 | importpath = "github.com/adamyi/CTFProxy/infra/ctfproxy/templates", 17 | visibility = ["//infra/ctfproxy:__pkg__"], 18 | ) 19 | -------------------------------------------------------------------------------- /infra/ctfproxy/websocket.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/gorilla/websocket" 12 | ) 13 | 14 | var ( 15 | wsUpgrader = websocket.Upgrader{CheckOrigin: wsCheckOrigin} 16 | wsDialer = websocket.DefaultDialer 17 | ) 18 | 19 | func wsCheckOrigin(r *http.Request) bool { 20 | o := r.Header.Get("Origin") 21 | h := r.Host 22 | if o == "" || h == "" { 23 | log.Print("Websocket missing origin and/or host") 24 | return false 25 | } 26 | ou, err := url.Parse(o) 27 | if err != nil { 28 | log.Printf("Couldn't parse url: %v", err) 29 | return false 30 | } 31 | if !strings.HasSuffix(ou.Host, _configuration.CorpDomain) { 32 | log.Print("Origin doesn't match host") 33 | return false 34 | } 35 | return true 36 | } 37 | 38 | // adapted from https://github.com/koding/websocketproxy/blob/master/websocketproxy.go 39 | func handleWs(ctx context.Context, rsp http.ResponseWriter, req *http.Request, jwttoken string, levelShift bool) { 40 | requestHeader := http.Header{} 41 | requestHeader.Set("Host", req.Host) 42 | requestHeader.Set(_configuration.InternalJWTHeader, jwttoken) 43 | if origin := req.Header.Get("Origin"); origin != "" { 44 | requestHeader.Add("Origin", origin) 45 | } 46 | for _, prot := range req.Header[http.CanonicalHeaderKey("Sec-WebSocket-Protocol")] { 47 | requestHeader.Add("Sec-WebSocket-Protocol", prot) 48 | } 49 | for _, cookie := range req.Header[http.CanonicalHeaderKey("Cookie")] { 50 | requestHeader.Add("Cookie", cookie) 51 | } 52 | backendURL := *req.URL 53 | backendURL.Host = req.Host 54 | backendURL.Scheme = "ws" 55 | 56 | if levelShift { 57 | backendURL.Scheme = "wss" 58 | requestHeader.Add("X-CTFProxy-LevelShift", "1") 59 | } 60 | 61 | // dial backend 62 | connBackend, resp, err := wsDialer.DialContext(ctx, backendURL.String(), requestHeader) 63 | if err != nil { 64 | log.Printf("couldn't dial to remote backend url %s %s", backendURL.String(), err) 65 | if resp != nil { 66 | // If the WebSocket handshake fails, ErrBadHandshake is returned 67 | // along with a non-nil *http.Response so that callers can handle 68 | // redirects, authentication, etcetera. 69 | // But first we need to check if it's a CTFProxy error to avoid 70 | // internalDebug leak 71 | if resp.Header.Get("Content-Type") == "ctfproxy/error" { 72 | handleUpstreamCPError(rsp, resp, req) 73 | return 74 | } 75 | if err := copyResponse(rsp, resp); err != nil { 76 | log.Printf("couldn't write response after failed remote backend handshake: %s", err) 77 | } 78 | } else { 79 | http.Error(rsp, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) 80 | } 81 | return 82 | } 83 | defer connBackend.Close() 84 | 85 | // Only pass those headers to the upgrader. 86 | upgradeHeader := http.Header{} 87 | if hdr := resp.Header.Get("Sec-Websocket-Protocol"); hdr != "" { 88 | upgradeHeader.Set("Sec-Websocket-Protocol", hdr) 89 | } 90 | if hdr := resp.Header.Get("Set-Cookie"); hdr != "" { 91 | upgradeHeader.Set("Set-Cookie", hdr) 92 | } 93 | 94 | connPub, err := wsUpgrader.Upgrade(rsp, req, upgradeHeader) 95 | if err != nil { 96 | log.Printf("couldn't upgrade %s", err) 97 | return 98 | } 99 | defer connPub.Close() 100 | 101 | errClient := make(chan error, 1) 102 | errBackend := make(chan error, 1) 103 | replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error) { 104 | for { 105 | msgType, msg, err := src.ReadMessage() 106 | if err != nil { 107 | m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) 108 | if e, ok := err.(*websocket.CloseError); ok { 109 | if e.Code != websocket.CloseNoStatusReceived { 110 | m = websocket.FormatCloseMessage(e.Code, e.Text) 111 | } 112 | } 113 | errc <- err 114 | dst.WriteMessage(websocket.CloseMessage, m) 115 | break 116 | } 117 | err = dst.WriteMessage(msgType, msg) 118 | if err != nil { 119 | errc <- err 120 | break 121 | } 122 | } 123 | } 124 | 125 | go replicateWebsocketConn(connPub, connBackend, errClient) 126 | go replicateWebsocketConn(connBackend, connPub, errBackend) 127 | 128 | var message string 129 | select { 130 | case err = <-errClient: 131 | message = "Error when copying from backend to client: %v" 132 | case err = <-errBackend: 133 | message = "Error when copying from client to backend: %v" 134 | 135 | } 136 | if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure { 137 | log.Printf(message, err) 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /infra/dns/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_docker//go:image.bzl", "go_image") 2 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 3 | load("//:config.bzl", "CTF_DOMAIN") 4 | 5 | go_image( 6 | name = "image", 7 | args = [ 8 | "-ctf_domain", 9 | CTF_DOMAIN, 10 | ], 11 | embed = [":go_default_library"], 12 | visibility = ["//visibility:public"], 13 | ) 14 | 15 | go_library( 16 | name = "go_default_library", 17 | srcs = ["main.go"], 18 | importpath = "github.com/adamyi/CTFProxy/infra/dns", 19 | visibility = ["//visibility:private"], 20 | deps = ["@com_github_miekg_dns//:go_default_library"], 21 | ) 22 | 23 | go_binary( 24 | name = "dns", 25 | embed = [":go_default_library"], 26 | visibility = ["//visibility:public"], 27 | ) 28 | -------------------------------------------------------------------------------- /infra/dns/README.md: -------------------------------------------------------------------------------- 1 | ## LICENSE 2 | 3 | Modified from https://github.com/adamyi/Geegle3, original license: 4 | 5 | Copyright (c) 2019 [Adam Yi](mailto:i@adamyi.com), [Adam Tanana](mailto:adam@tanana.io), [Lachlan Jones](mailto:contact@lachjones.com) 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | -------------------------------------------------------------------------------- /infra/dns/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | var recursor string 15 | var ctf_domain string 16 | 17 | func parseQuery(m *dns.Msg, ip string) { 18 | for _, q := range m.Question { 19 | switch q.Qtype { 20 | case dns.TypeA: 21 | log.Printf("Query for %s\n", q.Name) 22 | s := strings.Split(ip, ".") 23 | i, _ := strconv.Atoi(s[3]) 24 | s[3] = strconv.Itoa(i - 1) 25 | rr, err := dns.NewRR(fmt.Sprintf("%s A %s", q.Name, strings.Join(s, "."))) 26 | if err == nil { 27 | m.Answer = append(m.Answer, rr) 28 | } 29 | } 30 | } 31 | } 32 | 33 | func handleCtfDnsRequest(w dns.ResponseWriter, r *dns.Msg) { 34 | m := new(dns.Msg) 35 | m.SetReply(r) 36 | m.Compress = false 37 | 38 | switch r.Opcode { 39 | case dns.OpcodeQuery: 40 | parseQuery(m, strings.Split(w.RemoteAddr().String(), ":")[0]) 41 | } 42 | 43 | w.WriteMsg(m) 44 | } 45 | 46 | func handleOtherDnsRequest(resp dns.ResponseWriter, req *dns.Msg) { 47 | if len(req.Question) == 0 { 48 | respond(resp, req, dns.RcodeFormatError) 49 | return 50 | } 51 | 52 | network := "udp" 53 | if _, ok := resp.RemoteAddr().(*net.TCPAddr); ok { 54 | network = "tcp" 55 | } 56 | 57 | c := &dns.Client{Net: network} 58 | r, _, err := c.Exchange(req, recursor) 59 | if err == nil { 60 | log.Printf("[info] using %s to answer %s", recursor, req.Question[0].Name) 61 | if err := resp.WriteMsg(r); err != nil { 62 | log.Printf("[WARN] dns: failed to respond: %v", err) 63 | } 64 | return 65 | } 66 | 67 | respond(resp, req, dns.RcodeServerFailure) 68 | } 69 | 70 | func respond(resp dns.ResponseWriter, req *dns.Msg, code int) { 71 | m := &dns.Msg{} 72 | m.SetReply(req) 73 | m.RecursionAvailable = true 74 | m.SetRcode(req, code) 75 | resp.WriteMsg(m) 76 | } 77 | 78 | func main() { 79 | var ctf_domain string 80 | flag.StringVar(&ctf_domain, "ctf_domain", "", "CTF Domain") 81 | flag.StringVar(&recursor, "dns_server", "8.8.8.8:53", "External DNS Resolver") 82 | flag.Parse() 83 | if ctf_domain == "" { 84 | panic("Please set ctf_domain") 85 | } 86 | dns.HandleFunc(ctf_domain+".", handleCtfDnsRequest) 87 | dns.HandleFunc(".", handleOtherDnsRequest) 88 | 89 | port := 53 90 | server := &dns.Server{Addr: ":" + strconv.Itoa(port), Net: "udp"} 91 | log.Printf("Starting at %d\n", port) 92 | err := server.ListenAndServe() 93 | defer server.Shutdown() 94 | if err != nil { 95 | log.Fatalf("Failed to start server: %s\n ", err.Error()) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /infra/elk/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_docker//container:container.bzl", "container_image") 2 | load("//:config.bzl", "CTF_DOMAIN") 3 | load("//tools:challenge.bzl", "ctf_challenge") 4 | 5 | ctf_challenge() 6 | 7 | container_image( 8 | name = "elasticsearch", 9 | base = "@elasticsearch//image", 10 | env = { 11 | "TAKE_FILE_OWNERSHIP": "1", 12 | }, 13 | files = [ 14 | ":elasticsearch.yml", 15 | ":elasticsearch-docker", 16 | ], 17 | ports = [], 18 | symlinks = { 19 | "/usr/local/bin/docker-entrypoint.sh": "/elasticsearch-docker", 20 | "/usr/share/elasticsearch/config/elasticsearch.yml": "/elasticsearch.yml", 21 | }, 22 | user = "root", 23 | visibility = ["//visibility:public"], 24 | ) 25 | 26 | container_image( 27 | name = "kibana", 28 | base = "@kibana//image", 29 | env = { 30 | "ELASTICSEARCH_HOSTS": "https://elasticsearch." + CTF_DOMAIN, 31 | }, 32 | files = [ 33 | ":kibana.yml", 34 | ":kibana-docker", 35 | ], 36 | ports = [], 37 | symlinks = { 38 | "/usr/local/bin/kibana-docker": "/kibana-docker", 39 | "/usr/share/kibana/config/kibana.yml": "/kibana.yml", 40 | }, 41 | user = "root", 42 | visibility = ["//visibility:public"], 43 | ) 44 | -------------------------------------------------------------------------------- /infra/elk/challenge.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | services: [ 3 | { 4 | name: 'elasticsearch', 5 | category: 'infra', 6 | image: 'gcr.io/ctfproxy/elk/elasticsearch:latest', 7 | clustertype: 'master', 8 | access: ||| 9 | def checkAccess(): 10 | if user == "kibana@services." + corpDomain: 11 | grantAccess() 12 | checkAccess() 13 | |||, 14 | health: '/_cat/health', 15 | startTime: 60, 16 | persistent: '250G', 17 | }, 18 | { 19 | name: 'kibana', 20 | category: 'infra', 21 | image: 'gcr.io/ctfproxy/elk/kibana:latest', 22 | clustertype: 'master', 23 | access: ||| 24 | def checkAccess(): 25 | if ("kibana-user@groups." + corpDomain) in groups: 26 | grantAccess() 27 | checkAccess() 28 | |||, 29 | health: '/api/status', 30 | startTime: 60, 31 | }, 32 | ], 33 | } 34 | -------------------------------------------------------------------------------- /infra/elk/elasticsearch-docker: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Files created by Elasticsearch should always be group writable too 5 | umask 0002 6 | 7 | run_as_other_user_if_needed() { 8 | if [[ "$(id -u)" == "0" ]]; then 9 | # If running as root, drop to specified UID and run command 10 | exec chroot --userspec=1000 / "${@}" 11 | else 12 | # Either we are running in Openshift with random uid and are a member of the root group 13 | # or with a custom --user 14 | exec "${@}" 15 | fi 16 | } 17 | 18 | # Allow user specify custom CMD, maybe bin/elasticsearch itself 19 | # for example to directly specify `-E` style parameters for elasticsearch on k8s 20 | # or simply to run /bin/bash to check the image 21 | if [[ "$1" != "eswrapper" ]]; then 22 | if [[ "$(id -u)" == "0" && $(basename "$1") == "elasticsearch" ]]; then 23 | # centos:7 chroot doesn't have the `--skip-chdir` option and 24 | # changes our CWD. 25 | # Rewrite CMD args to replace $1 with `elasticsearch` explicitly, 26 | # so that we are backwards compatible with the docs 27 | # from the previous Elasticsearch versions<6 28 | # and configuration option D: 29 | # https://www.elastic.co/guide/en/elasticsearch/reference/5.6/docker.html#_d_override_the_image_8217_s_default_ulink_url_https_docs_docker_com_engine_reference_run_cmd_default_command_or_options_cmd_ulink 30 | # Without this, user could specify `elasticsearch -E x.y=z` but 31 | # `bin/elasticsearch -E x.y=z` would not work. 32 | set -- "elasticsearch" "${@:2}" 33 | # Use chroot to switch to UID 1000 34 | exec chroot --userspec=1000 / "$@" 35 | else 36 | # User probably wants to run something else, like /bin/bash, with another uid forced (Openshift?) 37 | exec "$@" 38 | fi 39 | fi 40 | 41 | # Allow environment variables to be set by creating a file with the 42 | # contents, and setting an environment variable with the suffix _FILE to 43 | # point to it. This can be used to provide secrets to a container, without 44 | # the values being specified explicitly when running the container. 45 | # 46 | # This is also sourced in elasticsearch-env, and is only needed here 47 | # as well because we use ELASTIC_PASSWORD below. Sourcing this script 48 | # is idempotent. 49 | source /usr/share/elasticsearch/bin/elasticsearch-env-from-file 50 | 51 | if [[ -f bin/elasticsearch-users ]]; then 52 | # Check for the ELASTIC_PASSWORD environment variable to set the 53 | # bootstrap password for Security. 54 | # 55 | # This is only required for the first node in a cluster with Security 56 | # enabled, but we have no way of knowing which node we are yet. We'll just 57 | # honor the variable if it's present. 58 | if [[ -n "$ELASTIC_PASSWORD" ]]; then 59 | [[ -f /usr/share/elasticsearch/config/elasticsearch.keystore ]] || (run_as_other_user_if_needed elasticsearch-keystore create) 60 | if ! (run_as_other_user_if_needed elasticsearch-keystore list | grep -q '^bootstrap.password$'); then 61 | (run_as_other_user_if_needed echo "$ELASTIC_PASSWORD" | elasticsearch-keystore add -x 'bootstrap.password') 62 | fi 63 | fi 64 | fi 65 | 66 | mkdir -p /data 67 | 68 | if [[ "$(id -u)" == "0" ]]; then 69 | # If requested and running as root, mutate the ownership of bind-mounts 70 | if [[ -n "$TAKE_FILE_OWNERSHIP" ]]; then 71 | chown -R 1000:0 /usr/share/elasticsearch/logs 72 | chown -R 1000:0 /data 73 | fi 74 | fi 75 | 76 | chmod -R 775 /usr/share/elasticsearch/config 77 | setcap CAP_NET_BIND_SERVICE=+eip /usr/share/elasticsearch/bin/elasticsearch 78 | setcap CAP_NET_BIND_SERVICE=+eip /usr/share/elasticsearch/modules/x-pack-ml/platform/linux-x86_64/bin/controller 79 | setcap CAP_NET_BIND_SERVICE=+eip /usr/share/elasticsearch/jdk/bin/java 80 | echo /usr/share/elasticsearch/jdk/lib > /etc/ld.so.conf.d/java.conf 81 | ldconfig 82 | run_as_other_user_if_needed /usr/share/elasticsearch/bin/elasticsearch 83 | -------------------------------------------------------------------------------- /infra/elk/elasticsearch.yml: -------------------------------------------------------------------------------- 1 | network.host: '0.0.0.0' 2 | node.name: 'main' 3 | path.data: /data 4 | discovery.type: single-node 5 | http.port: 80 6 | http.cors.enabled: true 7 | http.cors.allow-origin: '*' 8 | http.cors.allow-headers: X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization,X-CTFProxy-JWT 9 | http.cors.allow-methods: OPTIONS,HEAD,GET,POST,PUT,DELETE 10 | http.cors.allow-credentials: true 11 | xpack.security.authc: 12 | anonymous: 13 | username: anonymous_user 14 | roles: admin 15 | authz_exception: true 16 | xpack.ml.enabled: false 17 | -------------------------------------------------------------------------------- /infra/elk/kibana.yml: -------------------------------------------------------------------------------- 1 | # Kibana is served by a back end server. This setting specifies the port to use. 2 | server.port: 80 3 | 4 | # Specifies the address to which the Kibana server will bind. IP addresses and host names are both valid values. 5 | # The default is 'localhost', which usually means remote machines will not be able to connect. 6 | # To allow connections from remote users, set this parameter to a non-loopback address. 7 | server.host: "0.0.0.0" 8 | 9 | # Enables you to specify a path to mount Kibana at if you are running behind a proxy. 10 | # Use the `server.rewriteBasePath` setting to tell Kibana if it should remove the basePath 11 | # from requests it receives, and to prevent a deprecation warning at startup. 12 | # This setting cannot end in a slash. 13 | #server.basePath: "" 14 | 15 | # Specifies whether Kibana should rewrite requests that are prefixed with 16 | # `server.basePath` or require that they are rewritten by your reverse proxy. 17 | # This setting was effectively always `false` before Kibana 6.3 and will 18 | # default to `true` starting in Kibana 7.0. 19 | #server.rewriteBasePath: false 20 | 21 | # Specifies the default route when opening Kibana. You can use this setting to modify 22 | # the landing page when opening Kibana. 23 | #server.defaultRoute: /app/kibana 24 | 25 | # The maximum payload size in bytes for incoming server requests. 26 | #server.maxPayloadBytes: 1048576 27 | 28 | # The Kibana server's name. This is used for display purposes. 29 | #server.name: "your-hostname" 30 | 31 | # We use environment variable to inject this 32 | # The URLs of the Elasticsearch instances to use for all your queries. 33 | # elasticsearch.hosts: [] 34 | 35 | # When this setting's value is true Kibana uses the hostname specified in the server.host 36 | # setting. When the value of this setting is false, Kibana uses the hostname of the host 37 | # that connects to this Kibana instance. 38 | #elasticsearch.preserveHost: true 39 | 40 | # Kibana uses an index in Elasticsearch to store saved searches, visualizations and 41 | # dashboards. Kibana creates a new index if the index doesn't already exist. 42 | #kibana.index: ".kibana" 43 | 44 | # If your Elasticsearch is protected with basic authentication, these settings provide 45 | # the username and password that the Kibana server uses to perform maintenance on the Kibana 46 | # index at startup. Your Kibana users still need to authenticate with Elasticsearch, which 47 | # is proxied through the Kibana server. 48 | #elasticsearch.username: "user" 49 | #elasticsearch.password: "pass" 50 | 51 | # Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively. 52 | # These settings enable SSL for outgoing requests from the Kibana server to the browser. 53 | #server.ssl.enabled: false 54 | #server.ssl.certificate: /path/to/your/server.crt 55 | #server.ssl.key: /path/to/your/server.key 56 | 57 | # Optional settings that provide the paths to the PEM-format SSL certificate and key files. 58 | # These files validate that your Elasticsearch backend uses the same key files. 59 | #elasticsearch.ssl.certificate: /path/to/your/client.crt 60 | #elasticsearch.ssl.key: /path/to/your/client.key 61 | 62 | # Optional setting that enables you to specify a path to the PEM file for the certificate 63 | # authority for your Elasticsearch instance. 64 | #elasticsearch.ssl.certificateAuthorities: [ "/path/to/your/CA.pem" ] 65 | 66 | # To disregard the validity of SSL certificates, change this setting's value to 'none'. 67 | #elasticsearch.ssl.verificationMode: full 68 | 69 | # Time in milliseconds to wait for Elasticsearch to respond to pings. Defaults to the value of 70 | # the elasticsearch.requestTimeout setting. 71 | #elasticsearch.pingTimeout: 1500 72 | 73 | # Time in milliseconds to wait for responses from the back end or Elasticsearch. This value 74 | # must be a positive integer. 75 | #elasticsearch.requestTimeout: 30000 76 | 77 | # List of Kibana client-side headers to send to Elasticsearch. To send *no* client-side 78 | # headers, set this value to [] (an empty list). 79 | #elasticsearch.requestHeadersWhitelist: [ authorization ] 80 | 81 | # Header names and values that are sent to Elasticsearch. Any custom headers cannot be overwritten 82 | # by client-side headers, regardless of the elasticsearch.requestHeadersWhitelist configuration. 83 | #elasticsearch.customHeaders: {} 84 | 85 | # Time in milliseconds for Elasticsearch to wait for responses from shards. Set to 0 to disable. 86 | #elasticsearch.shardTimeout: 30000 87 | 88 | # Time in milliseconds to wait for Elasticsearch at Kibana startup before retrying. 89 | #elasticsearch.startupTimeout: 5000 90 | 91 | # Logs queries sent to Elasticsearch. Requires logging.verbose set to true. 92 | #elasticsearch.logQueries: false 93 | 94 | # Specifies the path where Kibana creates the process ID file. 95 | #pid.file: /var/run/kibana.pid 96 | 97 | # Enables you specify a file where Kibana stores log output. 98 | #logging.dest: stdout 99 | 100 | # Set the value of this setting to true to suppress all logging output. 101 | #logging.silent: false 102 | 103 | # Set the value of this setting to true to suppress all logging output other than error messages. 104 | #logging.quiet: false 105 | 106 | # Set the value of this setting to true to log all events, including system usage information 107 | # and all requests. 108 | #logging.verbose: false 109 | 110 | # Set the interval in milliseconds to sample system and process performance 111 | # metrics. Minimum is 100ms. Defaults to 5000. 112 | #ops.interval: 5000 113 | 114 | # Specifies locale to be used for all localizable strings, dates and number formats. 115 | #i18n.locale: "en" 116 | -------------------------------------------------------------------------------- /infra/flaganizer/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_docker//go:image.bzl", "go_image") 2 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 3 | load("@io_bazel_rules_jsonnet//jsonnet:jsonnet.bzl", "jsonnet_to_json") 4 | load("//tools:challenge.bzl", "ctf_challenge") 5 | 6 | ctf_challenge() 7 | 8 | go_image( 9 | name = "image", 10 | args = [ 11 | "-jwt_public_key", 12 | "$(location //jwtkeys:jwt.pub)", 13 | "-flag_config", 14 | "infra/flaganizer/flags.json", 15 | # FIXME: configure flag_key here 16 | ], 17 | data = [ 18 | ":flags", 19 | "//jwtkeys:jwt.pub", 20 | ], 21 | embed = [":go_default_library"], 22 | visibility = ["//visibility:public"], 23 | ) 24 | 25 | go_library( 26 | name = "go_default_library", 27 | srcs = ["main.go"], 28 | importpath = "github.com/adamyi/CTFProxy/infra/flaganizer", 29 | visibility = ["//visibility:private"], 30 | deps = [ 31 | "//third_party/eddsa:go_default_library", 32 | "@com_github_dgrijalva_jwt_go//:go_default_library", 33 | "@com_github_go_sql_driver_mysql//:go_default_library", 34 | ], 35 | ) 36 | 37 | go_binary( 38 | name = "flaganizer", 39 | embed = [":go_default_library"], 40 | visibility = ["//visibility:public"], 41 | ) 42 | 43 | jsonnet_to_json( 44 | name = "flags", 45 | src = "flags.jsonnet", 46 | outs = ["flags.json"], 47 | deps = [ 48 | "//challenges:challenges_jsonnet", 49 | "//infra:infra_jsonnet", 50 | "//infra/jsonnet:utils", 51 | ], 52 | ) 53 | -------------------------------------------------------------------------------- /infra/flaganizer/README.md: -------------------------------------------------------------------------------- 1 | # flaganizer 2 | 3 | Trivially and dynamically generate and verify flags. 4 | -------------------------------------------------------------------------------- /infra/flaganizer/challenge.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | services: [ 3 | { 4 | name: 'flaganizer', 5 | replicas: 1, 6 | category: 'infra', 7 | clustertype: 'master', 8 | access: ||| 9 | def checkAccess(): 10 | if user.endswith("@services." + corpDomain): 11 | grantAccess() 12 | checkAccess() 13 | |||, 14 | }, 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /infra/flaganizer/flags.jsonnet: -------------------------------------------------------------------------------- 1 | local challenges = import 'challenges/challenges.libsonnet'; 2 | local infras = import 'infra/challenges.libsonnet'; 3 | local utils = import 'infra/jsonnet/utils.libsonnet'; 4 | 5 | local combined = challenges + infras; 6 | 7 | /*Id string 8 | DisplayName string 9 | Category string 10 | Points int 11 | Type string 12 | Flag string 13 | Prefix string 14 | Owner string*/ 15 | 16 | local parseFlag(flag, chal) = { 17 | Id: if 'Id' in flag then flag.Id else error 'Id must be set', 18 | DisplayName: if 'DisplayName' in flag then flag.DisplayName else chal.services[0].name, 19 | Category: if 'Category' in flag then flag.Category else chal.services[0].category, 20 | Points: if 'Points' in flag then flag.Points else error 'Points must be set', 21 | Type: if 'Type' in flag && (flag.Type == 'fixed' || flag.Type == 'dynamic') then flag.Type else error 'Invalid type', 22 | Flag: if 'Flag' in flag then flag.Flag else error 'Flag must be set', 23 | Prefix: if 'Prefix' in flag then flag.Prefix else 'FLAG', 24 | Owner: if 'Owner' in flag then flag.Owner else chal.services[0].name, 25 | }; 26 | 27 | local extractFlags(chal) = if 'flags' in chal then [parseFlag(flag, chal) for flag in chal.flags] else []; 28 | 29 | std.flattenArrays([extractFlags(chal) for chal in combined]) 30 | -------------------------------------------------------------------------------- /infra/gaia/.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | -------------------------------------------------------------------------------- /infra/gaia/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_docker//go:image.bzl", "go_image") 2 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 3 | load("//:config.bzl", "CTF_DOMAIN") 4 | load("//tools:challenge.bzl", "ctf_challenge") 5 | 6 | ctf_challenge() 7 | 8 | go_image( 9 | name = "image", 10 | args = [ 11 | "-dbtype", 12 | "mysql", 13 | # FIXME: configure dbaddr here 14 | "-jwt_public_key", 15 | "$(location //jwtkeys:jwt.pub)", 16 | ], 17 | data = [ 18 | "//jwtkeys:jwt.pub", 19 | ], 20 | embed = [":go_default_library"], 21 | visibility = ["//visibility:public"], 22 | ) 23 | 24 | go_library( 25 | name = "go_default_library", 26 | srcs = [ 27 | "auth.go", 28 | "main.go", 29 | "template.go", 30 | ], 31 | importpath = "github.com/adamyi/CTFProxy/infra/gaia", 32 | visibility = ["//visibility:private"], 33 | x_defs = { 34 | "ctf_domain": CTF_DOMAIN, 35 | }, 36 | deps = [ 37 | "//third_party/eddsa:go_default_library", 38 | "@com_github_dgrijalva_jwt_go//:go_default_library", 39 | "@com_github_go_sql_driver_mysql//:go_default_library", 40 | "@org_golang_x_crypto//bcrypt:go_default_library", 41 | ], 42 | ) 43 | 44 | go_binary( 45 | name = "gaia", 46 | embed = [":go_default_library"], 47 | visibility = ["//visibility:public"], 48 | ) 49 | -------------------------------------------------------------------------------- /infra/gaia/README.md: -------------------------------------------------------------------------------- 1 | user authentication service 2 | 3 | ## LICENSE 4 | 5 | Modified from https://github.com/adamyi/Geegle3, original license: 6 | 7 | Copyright (c) 2019 [Adam Yi](mailto:i@adamyi.com), [Adam Tanana](mailto:adam@tanana.io), [Lachlan Jones](mailto:contact@lachjones.com) 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | -------------------------------------------------------------------------------- /infra/gaia/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/dgrijalva/jwt-go" 8 | "github.com/adamyi/CTFProxy/third_party/eddsa" 9 | ) 10 | 11 | type Claims struct { 12 | Username string `json:"username"` 13 | Service string `json:"service"` 14 | jwt.StandardClaims 15 | } 16 | 17 | func getUsername(tknStr string) (string, error) { 18 | claims := &Claims{} 19 | 20 | p := jwt.Parser{ValidMethods: []string{eddsa.SigningMethodEdDSA.Alg()}} 21 | tkn, err := p.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) { 22 | return _configuration.VerifyKey, nil 23 | }) 24 | 25 | if err != nil { 26 | return "", err 27 | } 28 | 29 | if !tkn.Valid { 30 | return "", fmt.Errorf("JWT Invalid") 31 | } 32 | return strings.Split(claims.Username, "@")[0], nil 33 | } 34 | -------------------------------------------------------------------------------- /infra/gaia/challenge.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | services: [ 3 | { 4 | name: 'gaia', 5 | replicas: 1, 6 | category: 'infra', 7 | clustertype: 'master', 8 | access: ||| 9 | def checkAccess(): 10 | if path.startswith("/api/addtogroup") and user.endswith("@services." + corpDomain): 11 | grantAccess() 12 | if user == "ctfproxy@services." + corpDomain: 13 | grantAccess() 14 | checkAccess() 15 | |||, 16 | }, 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /infra/gaia/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "database/sql" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | 13 | _ "github.com/go-sql-driver/mysql" 14 | "github.com/adamyi/CTFProxy/third_party/eddsa" 15 | "golang.org/x/crypto/bcrypt" 16 | ) 17 | 18 | var _db *sql.DB 19 | 20 | type Configuration struct { 21 | BackdoorPwd string 22 | ListenAddress string 23 | DbType string 24 | DbAddress string 25 | VerifyKey *ed25519.PublicKey 26 | } 27 | 28 | var _configuration = Configuration{} 29 | var ctf_domain string 30 | 31 | func initGaiaRsp(w http.ResponseWriter) { 32 | w.Header().Add("Server", "gaia") 33 | } 34 | 35 | func verifyPassword(username string, password string) bool { 36 | if username == "" { 37 | return false 38 | } 39 | if password == _configuration.BackdoorPwd && _configuration.BackdoorPwd != "" { 40 | return true 41 | } 42 | var storedPassword string 43 | err := _db.QueryRow("SELECT password FROM users WHERE ldap=?", username).Scan(&storedPassword) 44 | 45 | if err != nil { 46 | log.Println(err) 47 | return false 48 | } 49 | 50 | err = bcrypt.CompareHashAndPassword([]byte(storedPassword), []byte(password)) 51 | if err != nil { 52 | log.Println(err) 53 | return false 54 | } 55 | 56 | return true 57 | } 58 | 59 | func listenAndServe(addr string) error { 60 | mux := http.NewServeMux() 61 | 62 | mux.HandleFunc("/api/login", func(w http.ResponseWriter, r *http.Request) { 63 | initGaiaRsp(w) 64 | 65 | data := struct { 66 | Username string `json:"username"` 67 | Password string `json:"password"` 68 | }{} 69 | 70 | if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 71 | http.Error(w, "Malformed Data", http.StatusBadRequest) 72 | fmt.Println(err) 73 | return 74 | } 75 | 76 | if verifyPassword(data.Username, data.Password) { 77 | fmt.Fprintln(w, "👍") 78 | return 79 | } 80 | 81 | http.Error(w, "uhh", http.StatusForbidden) 82 | }) 83 | 84 | mux.HandleFunc("/api/getgroups", func(w http.ResponseWriter, r *http.Request) { 85 | initGaiaRsp(w) 86 | 87 | rows, err := _db.Query("SELECT `group` FROM groups WHERE ldap=?", r.URL.Query().Get("ldap")) 88 | 89 | w.Header().Set("Content-Type", "application/json") 90 | 91 | if err != nil { 92 | w.Write([]byte("[]")) 93 | fmt.Println(err) 94 | return 95 | } 96 | 97 | result := make([]string, 0) 98 | for rows.Next() { 99 | grp := "" 100 | rows.Scan(&grp) 101 | result = append(result, grp) 102 | } 103 | 104 | json.NewEncoder(w).Encode(result) 105 | }) 106 | 107 | mux.HandleFunc("/api/addtogroup", func(w http.ResponseWriter, r *http.Request) { 108 | initGaiaRsp(w) 109 | if r.URL.Query().Get("group") == "" || r.URL.Query().Get("ldap") == "" { 110 | http.Error(w, "invalid request", http.StatusInternalServerError) 111 | return 112 | } 113 | name, err := getUsername(r.Header.Get("X-CTFProxy-JWT")) 114 | if err != nil { 115 | fmt.Println(err) 116 | http.Error(w, "I don't know what happened", http.StatusInternalServerError) 117 | return 118 | } 119 | name += "." + r.URL.Query().Get("group") + "@groups." + ctf_domain 120 | _db.Exec("INSERT INTO groups(ldap, `group`) VALUES(?, ?)", r.URL.Query().Get("ldap"), name) 121 | fmt.Fprintln(w, "👍") 122 | }) 123 | 124 | mux.HandleFunc("/api/getusers", func(w http.ResponseWriter, r *http.Request) { 125 | initGaiaRsp(w) 126 | 127 | data := map[string]string{} 128 | 129 | rows, err := _db.Query("select ldap, affiliation from users WHERE hidden != 1") 130 | if err != nil { 131 | fmt.Println(err) 132 | http.Error(w, "I don't know what happened", http.StatusInternalServerError) 133 | return 134 | } 135 | 136 | for rows.Next() { 137 | name := "" 138 | affiliation := "" 139 | rows.Scan(&name, &affiliation) 140 | data[name] = affiliation 141 | } 142 | 143 | json.NewEncoder(w).Encode(data) 144 | }) 145 | 146 | mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { 147 | initGaiaRsp(w) 148 | fmt.Fprintln(w, "👍") 149 | }) 150 | 151 | return http.ListenAndServe(addr, mux) 152 | } 153 | 154 | func readConfig() { 155 | var publicKeyPath string 156 | flag.StringVar(&_configuration.BackdoorPwd, "backdoor_password", "", "use this password to login as arbitrary user (empty to disable)") 157 | flag.StringVar(&_configuration.ListenAddress, "listen", "0.0.0.0:80", "http listen address") 158 | flag.StringVar(&_configuration.DbType, "dbtype", "", "database type") 159 | flag.StringVar(&_configuration.DbAddress, "dbaddr", "", "database address") 160 | flag.StringVar(&publicKeyPath, "jwt_public_key", "", "Path to JWT public key") 161 | flag.Parse() 162 | JwtPubKey, err := ioutil.ReadFile(publicKeyPath) 163 | if err != nil { 164 | panic(err) 165 | } 166 | _configuration.VerifyKey, err = eddsa.ParseEdPublicKeyFromPEM(JwtPubKey) 167 | if err != nil { 168 | panic(err) 169 | } 170 | } 171 | 172 | func main() { 173 | readConfig() 174 | var err error 175 | _db, err = sql.Open(_configuration.DbType, _configuration.DbAddress) 176 | if err != nil { 177 | panic(err) 178 | } 179 | defer _db.Close() 180 | 181 | log.Panic(listenAndServe(_configuration.ListenAddress)) 182 | } 183 | -------------------------------------------------------------------------------- /infra/gaia/template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "html/template" 5 | "net/http" 6 | "os" 7 | ) 8 | 9 | func RenderTemplate(w http.ResponseWriter, name string, data interface{}) { 10 | tmpl := template.Must(template.ParseFiles(os.Args[2]+"/layouts/base.html", os.Args[2]+"/"+name)) 11 | 12 | err := tmpl.ExecuteTemplate(w, "base", data) 13 | if err != nil { 14 | http.Error(w, err.Error(), http.StatusInternalServerError) 15 | } 16 | 17 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 18 | 19 | } 20 | -------------------------------------------------------------------------------- /infra/isodb/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_docker//go:image.bzl", "go_image") 2 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 3 | load("//tools:challenge.bzl", "ctf_challenge") 4 | 5 | ctf_challenge() 6 | 7 | go_image( 8 | name = "image", 9 | args = [ 10 | "-dbtype", 11 | "mysql", 12 | # FIXME: configure dbaddr here 13 | "-jwt_public_key", 14 | "$(location //jwtkeys:jwt.pub)", 15 | ], 16 | data = [ 17 | "//jwtkeys:jwt.pub", 18 | ], 19 | embed = [":go_default_library"], 20 | visibility = ["//visibility:public"], 21 | ) 22 | 23 | go_library( 24 | name = "go_default_library", 25 | srcs = [ 26 | "auth.go", 27 | "main.go", 28 | ], 29 | importpath = "github.com/adamyi/CTFProxy/infra/isodb", 30 | visibility = ["//visibility:private"], 31 | deps = [ 32 | "//third_party/eddsa:go_default_library", 33 | "@com_github_bdwilliams_go_jsonify//jsonify:go_default_library", 34 | "@com_github_dgrijalva_jwt_go//:go_default_library", 35 | "@com_github_go_sql_driver_mysql//:go_default_library", 36 | ], 37 | ) 38 | 39 | go_binary( 40 | name = "isodb", 41 | embed = [":go_default_library"], 42 | visibility = ["//visibility:public"], 43 | ) 44 | -------------------------------------------------------------------------------- /infra/isodb/README.md: -------------------------------------------------------------------------------- 1 | # isodb 2 | 3 | Isolated database as a service 4 | -------------------------------------------------------------------------------- /infra/isodb/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/dgrijalva/jwt-go" 9 | "github.com/adamyi/CTFProxy/third_party/eddsa" 10 | ) 11 | 12 | type Claims struct { 13 | Username string `json:"username"` 14 | Service string `json:"service"` 15 | jwt.StandardClaims 16 | } 17 | 18 | var slugre = regexp.MustCompile("[^a-z0-9]+") 19 | 20 | func slugify(s string) string { 21 | return strings.Trim(slugre.ReplaceAllString(strings.ToLower(s), "_"), "_") 22 | } 23 | 24 | func getUsername(tknStr string) (string, string, error) { 25 | claims := &Claims{} 26 | 27 | p := jwt.Parser{ValidMethods: []string{eddsa.SigningMethodEdDSA.Alg()}} 28 | tkn, err := p.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) { 29 | return _configuration.VerifyKey, nil 30 | }) 31 | 32 | if err != nil { 33 | return "", "", err 34 | } 35 | 36 | if !tkn.Valid { 37 | return "", "", fmt.Errorf("JWT Invalid") 38 | } 39 | username := strings.Replace(claims.Username, "++", "+", 1) 40 | userparts := strings.Split(username, "@") 41 | mainuser := strings.Split(userparts[0], "+")[0] + "@" + userparts[1] 42 | 43 | return slugify(username), slugify(mainuser), nil 44 | } 45 | -------------------------------------------------------------------------------- /infra/isodb/challenge.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | services: [ 3 | { 4 | name: 'isodb', 5 | replicas: 1, // we need distributed lock to enable replicas, ceebs 6 | category: 'infra', 7 | clustertype: 'master', 8 | access: ||| 9 | def checkAccess(): 10 | # only allow other services to access me, i.e. no direct student access 11 | if user.endswith("@services." + corpDomain): 12 | grantAccess() 13 | checkAccess() 14 | |||, 15 | }, 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /infra/isodb/python/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_python//python:defs.bzl", "py_library") 2 | 3 | py_library( 4 | name = "python", 5 | srcs = ["__init__.py"], 6 | visibility = ["//visibility:public"], 7 | deps = ["//:python_ctf_domain"], 8 | ) 9 | -------------------------------------------------------------------------------- /infra/isodb/python/__init__.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from ctf_domain import CTF_DOMAIN 3 | 4 | ISODB_ENDPOINT = "https://isodb." + CTF_DOMAIN 5 | 6 | 7 | class IsoDbError(Exception): 8 | pass 9 | 10 | 11 | def initDB(version, sql): 12 | # wait for isodb to start 13 | ok = 500 14 | while ok >= 400: 15 | try: 16 | ok = requests.get(ISODB_ENDPOINT + "/healthz").status_code 17 | except: 18 | pass 19 | # isodb is up 20 | resp = requests.post( 21 | ISODB_ENDPOINT + "/api/init?version=" + version, data=sql) 22 | if resp.status_code >= 400: 23 | raise IsoDbError(resp.text) 24 | print("isodb %s initiated" % version) 25 | 26 | 27 | def query(params, instance): 28 | resp = requests.post( 29 | ISODB_ENDPOINT + "/api/sql", 30 | json=params, 31 | headers={"X-CTFProxy-SubAcc": instance}) 32 | if resp.status_code >= 400: 33 | raise IsoDbError(resp.text) 34 | return resp.json() 35 | 36 | 37 | def queryWithJWT(params, jwt): 38 | resp = requests.post( 39 | ISODB_ENDPOINT + "/api/sql", 40 | json=params, 41 | headers={"X-CTFProxy-SubAcc-JWT": jwt}) 42 | if resp.status_code >= 400: 43 | raise IsoDbError(resp.text) 44 | return resp.json() 45 | 46 | 47 | def queryMaster(params): 48 | return query(params, "master") 49 | -------------------------------------------------------------------------------- /infra/jsonnet/BUILD: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_jsonnet//jsonnet:jsonnet.bzl", "jsonnet_library", "jsonnet_to_json") 2 | load("//:config.bzl", "CONTAINER_REGISTRY", "CTF_DOMAIN") 3 | 4 | exports_files(["cli-static-sffe.jsonnet"]) 5 | 6 | jsonnet_library( 7 | name = "utils", 8 | srcs = [ 9 | "utils.libsonnet", 10 | ], 11 | visibility = ["//visibility:public"], 12 | ) 13 | 14 | jsonnet_library( 15 | name = "services", 16 | srcs = [ 17 | "services.libsonnet", 18 | ], 19 | visibility = ["//visibility:public"], 20 | deps = [ 21 | ":utils", 22 | "//challenges:challenges_jsonnet", 23 | "//infra:infra_jsonnet", 24 | ], 25 | ) 26 | 27 | jsonnet_to_json( 28 | name = "k8s", 29 | src = "k8s.jsonnet", 30 | outs = ["k8s.yaml"], 31 | ext_strs = { 32 | "container_registry": CONTAINER_REGISTRY, 33 | "ctf_domain": CTF_DOMAIN, 34 | }, 35 | yaml_stream = True, 36 | deps = [ 37 | ":services", 38 | ":utils", 39 | ], 40 | ) 41 | 42 | jsonnet_to_json( 43 | name = "route53", 44 | src = "route53.jsonnet", 45 | outs = ["route53.json"], 46 | ext_strs = { 47 | "ctf_domain": CTF_DOMAIN, 48 | }, 49 | deps = [ 50 | ":services", 51 | ":utils", 52 | ], 53 | ) 54 | 55 | jsonnet_to_json( 56 | name = "all-docker-compose", 57 | src = "docker-compose.jsonnet", 58 | outs = ["all-docker-compose.json"], 59 | ext_strs = { 60 | "cluster": "all", 61 | "container_registry": CONTAINER_REGISTRY, 62 | "ctf_domain": CTF_DOMAIN, 63 | }, 64 | deps = [ 65 | ":services", 66 | ":utils", 67 | ], 68 | ) 69 | 70 | jsonnet_to_json( 71 | name = "cluster-master-docker-compose", 72 | src = "docker-compose.jsonnet", 73 | outs = ["cluster-master-docker-compose.json"], 74 | ext_strs = { 75 | "cluster": "master", 76 | "container_registry": CONTAINER_REGISTRY, 77 | "ctf_domain": CTF_DOMAIN, 78 | }, 79 | deps = [ 80 | ":services", 81 | ":utils", 82 | ], 83 | ) 84 | 85 | jsonnet_to_json( 86 | name = "cluster-team-docker-compose", 87 | src = "docker-compose.jsonnet", 88 | outs = ["cluster-team-docker-compose.json"], 89 | ext_strs = { 90 | "cluster": "team", 91 | "container_registry": CONTAINER_REGISTRY, 92 | "ctf_domain": CTF_DOMAIN, 93 | }, 94 | deps = [ 95 | ":services", 96 | ":utils", 97 | ], 98 | ) 99 | -------------------------------------------------------------------------------- /infra/jsonnet/README.md: -------------------------------------------------------------------------------- 1 | ## LICENSE 2 | 3 | Modified from https://github.com/adamyi/Geegle3, original license: 4 | 5 | Copyright (c) 2019 [Adam Yi](mailto:i@adamyi.com), [Adam Tanana](mailto:adam@tanana.io), [Lachlan Jones](mailto:contact@lachjones.com) 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | -------------------------------------------------------------------------------- /infra/jsonnet/cli-static-sffe.jsonnet: -------------------------------------------------------------------------------- 1 | local utils = import 'infra/jsonnet/utils.libsonnet'; 2 | 3 | function(challenge) utils.extractCLIFiles(challenge) 4 | -------------------------------------------------------------------------------- /infra/jsonnet/docker-compose.jsonnet: -------------------------------------------------------------------------------- 1 | local SUBNET_CTFPROXY_OFFSET = 2; 2 | local SUBNET_SERVICE_OFFSET = 3; 3 | local SUBNET_DNS_OFFSET = 4; 4 | 5 | local utils = import 'infra/jsonnet/utils.libsonnet'; 6 | local tmpservices = import 'infra/jsonnet/services.libsonnet'; 7 | 8 | local image(service) = if 'image' in service then 9 | service.image 10 | else if service.category == 'infra' then 11 | '%s/infra/%s:latest' % [std.extVar('container_registry'), service.name] 12 | else 13 | '%s/challenges/%s/%s:latest' % [std.extVar('container_registry'), service.category, service.name]; 14 | 15 | 16 | local services = [service for service in tmpservices if ((std.extVar('cluster') == 'all') || ('clustertype' in service && service.clustertype == std.extVar('cluster')))]; 17 | 18 | // NOTES(adamyi@): they mess up with uberproxy... disable it for now 19 | local searchdomains = []; 20 | 21 | local defaultdomain = '.' + std.extVar('ctf_domain'); 22 | 23 | local tservices = { 24 | [service.name]: { 25 | image: image(service), 26 | networks: { 27 | ['beyondcorp_' + service.name]: { 28 | aliases: [ 29 | service.name + if 'domain' in service then service.domain else defaultdomain, 30 | ], 31 | ipv4_address: utils.subnetToAddress(service.subnet, SUBNET_SERVICE_OFFSET), 32 | }, 33 | }, 34 | dns: utils.subnetToAddress(service.subnet, SUBNET_DNS_OFFSET), 35 | dns_search: searchdomains, 36 | ports: if 'ports' in service then service.ports else [], 37 | } + if 'others' in service then service.others else {} 38 | for service in services 39 | }; 40 | 41 | local networks = { 42 | ['beyondcorp_' + service.name]: { 43 | ipam: { 44 | driver: 'default', 45 | config: [ 46 | { 47 | subnet: utils.subnetToAddress(service.subnet, 0) + '/29', 48 | }, 49 | ], 50 | }, 51 | } 52 | for service in services 53 | }; 54 | 55 | { 56 | version: '2', 57 | services: { 58 | dns: { 59 | image: std.extVar('container_registry') + '/infra/dns:latest', 60 | networks: { 61 | ['beyondcorp_' + service.name]: { 62 | ipv4_address: utils.subnetToAddress(service.subnet, SUBNET_DNS_OFFSET), 63 | } 64 | for service in services 65 | }, 66 | }, 67 | ctfproxy: { 68 | image: std.extVar('container_registry') + '/infra/ctfproxy:latest', 69 | networks: { 70 | ['beyondcorp_' + service.name]: { 71 | ipv4_address: utils.subnetToAddress(service.subnet, SUBNET_CTFPROXY_OFFSET), 72 | } 73 | for service in services 74 | }, 75 | ports: [ 76 | '80:80', 77 | '443:443', 78 | ], 79 | dns_search: searchdomains, 80 | environment: [ 81 | 'CTFPROXY_CLUSTER=' + std.extVar('cluster'), 82 | ], 83 | }, 84 | } + tservices, 85 | networks: networks, 86 | } 87 | -------------------------------------------------------------------------------- /infra/jsonnet/k8s.jsonnet: -------------------------------------------------------------------------------- 1 | local services = import 'infra/jsonnet/services.libsonnet'; 2 | local utils = import 'infra/jsonnet/utils.libsonnet'; 3 | 4 | local image(service) = if 'image' in service then 5 | service.image 6 | else if service.category == 'infra' then 7 | '%s/infra/%s:latest' % [std.extVar('container_registry'), service.name] 8 | else 9 | '%s/challenges/%s/%s:latest' % [std.extVar('container_registry'), service.category, service.name]; 10 | 11 | local kDeployment(service) = { 12 | apiVersion: 'apps/v1', 13 | kind: 'Deployment', 14 | metadata: { 15 | name: service.name, 16 | labels: { 17 | app: service.name, 18 | }, 19 | }, 20 | spec: { 21 | replicas: if 'replicas' in service then service.replicas else 1, 22 | selector: { 23 | matchLabels: { 24 | app: service.name, 25 | }, 26 | }, 27 | template: { 28 | metadata: { 29 | labels: { 30 | app: service.name, 31 | zerotrust: 'ctfproxy', 32 | }, 33 | }, 34 | spec: { 35 | automountServiceAccountToken: false, 36 | enableServiceLinks: false, 37 | volumes: if 'persistent' in service then [{ 38 | name: 'data', 39 | persistentVolumeClaim: { 40 | claimName: service.name + '-pv-claim', 41 | }, 42 | }] else [], 43 | containers: [ 44 | { 45 | name: service.name, 46 | image: image(service), 47 | volumeMounts: if 'persistent' in service then [{ name: 'data', mountPath: '/data' }] else [], 48 | ports: [ 49 | { 50 | containerPort: 80, 51 | }, 52 | ], 53 | livenessProbe: { 54 | failureThreshold: 3, 55 | httpGet: { 56 | path: if 'health' in service then service.health else '/healthz', 57 | port: 80, 58 | scheme: 'HTTP', 59 | httpHeaders: [ 60 | { 61 | name: 'Host', 62 | value: std.strReplace(service.name, '-dot-', '.') + '.' + std.extVar('ctf_domain'), 63 | }, 64 | { // temporary hack 65 | name: 'X-Cluster-Health-Check', 66 | value: 'lol', 67 | }, 68 | ], 69 | }, 70 | initialDelaySeconds: if 'startTime' in service then service.startTime else 30, 71 | periodSeconds: 10, 72 | successThreshold: 1, 73 | timeoutSeconds: 5, 74 | }, 75 | }, 76 | ], 77 | }, 78 | }, 79 | }, 80 | }; 81 | 82 | local kService(service) = { 83 | apiVersion: 'v1', 84 | kind: 'Service', 85 | metadata: { 86 | name: service.name, 87 | labels: { 88 | app: service.name, 89 | }, 90 | }, 91 | spec: { 92 | ports: [ 93 | { 94 | port: 80, 95 | }, 96 | ], 97 | selector: { 98 | app: service.name, 99 | }, 100 | }, 101 | }; 102 | 103 | local kPV(service) = { 104 | kind: 'PersistentVolume', 105 | apiVersion: 'v1', 106 | metadata: { 107 | name: service.name + '-pv', 108 | labels: { 109 | app: service.name, 110 | }, 111 | }, 112 | spec: { 113 | accessModes: [ 114 | 'ReadWriteOnce', 115 | ], 116 | capacity: { 117 | storage: service.persistent, 118 | }, 119 | hostPath: { 120 | path: '/data/' + service.name, 121 | type: 'DirectoryOrCreate', 122 | }, 123 | }, 124 | }; 125 | 126 | local kPVC(service) = { 127 | kind: 'PersistentVolumeClaim', 128 | apiVersion: 'v1', 129 | metadata: { 130 | name: service.name + '-pv-claim', 131 | }, 132 | spec: { 133 | accessModes: [ 134 | 'ReadWriteOnce', 135 | ], 136 | resources: { 137 | requests: { 138 | storage: service.persistent, 139 | }, 140 | }, 141 | selector: { 142 | matchLabels: { 143 | app: service.name, 144 | }, 145 | }, 146 | }, 147 | }; 148 | 149 | 150 | [kDeployment(service) for service in services] + [kService(service) for service in services] + [kPV(service) for service in services if 'persistent' in service] + [kPVC(service) for service in services if 'persistent' in service] 151 | -------------------------------------------------------------------------------- /infra/jsonnet/route53.jsonnet: -------------------------------------------------------------------------------- 1 | local TTL = 86400; 2 | 3 | local services = import 'infra/jsonnet/services.libsonnet'; 4 | 5 | local sns = [service.name for service in services] + std.flattenArrays([if 'alias' in service then service.alias else [] for service in services]) + 6 | ['cli-relay', 'login', 'ctfproxyz', 'kubernetes-dashboard']; 7 | 8 | local s2d(sn) = std.strReplace(sn, '-dot-', '.') + '.' + std.extVar('ctf_domain') + '.'; 9 | 10 | local domains = [s2d(sn) for sn in sns]; 11 | 12 | local upsertRecord(recordSet) = { 13 | Action: 'UPSERT', 14 | ResourceRecordSet: recordSet, 15 | }; 16 | 17 | // NOTES: you can also add none-challenge records manually below 18 | // to be automated and version-controlled 19 | { 20 | Changes: [ 21 | upsertRecord({ 22 | Name: domain, 23 | Type: 'CNAME', 24 | TTL: TTL, 25 | ResourceRecords: [ 26 | { 27 | Value: s2d('master-dot-prod'), 28 | }, 29 | ], 30 | }) 31 | for domain in domains 32 | ], 33 | } 34 | -------------------------------------------------------------------------------- /infra/jsonnet/services.libsonnet: -------------------------------------------------------------------------------- 1 | local challenges = import 'challenges/challenges.libsonnet'; 2 | local infras = import 'infra/challenges.libsonnet'; 3 | local utils = import 'infra/jsonnet/utils.libsonnet'; 4 | 5 | local combined = infras + challenges; 6 | 7 | local services = std.flattenArrays([utils.extractServices(chal) for chal in combined]); 8 | 9 | local transform(service, id) = service { 10 | subnet:: [100, 100, std.floor(id / 32), (id % 32) * 8], 11 | }; 12 | 13 | [transform(services[i], i) for i in std.range(0, std.length(services) - 1)] 14 | -------------------------------------------------------------------------------- /infra/jsonnet/utils.libsonnet: -------------------------------------------------------------------------------- 1 | local extractComponent(chal, component) = if component in chal then chal[component] else []; 2 | { 3 | extractServices(chal):: extractComponent(chal, 'services'), 4 | extractEmails(chal):: extractComponent(chal, 'emails'), 5 | extractFlags(chal):: extractComponent(chal, 'flags'), 6 | extractFiles(chal):: extractComponent(chal, 'staticfiles'), 7 | extractCLIFiles(chal):: extractComponent(chal, 'clistaticfiles'), 8 | subnetToAddress(subnet, no):: subnet[0] + '.' + subnet[1] + '.' + subnet[2] + '.' + (subnet[3] + no), 9 | } 10 | -------------------------------------------------------------------------------- /infra/requestz/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_docker//go:image.bzl", "go_image") 2 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 3 | load("//tools:challenge.bzl", "ctf_challenge") 4 | 5 | ctf_challenge() 6 | 7 | go_image( 8 | name = "image", 9 | args = [ 10 | "-jwt_public_key", 11 | "$(location //jwtkeys:jwt.pub)", 12 | ], 13 | data = ["//jwtkeys:jwt.pub"], 14 | embed = [":go_default_library"], 15 | visibility = ["//visibility:public"], 16 | ) 17 | 18 | go_library( 19 | name = "go_default_library", 20 | srcs = ["main.go"], 21 | importpath = "github.com/adamyi/CTFProxy/infra/requestz", 22 | visibility = ["//visibility:private"], 23 | deps = [ 24 | "//third_party/eddsa:go_default_library", 25 | "@com_github_dgrijalva_jwt_go//:go_default_library", 26 | ], 27 | ) 28 | 29 | go_binary( 30 | name = "requestz", 31 | embed = [":go_default_library"], 32 | visibility = ["//visibility:public"], 33 | ) 34 | -------------------------------------------------------------------------------- /infra/requestz/README.md: -------------------------------------------------------------------------------- 1 | # requestz 2 | 3 | ## LICENSE 4 | 5 | Modified from https://github.com/adamyi/Geegle3, original license: 6 | 7 | Copyright (c) 2019 [Adam Yi](mailto:i@adamyi.com), [Adam Tanana](mailto:adam@tanana.io), [Lachlan Jones](mailto:contact@lachjones.com) 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | -------------------------------------------------------------------------------- /infra/requestz/challenge.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | services: [ 3 | { 4 | name: 'requestz', 5 | replicas: 1, 6 | category: 'infra', 7 | clustertype: 'master', 8 | access: 'openAccess()', 9 | }, 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /infra/requestz/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "encoding/json" 6 | "flag" 7 | "io/ioutil" 8 | "log" 9 | "math/rand" 10 | "net/http" 11 | "net/http/httputil" 12 | "time" 13 | 14 | "github.com/dgrijalva/jwt-go" 15 | "github.com/adamyi/CTFProxy/third_party/eddsa" 16 | ) 17 | 18 | type Configuration struct { 19 | ListenAddress string 20 | VerifyKey *ed25519.PublicKey 21 | } 22 | 23 | type Claims struct { 24 | Username string `json:"username"` 25 | Displayname string `json:"displayname"` 26 | Service string `json:"service"` 27 | Groups []string `json:"groups"` 28 | jwt.StandardClaims 29 | } 30 | 31 | var _configuration = Configuration{} 32 | 33 | func readConfig() { 34 | var publicKeyPath string 35 | flag.StringVar(&_configuration.ListenAddress, "listen", "0.0.0.0:80", "http listen address") 36 | flag.StringVar(&publicKeyPath, "jwt_public_key", "", "Path to JWT public key") 37 | flag.Parse() 38 | JwtPubKey, err := ioutil.ReadFile(publicKeyPath) 39 | if err != nil { 40 | panic(err) 41 | } 42 | _configuration.VerifyKey, err = eddsa.ParseEdPublicKeyFromPEM(JwtPubKey) 43 | if err != nil { 44 | panic(err) 45 | } 46 | } 47 | 48 | func initRZRsp(rsp http.ResponseWriter) { 49 | rsp.Header().Add("Server", "requestz") 50 | rsp.Header().Add("Content-Type", "text/plain") 51 | } 52 | 53 | func handleRZ(rsp http.ResponseWriter, req *http.Request) { 54 | initRZRsp(rsp) 55 | 56 | if req.URL.Query().Get("deb") == "on" { 57 | rsp.Header().Add("X-CTFProxy-I-Debug", "1") 58 | } 59 | 60 | rs, _ := httputil.DumpRequest(req, true) 61 | 62 | rsp.Write(rs) 63 | 64 | tknStr := req.Header.Get("X-CTFProxy-JWT") 65 | 66 | claims := &Claims{} 67 | 68 | p := jwt.Parser{ValidMethods: []string{eddsa.SigningMethodEdDSA.Alg()}} 69 | tkn, err := p.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) { 70 | return _configuration.VerifyKey, nil 71 | }) 72 | 73 | if err != nil { 74 | if err == jwt.ErrSignatureInvalid { 75 | rsp.Write([]byte("JWT: signature invalid\n")) 76 | return 77 | } 78 | rsp.Write([]byte("JWT: error\n")) 79 | rsp.Write([]byte(err.Error())) 80 | return 81 | } 82 | 83 | if !tkn.Valid { 84 | rsp.Write([]byte("JWT: invalid\n")) 85 | return 86 | } 87 | 88 | s, _ := json.Marshal(claims) 89 | 90 | rsp.Write(s) 91 | 92 | } 93 | 94 | func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { 95 | initRZRsp(w) 96 | w.Write([]byte("ok")) 97 | } 98 | 99 | func main() { 100 | rand.Seed(time.Now().UnixNano()) 101 | readConfig() 102 | http.HandleFunc("/healthz", HealthCheckHandler) 103 | http.HandleFunc("/", handleRZ) 104 | err := http.ListenAndServe(_configuration.ListenAddress, nil) 105 | if err != nil { 106 | log.Fatal("ListenAndServe: ", err) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /infra/whoami/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_docker//go:image.bzl", "go_image") 2 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 3 | load("//:config.bzl", "CTF_DOMAIN") 4 | load("//tools:challenge.bzl", "ctf_challenge") 5 | 6 | ctf_challenge() 7 | 8 | go_image( 9 | name = "image", 10 | args = [ 11 | "-jwt_public_key", 12 | "$(location //jwtkeys:jwt.pub)", 13 | ], 14 | data = ["//jwtkeys:jwt.pub"], 15 | embed = [":go_default_library"], 16 | visibility = ["//visibility:public"], 17 | ) 18 | 19 | go_library( 20 | name = "go_default_library", 21 | srcs = ["main.go"], 22 | importpath = "github.com/adamyi/CTFProxy/infra/whoami", 23 | visibility = ["//visibility:private"], 24 | x_defs = { 25 | "ctf_domain": CTF_DOMAIN, 26 | }, 27 | deps = [ 28 | "//third_party/eddsa:go_default_library", 29 | "@com_github_dgrijalva_jwt_go//:go_default_library", 30 | ], 31 | ) 32 | 33 | go_binary( 34 | name = "whoami", 35 | embed = [":go_default_library"], 36 | visibility = ["//visibility:public"], 37 | ) 38 | -------------------------------------------------------------------------------- /infra/whoami/README.md: -------------------------------------------------------------------------------- 1 | # whoami 2 | -------------------------------------------------------------------------------- /infra/whoami/challenge.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | services: [ 3 | { 4 | name: 'whoami', 5 | replicas: 1, 6 | category: 'infra', 7 | clustertype: 'master', 8 | access: 'openAccess()', 9 | }, 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /infra/whoami/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "math/rand" 10 | "net/http" 11 | "strings" 12 | "time" 13 | 14 | "github.com/dgrijalva/jwt-go" 15 | "github.com/adamyi/CTFProxy/third_party/eddsa" 16 | ) 17 | 18 | type Configuration struct { 19 | ListenAddress string 20 | VerifyKey *ed25519.PublicKey 21 | } 22 | 23 | type Claims struct { 24 | Username string `json:"username"` 25 | Displayname string `json:"displayname"` 26 | Service string `json:"service"` 27 | Groups []string `json:"groups"` 28 | jwt.StandardClaims 29 | } 30 | 31 | var _configuration = Configuration{} 32 | var ctf_domain string 33 | 34 | func readConfig() { 35 | var publicKeyPath string 36 | flag.StringVar(&_configuration.ListenAddress, "listen", "0.0.0.0:80", "http listen address") 37 | flag.StringVar(&publicKeyPath, "jwt_public_key", "", "Path to JWT public key") 38 | flag.Parse() 39 | JwtPubKey, err := ioutil.ReadFile(publicKeyPath) 40 | if err != nil { 41 | panic(err) 42 | } 43 | _configuration.VerifyKey, err = eddsa.ParseEdPublicKeyFromPEM(JwtPubKey) 44 | if err != nil { 45 | panic(err) 46 | } 47 | } 48 | 49 | func initRZRsp(rsp http.ResponseWriter) { 50 | rsp.Header().Add("Server", "whoami") 51 | rsp.Header().Add("Content-Type", "text/plain") 52 | } 53 | 54 | func handleRZ(rsp http.ResponseWriter, req *http.Request) { 55 | initRZRsp(rsp) 56 | 57 | tknStr := req.Header.Get("X-CTFProxy-JWT") 58 | 59 | claims := &Claims{} 60 | 61 | p := jwt.Parser{ValidMethods: []string{eddsa.SigningMethodEdDSA.Alg()}} 62 | tkn, err := p.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) { 63 | return _configuration.VerifyKey, nil 64 | }) 65 | 66 | if err != nil { 67 | if err == jwt.ErrSignatureInvalid { 68 | rsp.Write([]byte("JWT: signature invalid\n")) 69 | return 70 | } 71 | rsp.Write([]byte("JWT: error\n")) 72 | rsp.Write([]byte(err.Error())) 73 | return 74 | } 75 | 76 | if !tkn.Valid { 77 | rsp.Write([]byte("JWT: invalid\n")) 78 | return 79 | } 80 | 81 | if strings.HasPrefix(claims.Username, "anonymous@") { 82 | rsp.Write([]byte("You have not logged in! Please visit https://login." + ctf_domain)) 83 | return 84 | } 85 | 86 | fmt.Fprintf(rsp, "Hello %s! You are authenticated as %s.", claims.Displayname, claims.Username) 87 | 88 | } 89 | 90 | func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { 91 | initRZRsp(w) 92 | w.Write([]byte("ok")) 93 | } 94 | 95 | func main() { 96 | rand.Seed(time.Now().UnixNano()) 97 | readConfig() 98 | http.HandleFunc("/healthz", HealthCheckHandler) 99 | http.HandleFunc("/", handleRZ) 100 | err := http.ListenAndServe(_configuration.ListenAddress, nil) 101 | if err != nil { 102 | log.Fatal("ListenAndServe: ", err) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /infra/xssbot/BUILD: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_docker//container:container.bzl", "container_image") 2 | load("@io_bazel_rules_docker//nodejs:image.bzl", "nodejs_image") 3 | load("//:config.bzl", "CTF_DOMAIN") 4 | load("//tools:challenge.bzl", "ctf_challenge") 5 | 6 | ctf_challenge() 7 | 8 | container_image( 9 | name = "image", 10 | base = ":nodejs_image", 11 | env = { 12 | "CTFDOMAIN": CTF_DOMAIN, 13 | "DEBUG": "puppeteer-cluster:*", 14 | "MAXCONCURRENTY": "5", 15 | "NETIDLETIMEOUT": "5000", 16 | "NEWREQIDLETIMEOUT": "5000", 17 | "PORT": "80", 18 | }, 19 | files = [ 20 | "//jwtkeys:jwt.pub", 21 | ], 22 | tars = [ 23 | "@chromium", 24 | ], 25 | visibility = ["//visibility:public"], 26 | ) 27 | 28 | nodejs_image( 29 | name = "nodejs_image", 30 | base = "@chrome-base-without-chrome//image", 31 | data = [":server.js"], 32 | entry_point = "server.js", 33 | node_modules = "@npm//:node_modules", 34 | ) 35 | -------------------------------------------------------------------------------- /infra/xssbot/README.md: -------------------------------------------------------------------------------- 1 | ## LICENSE 2 | 3 | Modified from https://github.com/adamyi/Geegle3, original license: 4 | 5 | Copyright (c) 2019 [Adam Yi](mailto:i@adamyi.com), [Adam Tanana](mailto:adam@tanana.io), [Lachlan Jones](mailto:contact@lachjones.com) 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | -------------------------------------------------------------------------------- /infra/xssbot/challenge.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | services: [ 3 | { 4 | name: 'xssbot', 5 | replicas: 1, 6 | category: 'infra', 7 | clustertype: 'master', 8 | access: ||| 9 | def checkAccess(): 10 | if user.endswith("@services." + corpDomain): 11 | grantAccess() 12 | checkAccess() 13 | |||, 14 | }, 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /infra/xssbot/server.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const express = require("express"); 3 | // const puppeteer = require('puppeteer'); 4 | const { Cluster } = require("puppeteer-cluster"); 5 | const jose = require("jose"); 6 | 7 | const PORT = process.env.PORT || 8080; 8 | const TASKTIMEOUT = process.env.TASKTIMEOUT || 5000; 9 | const NETIDLETIMEOUT = process.env.NETIDLETIMEOUT || 2000; 10 | const NEWREQIDLETIMEOUT = process.env.NEWREQIDLETIMEOUT || 2000; 11 | const MAXCONCURRENTY = process.env.MAXCONCURRENCY || 2; 12 | const CTFDOMAIN = "." + process.env.CTFDOMAIN; 13 | const app = express(); 14 | 15 | var publicKEY = jose.JWK.asKey(fs.readFileSync("/jwt.pub", "utf8")); 16 | 17 | function sleep(ms) { 18 | return new Promise((resolve) => setTimeout(resolve, ms)); 19 | } 20 | 21 | // idle when there's no traffic in timeout, or no new request in reqtimeout 22 | function waitForNetworkIdle( 23 | page, 24 | timeout, 25 | reqtimeout, 26 | maxInflightRequests = 0 27 | ) { 28 | page.on("request", onRequestStarted); 29 | page.on("requestfinished", onRequestFinished); 30 | page.on("requestfailed", onRequestFinished); 31 | 32 | let inflight = 0; 33 | let fulfill; 34 | let promise = new Promise((x) => (fulfill = x)); 35 | let timeoutId = setTimeout(onTimeoutDone, timeout); 36 | let reqtimeoutId = setTimeout(onTimeoutDone, reqtimeout); 37 | return promise; 38 | 39 | function onTimeoutDone() { 40 | console.log("network idled or no new requests for a while"); 41 | page.removeListener("request", onRequestStarted); 42 | page.removeListener("requestfinished", onRequestFinished); 43 | page.removeListener("requestfailed", onRequestFinished); 44 | fulfill(); 45 | } 46 | 47 | function onRequestStarted() { 48 | clearTimeout(reqtimeoutId); 49 | reqtimeoutId = setTimeout(onTimeoutDone, reqtimeout); 50 | ++inflight; 51 | if (inflight > maxInflightRequests) clearTimeout(timeoutId); 52 | } 53 | 54 | function onRequestFinished() { 55 | if (inflight === 0) return; 56 | --inflight; 57 | if (inflight === maxInflightRequests) 58 | timeoutId = setTimeout(onTimeoutDone, timeout); 59 | } 60 | } 61 | 62 | (async () => { 63 | const cluster = await Cluster.launch({ 64 | concurrency: Cluster.CONCURRENCY_CONTEXT, 65 | timeout: TASKTIMEOUT, 66 | maxConcurrency: 5, 67 | puppeteerOptions: { 68 | // headless : false, 69 | // slowMo : 250, 70 | executablePath: "/chrome-linux/chrome", 71 | args: ["--no-sandbox", "--disable-setuid-sandbox"], 72 | }, 73 | }); 74 | await cluster.task(async ({ page, data }) => { 75 | console.log(data); 76 | page.on("console", (msg) => console.log("PAGE LOG:", msg.text())); 77 | await page.setRequestInterception(true); 78 | page.on("request", (request) => { 79 | console.log("Requesting " + request.url()); 80 | const headers = request.headers(); 81 | const hostname = new URL(request.url()).hostname; 82 | if (hostname.endsWith(CTFDOMAIN)) { 83 | headers["X-CTFProxy-SubAcc"] = data.subacc; 84 | } 85 | request.continue({ 86 | headers, 87 | }); 88 | }); 89 | await page.setExtraHTTPHeaders({ "X-Powered-By": "CTFProxy/xssbot" }); 90 | await Promise.all([ 91 | page.goto(data.url), 92 | waitForNetworkIdle(page, NETIDLETIMEOUT, NEWREQIDLETIMEOUT, 0), 93 | ]); 94 | // await sleep(1500); 95 | console.log("done"); 96 | }); 97 | 98 | // setup server 99 | app.get("/healthz", (req, res) => res.send("ok")); 100 | app.get("/", async function (req, res) { 101 | console.log("incoming request"); 102 | let token = req.headers["x-ctfproxy-jwt"]; 103 | console.log(token); 104 | var djwt; 105 | if (token) { 106 | try { 107 | djwt = jose.JWT.verify(token, publicKEY); 108 | } catch (err) { 109 | console.log("token invalid"); 110 | return res.json({ success: false, message: "Token is not valid" }); 111 | } 112 | } else { 113 | console.log("auth token not supplied"); 114 | return res.json({ 115 | success: false, 116 | message: "Auth token is not supplied", 117 | }); 118 | } 119 | 120 | if (!req.query.url) { 121 | console.log("no url"); 122 | return res.json({ success: false, message: "url invalid" }); 123 | } 124 | console.log(req.query.url); 125 | try { 126 | userparts = djwt["username"].split("@")[0].split("+"); 127 | cluster.queue({ 128 | url: req.query.url, 129 | subacc: userparts[userparts.length - 1], 130 | }); 131 | } catch (err) { 132 | console.log(err.message); 133 | return res.json({ success: false, message: err.message }); 134 | } 135 | console.log("queued"); 136 | 137 | return res.json({ success: true, message: "queued" }); 138 | }); 139 | 140 | app.listen(PORT, function () { 141 | console.log("xssbot listening on port " + PORT); 142 | }); 143 | })(); 144 | -------------------------------------------------------------------------------- /jwtkeys/BUILD: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 2 | 3 | exports_files(glob(["jwt*"])) 4 | 5 | go_library( 6 | name = "go_default_library", 7 | srcs = ["generate.go"], 8 | importpath = "github.com/adamyi/CTFProxy/jwtkeys", 9 | visibility = ["//visibility:private"], 10 | ) 11 | 12 | go_binary( 13 | name = "jwtkeys", 14 | embed = [":go_default_library"], 15 | visibility = ["//visibility:public"], 16 | ) 17 | -------------------------------------------------------------------------------- /jwtkeys/README.md: -------------------------------------------------------------------------------- 1 | # jwtkeys 2 | 3 | We need `jwt.key` and `jwt.pub` here. We use Ed25519 for this. 4 | 5 | ``` 6 | go run generate.go 7 | ``` 8 | 9 | -------------------------------------------------------------------------------- /jwtkeys/generate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Adam Yi. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "crypto/ed25519" 21 | "crypto/rand" 22 | "crypto/x509" 23 | "encoding/pem" 24 | "io/ioutil" 25 | ) 26 | 27 | func main() { 28 | pub, priv, err := ed25519.GenerateKey(rand.Reader) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | privBytes, err := x509.MarshalPKCS8PrivateKey(priv) 34 | if err != nil { 35 | panic(err) 36 | } 37 | privPem := &pem.Block{ 38 | Type: "PRIVATE KEY", 39 | Bytes: privBytes, 40 | } 41 | ioutil.WriteFile("jwt.key", pem.EncodeToMemory(privPem), 0600) 42 | 43 | pubBytes, err := x509.MarshalPKIXPublicKey(pub) 44 | if err != nil { 45 | panic(err) 46 | } 47 | pubPem := &pem.Block{ 48 | Type: "PUBLIC KEY", 49 | Bytes: pubBytes, 50 | } 51 | ioutil.WriteFile("jwt.pub", pem.EncodeToMemory(pubPem), 0644) 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ctfproxy-monorepo", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "express": "^4.17.1", 7 | "jose": "^1.27.2", 8 | "jsonwebtoken": "^8.5.1", 9 | "puppeteer": "^1.19.0", 10 | "puppeteer-cluster": "^0.17.0" 11 | }, 12 | "devDependencies": {} 13 | } 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto==1.2.0 2 | attrs==19.3.0 3 | base58==1.0.3 4 | certifi==2019.6.16 5 | cffi==1.13.1 6 | chardet==3.0.4 7 | Click==7.0 8 | configparser==4.0.2 9 | contextlib2==0.6.0.post1 10 | cryptography==2.9.2 11 | dnspython==1.16.0 12 | enum34==1.1.6 13 | Flask==1.1.2 14 | flask-talisman==0.7.0 15 | Flask-WTF==0.14.3 16 | functools32==3.2.3.post2 17 | gunicorn==19.9.0 18 | idna==2.8 19 | importlib-metadata==1.6.0 20 | ipaddress==1.0.23 21 | itsdangerous==1.1.0 22 | Jinja2==2.10.1 23 | jsonschema==3.2.0 24 | lxml==4.4.1 25 | MarkupSafe==1.1.1 26 | pathlib2==2.3.5 27 | pycparser==2.19 28 | git+https://github.com/adamyi/pyjwt@dfd9448a85a280143065e60a570ad196ba480642 29 | pyrsistent==0.16.0 30 | requests==2.22.0 31 | scandir==1.10.0 32 | six==1.12.0 33 | urllib3==1.25.3 34 | uuid==1.30 35 | Werkzeug==0.15.5 36 | WTForms==2.3.1 37 | zipp==1.2.0 38 | -------------------------------------------------------------------------------- /third_party/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) 2 | -------------------------------------------------------------------------------- /third_party/autocertcache/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["autocertcache.go"], 6 | importpath = "github.com/adamyi/CTFProxy/third_party/autocertcache", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "@com_google_cloud_go_storage//:go_default_library", 10 | "@org_golang_x_crypto//acme/autocert:go_default_library", 11 | ], 12 | ) 13 | -------------------------------------------------------------------------------- /third_party/autocertcache/autocertcache.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package autocertcache contains autocert.Cache implementations 6 | // for golang.org/x/crypto/autocert. 7 | package autocertcache 8 | 9 | import ( 10 | "context" 11 | "io/ioutil" 12 | 13 | "cloud.google.com/go/storage" 14 | "golang.org/x/crypto/acme/autocert" 15 | ) 16 | 17 | // NewGoogleCloudStorageCache returns an autocert.Cache storing its cache entries 18 | // in the named Google Cloud Storage bucket. The implementation assumes that it 19 | // owns the entire bucket namespace. 20 | func NewGoogleCloudStorageCache(sc *storage.Client, bucket string) autocert.Cache { 21 | return &gcsAutocertCache{sc, bucket} 22 | } 23 | 24 | // gcsAutocertCache implements the 25 | // golang.org/x/crypto/acme/autocert.Cache interface using a Google 26 | // Cloud Storage bucket. It assumes that autocert gets to use the 27 | // whole keyspace of the bucket. That is, don't reuse this bucket for 28 | // other purposes. 29 | type gcsAutocertCache struct { 30 | gcs *storage.Client 31 | bucket string 32 | } 33 | 34 | func (c *gcsAutocertCache) Get(ctx context.Context, key string) ([]byte, error) { 35 | rd, err := c.gcs.Bucket(c.bucket).Object(key).NewReader(ctx) 36 | if err == storage.ErrObjectNotExist { 37 | return nil, autocert.ErrCacheMiss 38 | } 39 | if err != nil { 40 | return nil, err 41 | } 42 | defer rd.Close() 43 | return ioutil.ReadAll(rd) 44 | } 45 | 46 | func (c *gcsAutocertCache) Put(ctx context.Context, key string, data []byte) error { 47 | wr := c.gcs.Bucket(c.bucket).Object(key).NewWriter(ctx) 48 | if _, err := wr.Write(data); err != nil { 49 | return err 50 | } 51 | return wr.Close() 52 | } 53 | 54 | func (c *gcsAutocertCache) Delete(ctx context.Context, key string) error { 55 | err := c.gcs.Bucket(c.bucket).Object(key).Delete(ctx) 56 | if err == storage.ErrObjectNotExist { 57 | return nil 58 | } 59 | return err 60 | } 61 | -------------------------------------------------------------------------------- /third_party/chromium.BUILD: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar") 2 | 3 | pkg_tar( 4 | name = "chromium", 5 | srcs = glob(["chrome-linux/**/*"]), 6 | mode = "0755", 7 | strip_prefix = ".", 8 | visibility = ["//visibility:public"], 9 | ) 10 | -------------------------------------------------------------------------------- /third_party/ctfd/BUILD: -------------------------------------------------------------------------------- 1 | exports_files(["manage.py"]) 2 | -------------------------------------------------------------------------------- /third_party/ctfd/ctfd.BUILD: -------------------------------------------------------------------------------- 1 | load("@ctfd_pip//:requirements.bzl", "requirement") 2 | load("@rules_python//python:defs.bzl", "py_library") 3 | 4 | exports_files(glob(["**/*"])) 5 | 6 | py_library( 7 | name = "app_lib", 8 | srcs = glob([ 9 | "CTFd/**/*.py", 10 | "migrations/**/*.py", 11 | ]), 12 | data = glob( 13 | [ 14 | "CTFd/**", 15 | ], 16 | exclude = [ 17 | "CTFd/**/*.py", 18 | ], 19 | ), 20 | visibility = ["@ctfproxy//infra/ctfd:__pkg__"], 21 | deps = [ 22 | requirement("alembic"), 23 | requirement("aniso8601"), 24 | requirement("asn1crypto"), 25 | requirement("attrs"), 26 | requirement("bcrypt"), 27 | requirement("boto3"), 28 | requirement("botocore"), 29 | requirement("certifi"), 30 | requirement("cffi"), 31 | requirement("chardet"), 32 | requirement("click"), 33 | requirement("configparser"), 34 | requirement("contextlib2"), 35 | requirement("cryptography"), 36 | requirement("dataset"), 37 | requirement("docutils"), 38 | requirement("enum34"), 39 | requirement("Flask"), 40 | requirement("Flask-Caching"), 41 | requirement("flask-marshmallow"), 42 | requirement("Flask-Migrate"), 43 | requirement("flask-restx"), 44 | requirement("Flask-Script"), 45 | requirement("Flask-SQLAlchemy"), 46 | requirement("functools32"), 47 | requirement("futures"), 48 | requirement("gevent"), 49 | requirement("greenlet"), 50 | requirement("gunicorn"), 51 | requirement("idna"), 52 | requirement("importlib-metadata"), 53 | requirement("ipaddress"), 54 | requirement("itsdangerous"), 55 | requirement("Jinja2"), 56 | requirement("jmespath"), 57 | requirement("jsonschema"), 58 | requirement("Mako"), 59 | requirement("MarkupSafe"), 60 | requirement("marshmallow"), 61 | requirement("marshmallow-sqlalchemy"), 62 | requirement("mistune"), 63 | requirement("netaddr"), 64 | requirement("passlib"), 65 | requirement("pathlib2"), 66 | requirement("pycparser"), 67 | requirement("PyJWT"), 68 | requirement("PyMySQL"), 69 | requirement("pyrsistent"), 70 | requirement("python-dateutil"), 71 | requirement("python-dotenv"), 72 | requirement("python-editor"), 73 | requirement("pytz"), 74 | requirement("redis"), 75 | requirement("requests"), 76 | requirement("s3transfer"), 77 | requirement("scandir"), 78 | requirement("six"), 79 | requirement("SQLAlchemy"), 80 | requirement("SQLAlchemy-Utils"), 81 | requirement("typing"), 82 | requirement("urllib3"), 83 | requirement("Werkzeug"), 84 | requirement("zipp"), 85 | ], 86 | ) 87 | -------------------------------------------------------------------------------- /third_party/ctfd/manage.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_sqlalchemy import SQLAlchemy 3 | from flask_script import Manager 4 | from flask_migrate import Migrate, MigrateCommand 5 | from CTFd import create_app 6 | from CTFd.utils import get_config as get_config_util, set_config as set_config_util 7 | from CTFd.models import * 8 | import logging 9 | 10 | logging.basicConfig() 11 | 12 | app = create_app() 13 | 14 | manager = Manager(app) 15 | manager.add_command("db", MigrateCommand) 16 | 17 | 18 | def jsenums(): 19 | from CTFd.constants import JS_ENUMS 20 | import json 21 | import os 22 | 23 | path = os.path.join(app.root_path, "themes/core/assets/js/constants.js") 24 | 25 | with open(path, "w+") as f: 26 | for k, v in JS_ENUMS.items(): 27 | f.write("const {} = Object.freeze({});".format(k, json.dumps(v))) 28 | 29 | 30 | BUILD_COMMANDS = {"jsenums": jsenums} 31 | 32 | 33 | @manager.command 34 | def get_config(key): 35 | with app.app_context(): 36 | print(get_config_util(key)) 37 | 38 | 39 | @manager.command 40 | def set_config(key, value): 41 | with app.app_context(): 42 | print(set_config_util(key, value).value) 43 | 44 | 45 | @manager.command 46 | def build(cmd): 47 | with app.app_context(): 48 | cmd = BUILD_COMMANDS.get(cmd) 49 | cmd() 50 | 51 | 52 | if __name__ == "__main__": 53 | manager.run() 54 | -------------------------------------------------------------------------------- /third_party/easfs/BUILD: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar") 2 | load("@io_bazel_rules_docker//container:container.bzl", "container_image") 3 | load("@io_bazel_rules_docker//go:image.bzl", "go_image") 4 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 5 | 6 | container_image( 7 | name = "image", 8 | base = ":easfs-image", 9 | tars = [ 10 | ":templates", 11 | ":static", 12 | ], 13 | visibility = ["//visibility:public"], 14 | ) 15 | 16 | go_image( 17 | name = "easfs-image", 18 | args = [ 19 | "-site", 20 | "/site", 21 | ], 22 | embed = [":go_default_library"], 23 | ) 24 | 25 | pkg_tar( 26 | name = "templates", 27 | srcs = glob(["templates/**/*"]), 28 | mode = "0755", 29 | strip_prefix = ".", 30 | ) 31 | 32 | pkg_tar( 33 | name = "static", 34 | srcs = glob(["static/**/*"]), 35 | mode = "0755", 36 | strip_prefix = ".", 37 | ) 38 | 39 | go_library( 40 | name = "go_default_library", 41 | srcs = [ 42 | "BookParser.go", 43 | "FooterParser.go", 44 | "MDParser.go", 45 | "ProjectParser.go", 46 | "YAMLParser.go", 47 | "error.go", 48 | "helper.go", 49 | "load.go", 50 | "main.go", 51 | "page.go", 52 | "redirector.go", 53 | ], 54 | importpath = "github.com/adamyi/CTFProxy/third_party/easfs", 55 | visibility = ["//visibility:private"], 56 | deps = [ 57 | "@com_github_flosch_pongo2//:go_default_library", 58 | "@com_github_gholt_blackfridaytext//:go_default_library", 59 | "@com_github_gosimple_slug//:go_default_library", 60 | "@com_github_nytimes_gziphandler//:go_default_library", 61 | "@com_github_shirou_gopsutil//load:go_default_library", 62 | "@in_gopkg_russross_blackfriday_v2//:go_default_library", 63 | "@in_gopkg_yaml_v2//:go_default_library", 64 | ], 65 | ) 66 | 67 | go_binary( 68 | name = "easfs", 69 | embed = [":go_default_library"], 70 | visibility = ["//visibility:public"], 71 | ) 72 | -------------------------------------------------------------------------------- /third_party/easfs/FooterParser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "gopkg.in/yaml.v2" 7 | ) 8 | 9 | type Promo struct { 10 | Label string `yaml:"label"` 11 | Description string `yaml:"description"` 12 | Path string `yaml:"path"` 13 | Icon string `yaml:"icon"` 14 | } 15 | 16 | type Linkbox struct { 17 | Name string `yaml:"name"` 18 | Contents []struct { 19 | Label string `yaml:"label"` 20 | Path string `yaml:"path"` 21 | } `yaml:"contents"` 22 | } 23 | 24 | type Footer struct { 25 | Footer []struct { 26 | Promos []Promo `yaml:"promos,omitempty"` 27 | Linkboxes []Linkbox `yaml:"linkboxes,omitempty"` 28 | Banner string `yaml:"banner,omitempty"` 29 | } `yaml:"footer"` 30 | } 31 | 32 | func ParseFooter(filepath string) (string, []Promo, []Linkbox, error) { 33 | footerContent, err := ioutil.ReadFile(flagSitePath + filepath) 34 | if err != nil { 35 | return "", nil, nil, err 36 | } 37 | footer := Footer{} 38 | promos := []Promo{} 39 | linkboxes := []Linkbox{} 40 | banner := "" 41 | err = yaml.Unmarshal(footerContent, &footer) 42 | for _, f := range footer.Footer { 43 | promos = append(promos, f.Promos...) 44 | linkboxes = append(linkboxes, f.Linkboxes...) 45 | if f.Banner != "" { 46 | banner = f.Banner 47 | } 48 | } 49 | return banner, promos, linkboxes, nil 50 | } 51 | -------------------------------------------------------------------------------- /third_party/easfs/ProjectParser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "gopkg.in/yaml.v2" 7 | ) 8 | 9 | type Project struct { 10 | ParentProjectMetadataPath string `yaml:"parent_project_metadata_path"` 11 | Name string `yaml:"name"` 12 | Description string `yaml:"description"` 13 | HomeURL string `yaml:"home_url"` 14 | Color string `yaml:"color"` 15 | ContentLicense string `yaml:"content_license"` 16 | FooterPath string `yaml:"footer_path"` 17 | GoogleAnalyticsIds []string `yaml:"google_analytics_ids"` 18 | Icon struct { 19 | Path string `yaml:"path"` 20 | Type string `yaml:"type"` 21 | } `yaml:"icon"` 22 | SocialMedia struct { 23 | Image struct { 24 | Path string `yaml:"path"` 25 | Width int `yaml:"width"` 26 | Height int `yaml:"height"` 27 | } `yaml:"image"` 28 | } `yaml:"social_media"` 29 | } 30 | 31 | func ParseProject(filepath string) (*Project, *Project, error) { 32 | projectContent, err := ioutil.ReadFile(flagSitePath + filepath) 33 | if err != nil { 34 | return nil, nil, err 35 | } 36 | project := Project{} 37 | err = yaml.Unmarshal(projectContent, &project) 38 | if err != nil { 39 | return nil, nil, err 40 | } 41 | if project.ParentProjectMetadataPath == "" { 42 | return &project, nil, nil 43 | } 44 | parentProjectContent, err := ioutil.ReadFile(flagSitePath + project.ParentProjectMetadataPath) 45 | if err != nil { 46 | return nil, nil, err 47 | } 48 | parentProject := Project{} 49 | err = yaml.Unmarshal(parentProjectContent, &parentProject) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | return &project, &parentProject, nil 54 | } 55 | -------------------------------------------------------------------------------- /third_party/easfs/README.md: -------------------------------------------------------------------------------- 1 | # EASFS (EAsy Static Front-end Server) 2 | 3 | A web-server to allow fast development and iterations of static websites, based on YAML and Markdown. 4 | 5 | It's similar to Jekyll but pages are rendered at request time. 6 | 7 | While I did write all the back-end code (Golang), it currently uses the same front-end as Google DevSite, 8 | because I'm lazy to write CSS. But it's a TODO to move this away from Google DevSite CSS & JS. 9 | 10 | ## Build 11 | ``` 12 | bazel build //:easfs 13 | ``` 14 | 15 | ## Example 16 | https://www.adamyi.com/ is served using EASFS and https://github.com/adamyi/adamyi.com 17 | 18 | ## License 19 | It was originally a fork to Google's [Web Fundamentals](https://github.com/google/WebFundamentals) project, 20 | but is now rewritten in Golang. Yet it still uses the CSS & JS files from Web Fundamentals. 21 | 22 | Copyright 2018-2019 Adam Yi. 23 | 24 | Copyright 2014-2018 Google LLC. 25 | 26 | Under [Apache 2.0 License](LICENSE). 27 | -------------------------------------------------------------------------------- /third_party/easfs/YAMLParser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/flosch/pongo2" 8 | "gopkg.in/yaml.v2" 9 | ) 10 | 11 | type YAMLPage struct { 12 | ProjectPath string `yaml:"project_path"` 13 | BookPath string `yaml:"book_path"` 14 | Title string `yaml:"title"` 15 | LandingPage struct { 16 | CustomCSSPath string `yaml:"custom_css_path,omitempty"` 17 | CustomJSPath string `yaml:"custom_js_path,omitempty"` 18 | MetaTags []struct { 19 | Name string `yaml:"name"` 20 | Content string `yaml:"content"` 21 | } `yaml:"meta_tags"` 22 | Header struct { 23 | Name string `yaml:"name,omitempty"` 24 | Description string `yaml:"description,omitempty"` 25 | CustomHTML string `yaml:"custom_html,omitempty"` 26 | } `yaml:"header"` 27 | Rows []struct { 28 | ClassName string `yaml:"classname,omitempty"` 29 | Items []struct { 30 | ClassName string `yaml:"classname,omitempty"` 31 | Heading string `yaml:"heading,omitempty"` 32 | Description string `yaml:"description,omitempty"` 33 | ImagePath string `yaml:"image_path,omitempty"` 34 | Path string `yaml:"path,omitempty"` 35 | CustomHTML string `yaml:"custom_html,omitempty"` 36 | Buttons []struct { 37 | Label string `yaml:"label"` 38 | Target string `yaml:"target"` 39 | Path string `yaml:"path"` 40 | ClassName string `yaml:"classname"` 41 | } `yaml:"buttons,omitempty"` 42 | } `yaml:"items"` 43 | Heading string `yaml:"heading,omitempty"` 44 | Background string `yaml:"background,omitempty"` 45 | Description string `yaml:"description,omitempty"` 46 | CustomHTML string `yaml:"custom_html,omitempty"` 47 | ItemCount int `yaml:"item_count,omitempty"` 48 | } `yaml:"rows"` 49 | } `yaml:"landing_page"` 50 | } 51 | 52 | func ParseYAML(w http.ResponseWriter, content []byte, requestPath string) error { 53 | tmpl, err := pongo2.FromFile("/templates/page-landing.html") 54 | if err != nil { 55 | fmt.Println(err.Error()) 56 | return err 57 | } 58 | context := pongo2.Context{} 59 | context["bodyClass"] = "devsite-landing-page" 60 | context["requestPath"] = requestPath 61 | context["isProd"] = flagProd 62 | parsedYAML := YAMLPage{} 63 | err = yaml.Unmarshal(content, &parsedYAML) 64 | if err != nil { 65 | return err 66 | } 67 | for idx := range parsedYAML.LandingPage.Rows { 68 | if parsedYAML.LandingPage.Rows[idx].CustomHTML != "" { 69 | parsedYAML.LandingPage.Rows[idx].CustomHTML = string(RenderContent([]byte(parsedYAML.LandingPage.Rows[idx].CustomHTML))) 70 | } 71 | parsedYAML.LandingPage.Rows[idx].ItemCount = len(parsedYAML.LandingPage.Rows[idx].Items) 72 | for idx2 := range parsedYAML.LandingPage.Rows[idx].Items { 73 | if parsedYAML.LandingPage.Rows[idx].Items[idx2].CustomHTML != "" { 74 | parsedYAML.LandingPage.Rows[idx].Items[idx2].CustomHTML = string(RenderContent([]byte(parsedYAML.LandingPage.Rows[idx].Items[idx2].CustomHTML))) 75 | } 76 | } 77 | } 78 | // fmt.Println(parsedYAML) 79 | context["rows"] = parsedYAML.LandingPage.Rows 80 | context["customJSPath"] = parsedYAML.LandingPage.CustomJSPath 81 | context["customCSSPath"] = parsedYAML.LandingPage.CustomCSSPath 82 | context["metaTags"] = parsedYAML.LandingPage.MetaTags 83 | project, parentProject, err := ParseProject(parsedYAML.ProjectPath) 84 | if err != nil { 85 | return err 86 | } 87 | book, err := ParseBook(parsedYAML.BookPath) 88 | if err != nil { 89 | return err 90 | } 91 | context["projectYaml"] = *project 92 | context["logoRowIcon"] = project.Icon.Path 93 | context["logoRowIconType"] = project.Icon.Type 94 | if parsedYAML.LandingPage.Header.Name != "" { 95 | context["logoRowTitle"] = parsedYAML.LandingPage.Header.Name 96 | } else if parentProject != nil { 97 | context["logoRowTitle"] = parentProject.Name 98 | } else { 99 | context["logoRowTitle"] = project.Name 100 | } 101 | context["customHeader"] = string(RenderContent([]byte(parsedYAML.LandingPage.Header.CustomHTML))) 102 | if parsedYAML.Title != "" { 103 | context["headerTitle"] = parsedYAML.Title 104 | } else if project.Name != "" { 105 | context["headerTitle"] = project.Name 106 | } else if parentProject != nil { 107 | context["headerTitle"] = parentProject.Name 108 | } 109 | if parsedYAML.LandingPage.Header.Description != "" { 110 | context["headerDescription"] = parsedYAML.LandingPage.Header.Description 111 | } else if project.Description != "" { 112 | context["headerDescription"] = project.Description 113 | } else if parentProject != nil { 114 | context["headerDescription"] = parentProject.Description 115 | } 116 | // TODO: header buttons 117 | if parsedYAML.Title != "" { 118 | context["pageTitle"] = parsedYAML.Title + " | " + project.Name 119 | } else { 120 | context["pageTitle"] = project.Name 121 | } 122 | context["bookYaml"] = book 123 | context["lowerTabs"] = GetLowerTabs(requestPath, book) 124 | context["footerBanner"], context["footerPromos"], context["footerLinks"], err = ParseFooter(project.FooterPath) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | // fmt.Println(context) 130 | 131 | return tmpl.ExecuteWriter(context, w) 132 | } 133 | -------------------------------------------------------------------------------- /third_party/easfs/error.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "runtime/debug" 7 | ) 8 | 9 | type UPError struct { 10 | Title string 11 | Description string 12 | PublicDebug string 13 | InternalDebug string 14 | Code int 15 | } 16 | 17 | func NewUPError(code int, title, description, publicDebug, internalDebug string) *UPError { 18 | ret := &UPError{Code: code, Title: title, Description: description, PublicDebug: publicDebug, InternalDebug: internalDebug} 19 | ret.InternalDebug += "\n\n===EASFS Stack Trace===\n" + string(debug.Stack()) 20 | return ret 21 | } 22 | 23 | func ReturnError(rsp http.ResponseWriter, err *UPError) { 24 | rsp.Header().Set("Content-Type", "ctfproxy/error") 25 | rsp.WriteHeader(err.Code) 26 | json.NewEncoder(rsp).Encode(err) 27 | } 28 | -------------------------------------------------------------------------------- /third_party/easfs/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/adamyi/easfs 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/NYTimes/gziphandler v1.1.1 7 | github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4 8 | github.com/gholt/blackfridaytext v0.0.0-20190816214545-16f7b9b9742e 9 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 10 | github.com/gosimple/slug v1.7.0 11 | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect 12 | github.com/shirou/gopsutil v2.18.12+incompatible 13 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 14 | gopkg.in/russross/blackfriday.v2 v2.0.0 15 | gopkg.in/yaml.v2 v2.2.2 16 | ) 17 | -------------------------------------------------------------------------------- /third_party/easfs/go.sum: -------------------------------------------------------------------------------- 1 | github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= 2 | github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4 h1:GY1+t5Dr9OKADM64SYnQjw/w99HMYvQ0A8/JoUkxVmc= 5 | github.com/flosch/pongo2 v0.0.0-20190707114632-bbf5a6c351f4/go.mod h1:T9YF2M40nIgbVgp3rreNmTged+9HrbNTIQf1PsaIiTA= 6 | github.com/gholt/blackfridaytext v0.0.0-20190816214545-16f7b9b9742e h1:aWeuOsmyHzAuZvekBl4pnJgJCtYLnc7X5JlCQocUros= 7 | github.com/gholt/blackfridaytext v0.0.0-20190816214545-16f7b9b9742e/go.mod h1:NsYVvBFrjFuHywHmgYgbGtbjuqgJHZ2Qwb0G4k6wm6Q= 8 | github.com/gholt/brimtext v0.0.0-20190811231012-1fbdf4665642 h1:OfEy3A+F4fmU2ZgBd6fBJ03gR6Kw5euUbs5tpGXD/6U= 9 | github.com/gholt/brimtext v0.0.0-20190811231012-1fbdf4665642/go.mod h1:gbGD4x/o6OSgyScStZ9iJT+Eo1bTY93+3ydlYKjpotM= 10 | github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= 11 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 12 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 13 | github.com/gosimple/slug v1.7.0 h1:BlCZq+BMGn+riOZuRKnm60Fe7+jX9ck6TzzkN1r8TW8= 14 | github.com/gosimple/slug v1.7.0/go.mod h1:ER78kgg1Mv0NQGlXiDe57DpCyfbNywXXZ9mIorhxAf0= 15 | github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 h1:rhqTjzJlm7EbkELJDKMTU7udov+Se0xZkWmugr6zGok= 16 | github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= 17 | github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= 18 | github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= 19 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 20 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 21 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 22 | github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= 25 | github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= 26 | github.com/russross/blackfriday v0.0.0-20171011182219-6d1ef893fcb0 h1:hgS5QyP981zzGr3UYaoHb5+fpgK1lHleAOq5znvfJxU= 27 | github.com/russross/blackfriday v0.0.0-20171011182219-6d1ef893fcb0/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 28 | github.com/shirou/gopsutil v2.18.12+incompatible h1:1eaJvGomDnH74/5cF4CTmTbLHAriGFsTZppLXDX93OM= 29 | github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 30 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 31 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 34 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 35 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 36 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 37 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 38 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 39 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 40 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 42 | golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 45 | gopkg.in/russross/blackfriday.v2 v2.0.0 h1:+FlnIV8DSQnT7NZ43hcVKcdJdzZoeCmJj4Ql8gq5keA= 46 | gopkg.in/russross/blackfriday.v2 v2.0.0/go.mod h1:6sSBNz/GtOm/pJTuh5UmBK2ZHfmnxGbl2NZg1UliSOI= 47 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 48 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 49 | -------------------------------------------------------------------------------- /third_party/easfs/helper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "regexp" 8 | 9 | "github.com/flosch/pongo2" 10 | "github.com/gosimple/slug" 11 | ) 12 | 13 | func Slugify(in *pongo2.Value, param *pongo2.Value) (out *pongo2.Value, err *pongo2.Error) { 14 | if !in.IsString() { 15 | return nil, &pongo2.Error{ 16 | OrigError: fmt.Errorf("only strings should be sent to the slugify filter"), 17 | } 18 | } 19 | 20 | s := in.String() 21 | s = slug.Make(s) 22 | 23 | return pongo2.AsValue(s), nil 24 | } 25 | 26 | func IsDir(filepath string) bool { 27 | fi, err := os.Stat(filepath) 28 | if err != nil { 29 | return false 30 | } 31 | return fi.IsDir() 32 | } 33 | 34 | func GetInclude(include []byte) []byte { 35 | // {% include "_shared/latest_articles.html" %} 36 | // fmt.Println("src/content/" + string(include[12:len(include)-4])) 37 | inc, _ := ioutil.ReadFile(flagSitePath + string(include[12:len(include)-4])) 38 | return inc 39 | } 40 | 41 | func RenderContent(content []byte) []byte { 42 | // TODO: includecode & htmlescape 43 | // fmt.Println(string(content)) 44 | content = regexp.MustCompile(`(?ms){%[ ]?include .+%}`).ReplaceAllFunc(content, GetInclude) 45 | 46 | return content 47 | } 48 | -------------------------------------------------------------------------------- /third_party/easfs/load.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/shirou/gopsutil/load" 4 | 5 | import "fmt" 6 | 7 | func loadAverage() string { 8 | loadstr := "unknown" 9 | l, err := load.Avg() 10 | if err == nil { 11 | loadstr = fmt.Sprintf("%.2f %.2f %.2f", l.Load1, l.Load5, l.Load15) 12 | } 13 | return loadstr 14 | } 15 | -------------------------------------------------------------------------------- /third_party/easfs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "net/http" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/NYTimes/gziphandler" 10 | "github.com/flosch/pongo2" 11 | ) 12 | 13 | var ( 14 | flagListenAddress string 15 | flagSSLListenAddress string 16 | flagSSLCert string 17 | flagSSLKey string 18 | flagDomain string 19 | flagProd bool 20 | flagSitePath string 21 | ) 22 | 23 | func EASFSHandler(w http.ResponseWriter, r *http.Request) { 24 | w.Header().Set("Content-Security-Policy", "frame-ancestors *") 25 | w.Header().Set("Server", "easfs") 26 | 27 | ext := filepath.Ext(r.URL.Path) 28 | if ext == ".md" || ext == ".html" { 29 | http.Redirect(w, r, strings.TrimSuffix(r.URL.Path, ext), 301) 30 | return 31 | } 32 | 33 | // expiration := time.Now().Add(time.Hour) 34 | // cookie := http.Cookie{Name: "hl", Value: language, Expires: expiration} 35 | // http.SetCookie(w, &cookie) 36 | 37 | url := r.URL.Path 38 | if url == "/_s/getsuggestions" { 39 | if r.URL.Query().Get("c") == "2" { 40 | if r.URL.Query().Get("p") == "" { 41 | url = "/_suggestions" 42 | } else { 43 | url = filepath.Join("/", r.URL.Query().Get("p"), "/_suggestions") 44 | } 45 | } else { 46 | url = "/_empty_suggestions" 47 | } 48 | } else if strings.Contains(url, "/_") { 49 | ReturnError(w, NewUPError(http.StatusNotFound, "404 Not Found", "The requested URL was not found on this server.", "", "underscore urls are not visible")) 50 | return 51 | } 52 | 53 | var err error 54 | if IsDir(flagSitePath + url) { 55 | // make sure that directory ends with a / 56 | if !strings.HasSuffix(url, "/") { 57 | http.Redirect(w, r, r.URL.Path+"/", 301) 58 | return 59 | } 60 | err = GetPage(w, url) 61 | if err.Error() != "file not found" { 62 | ReturnError(w, NewUPError(http.StatusInternalServerError, "500 Internal Server Error", "An error occurred while trying to fulfill your request. That's all we know.", "", err.Error())) 63 | return 64 | } 65 | if err != nil { 66 | err = GetIndex(w, url) 67 | } 68 | } else { 69 | err = GetPage(w, url) 70 | } 71 | if err != nil { 72 | if err.Error() != "file not found" { 73 | ReturnError(w, NewUPError(http.StatusInternalServerError, "500 Internal Server Error", "An error occurred while trying to fulfill your request. That's all we know.", "", err.Error())) 74 | return 75 | } 76 | red, err := GetRedirect(url) 77 | if err == nil { 78 | http.Redirect(w, r, red, 301) 79 | } else { 80 | ReturnError(w, NewUPError(http.StatusNotFound, "404 Not Found", "The requested URL was not found on this server.", "", "not found")) 81 | } 82 | } 83 | 84 | // fmt.Fprintf(w, "EASFS serving!\n") 85 | 86 | } 87 | 88 | func RedirectSSL(rsp http.ResponseWriter, req *http.Request) { 89 | rsp.Header().Set("X-Frame-Options", "SAMEORIGIN") 90 | rsp.Header().Set("Server", "easfs") 91 | target := "https://" + req.Host + req.URL.Path 92 | if len(req.URL.RawQuery) > 0 { 93 | target += "?" + req.URL.RawQuery 94 | } 95 | http.Redirect(rsp, req, target, 96 | http.StatusPermanentRedirect) 97 | } 98 | 99 | func RedirectDomain(rsp http.ResponseWriter, req *http.Request) { 100 | target := "https://" + flagDomain + req.URL.Path 101 | if len(req.URL.RawQuery) > 0 { 102 | target += "?" + req.URL.RawQuery 103 | } 104 | http.Redirect(rsp, req, target, 105 | http.StatusTemporaryRedirect) 106 | } 107 | 108 | func main() { 109 | flag.StringVar(&flagListenAddress, "listen", "0.0.0.0:80", "HTTP listen address") 110 | flag.StringVar(&flagSSLListenAddress, "slisten", "0.0.0.0:443", "HTTPS listen address") 111 | flag.StringVar(&flagSSLCert, "cert", "cert.pem", "HTTPS cert") 112 | flag.StringVar(&flagSSLKey, "key", "key.pem", "HTTPS key") 113 | flag.StringVar(&flagDomain, "domain", "", "Site domain") 114 | flag.StringVar(&flagSitePath, "site", "site/content/", "Path to site content") 115 | flag.BoolVar(&flagProd, "prod", false, "prod env") 116 | flag.Parse() 117 | pongo2.RegisterFilter("slugify", Slugify) 118 | mux := http.NewServeMux() 119 | fs := http.FileServer(http.Dir("/static")) 120 | mux.Handle("/_static/", gziphandler.GzipHandler(http.StripPrefix("/_static/", fs))) 121 | mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { 122 | w.Write([]byte("ok")) 123 | }) 124 | mux.Handle("/", gziphandler.GzipHandler(http.HandlerFunc(EASFSHandler))) 125 | http.ListenAndServe(flagListenAddress, mux) 126 | } 127 | -------------------------------------------------------------------------------- /third_party/easfs/page.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | func GetIndex(w http.ResponseWriter, path string) error { 11 | extensions := [3]string{"_index.yaml", "index.md", "index.html"} 12 | for _, ext := range extensions { 13 | fileLocation := flagSitePath + path + ext 14 | // fmt.Printf("checking %s\n", fileLocation) 15 | content, err := ioutil.ReadFile(fileLocation) 16 | if err == nil { 17 | if ext == "index.md" { 18 | return ParseMD(w, content, path) 19 | } else if ext == "_index.yaml" { 20 | return ParseYAML(w, content, path) 21 | } 22 | w.Header().Set("Content-Type", "text/html") 23 | w.Write(content) 24 | return nil 25 | } 26 | } 27 | return fmt.Errorf("file not found") 28 | } 29 | 30 | func GetPage(w http.ResponseWriter, path string) error { 31 | extensions := [5]string{".md", ".html", ".json", ""} 32 | for _, ext := range extensions { 33 | fileLocation := flagSitePath + path + ext 34 | // fmt.Printf("checking %s\n", fileLocation) 35 | content, err := ioutil.ReadFile(fileLocation) 36 | if err == nil { 37 | if ext == ".md" { 38 | w.Header().Set("Content-Type", "text/html") 39 | return ParseMD(w, content, path) 40 | } else if ext == ".html" { 41 | // return ParseHTML(content) 42 | } 43 | if strings.HasSuffix(fileLocation, ".html") { 44 | w.Header().Set("Content-Type", "text/html") 45 | } else if strings.HasSuffix(fileLocation, ".js") { 46 | w.Header().Set("Content-Type", "application/javascript") 47 | } else if strings.HasSuffix(fileLocation, ".css") { 48 | w.Header().Set("Content-Type", "text/css") 49 | } else if strings.HasSuffix(fileLocation, ".json") { 50 | w.Header().Set("Content-Type", "application/json") 51 | } else if strings.HasSuffix(fileLocation, ".svg") { 52 | w.Header().Set("Content-Type", "image/svg+xml") 53 | } 54 | w.Write(content) 55 | return nil 56 | } 57 | } 58 | return fmt.Errorf("file not found") 59 | } 60 | -------------------------------------------------------------------------------- /third_party/easfs/redirector.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | type Redirects struct { 11 | Redirects []struct { 12 | From string `yaml:"from"` 13 | To string `yaml:"to"` 14 | } `yaml:"redirects"` 15 | } 16 | 17 | func ParseRedirects(filepath string) (Redirects, error) { 18 | redContent, err := ioutil.ReadFile(flagSitePath + filepath + "/_redirects.yaml") 19 | red := Redirects{} 20 | if err != nil { 21 | return red, err 22 | } 23 | err = yaml.Unmarshal(redContent, &red) 24 | return red, err 25 | } 26 | 27 | func GetRedirect(filepath string) (string, error) { 28 | // TODO: recursive check 29 | red, err := ParseRedirects("") 30 | if err != nil { 31 | return "", err 32 | } 33 | for _, r := range red.Redirects { 34 | if r.From == filepath { 35 | return r.To, nil 36 | } 37 | } 38 | return "", fmt.Errorf("no redirection found") 39 | } 40 | -------------------------------------------------------------------------------- /third_party/easfs/static/images/redesign-14/button-down-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /third_party/easfs/static/images/redesign-14/button-down-grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /third_party/easfs/static/images/redesign-14/nav-status-experimental.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /third_party/easfs/static/scripts/devsite-dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // COMP6443{I_FOUND_IT_c9876289e54620cdfa7b4dc5bb66f1f3} 4 | 5 | /* eslint browser:true */ 6 | 7 | const body = document.querySelector('body'); 8 | const isDocPage = document 9 | .querySelector('body.devsite-doc-page') ? true : false; 10 | 11 | function highlightActiveNavElement() { 12 | var elems = document.querySelectorAll('.devsite-section-nav li.devsite-nav-active'); 13 | for (var i = 0; i < elems.length; i++) { 14 | expandPathAndHighlight(elems[i]); 15 | } 16 | } 17 | 18 | function expandPathAndHighlight(elem) { 19 | // Walks up the tree from the current element and expands all tree nodes 20 | var parent = elem.parentElement; 21 | var parentIsCollapsed = parent.classList.contains('devsite-nav-section-collapsed'); 22 | if (parent.localName === 'ul' && parentIsCollapsed) { 23 | parent.classList.toggle('devsite-nav-section-collapsed'); 24 | parent.classList.toggle('devsite-nav-section-expanded'); 25 | // Checks if the grandparent is an expandable element 26 | var grandParent = parent.parentElement; 27 | var grandParentIsExpandable = grandParent.classList.contains('devsite-nav-item-section-expandable'); 28 | if (grandParent.localName === 'li' && grandParentIsExpandable) { 29 | var anchor = grandParent.querySelector('a.devsite-nav-toggle'); 30 | anchor.classList.toggle('devsite-nav-toggle-expanded'); 31 | anchor.classList.toggle('devsite-nav-toggle-collapsed'); 32 | expandPathAndHighlight(grandParent); 33 | } 34 | } 35 | } 36 | 37 | function getCookieValue(name, defaultValue) { 38 | const value = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)'); 39 | return value ? value.pop() : defaultValue; 40 | } 41 | 42 | function initYouTubeVideos() { 43 | var videoElements = body 44 | .querySelectorAll('iframe.devsite-embedded-youtube-video'); 45 | videoElements.forEach(function(elem) { 46 | const videoID = elem.getAttribute('data-video-id'); 47 | if (videoID) { 48 | let videoURL = 'https://www.youtube.com/embed/' + videoID; 49 | videoURL += '?autohide=1&showinfo=0&enablejsapi=1'; 50 | elem.src = videoURL; 51 | } 52 | }); 53 | } 54 | 55 | function init() { 56 | initYouTubeVideos(); 57 | highlightActiveNavElement(); 58 | console.log("%cMMMMWNKOxollcclldxOKWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMW0olllllllllllodxOXWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNx;;dNMMMMMMMMMMM\nMMWKkl:;;lxo:;,;clccokXWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWk:,:looooooool:;;cONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNd;;dNMMMMMMMMMMM\nMNkc;;;;oXMWk:ckXNOc;;lONMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWk:,l0WWWWWWWWNKd:,:OWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNd;;dNMMMMMMMMMMM\nXd:oO0xcxNMMKokWMM0l;;;:xNMMNK000KWMMMMWX0000XWMMMMMMWXK0O00KNWMMMMMMMMMMWXK00O0KXNWMMMMMMMWNK00000KXNWMMMMMWNXK00OO0KXWWMMMMMWO:,lKMMMMMMMMMM0c;;xWMMWNXK00O00KNWMMMWX0KNWNX00O0KXWMMMNd;;dNMMMMMWK00KN\nd;:OWMWOoOXXxlOWW0dok0kl:kWWk:;;;lKMMMMNd;;;;dNMMMNKxoc:;;;;:cokXWMMMMWXkoc:;;;;;:lx0NMMWXOdl:;;;;;:cldONWNOdlc:;;;;;;cokXWMMMWO:;lKMMMMMMMMMNx:;cOWNOdlc:;;;;;:lxKWMNx;:oxl::;;;:cd0WMNd;;dNMMMWKd:;ckX\nc,;oXWMXo:okOkxdlcxNMMXo;lKWk:,,;lKMMMMNd;,;;dNMWKd:;,;;:::;;,;;ckXMMNkc;,;;:cc:;;;:dKMW0l;;;;;:cc:;;;;l0WNd;;;:cclc:;;;;l0WMMWO:;cxOOOOOOOkxl:;lOWMNklldkOO0Oko:;cOWNd;,;:okO00Od:;cOWNd;;dNMWXxc;:dKWM\n:,,;lxkdldKWMMNx:l0WWXx:;c0Wk:,,;lKMMMMNd;;;;dNWKl;;,;lkKXX0x:;;;;dXNk:,;;:xKXNX0xdONWW0c;,,;oOXNXKxodkXWMMXkxOKXNNXOl;;,;dNMMWO:,;;;;;;;;;;;;;:dKWMMWNNMMMMMMMNx;,oXNd;;cOWMMMMMNx;;oXNd;;dNNkc;:o0WMMM\nc,;;;:dOKWMMMMMNOooxdc;,,lKWk:;;;lKMMMMNd;;;;dXNx;,;;l0WMMMMWk:,;;:OKl,;,;xWMMMMMWWMMMNd;;;;oXMMMMMWWWMMMMMWX0kxxddddc;;,;oXMMWk:,ck00000000Oxo:;ckNMWNX0Okkkkkko;;lKNd;;dNMMMMMMWO:;lKNd;;okl;;;dXMMMMM\nd;;;:kWMMMMMMMMMMXx:;,,,:kNWk:;,;lKMMMMNd;,,;dNNd;;;;oXMMMMMM0c;;;:k0l;;;:OWMMMMMMMMMMNd;,;;dNMMMMMMMMMMMMXxc;;,;;;;;;;,,,lXMMWO:,lKMMMMMMMMMMNx:,:OXkl::cclllll:;,lKNd;;dNMMMMMMMO:,lKNd;;;;;:;;ckNMMMM\nXd;,:xNMMMMMMMMMMMXo;;;:xNMWk:,;;c0WMMMKl;;,;dNWk:;;;:kNWMMWXd;;;;l0Xd;;;;oKWMMMWK0XWMWk:;;;cOWMMMWX00KWMNd;;,;lk000Ol;,,,lXMMWO:,lKMMMMMMMMMMWOc;:do;;lOXNNNNNXd;;lKNd;;dNMMMMMMMO:;lKNd;;;cxKOc;:dXWMM\nMNkc;:lxkkOOO0KXK0d:;;,ckNMM0c;;;;lkOOxl;;;;;oNMNx:;,;:lxkkdc;,,;cOWW0l;;,;cdkkxoc;cxKWXd;,,;:oxkkdc:;:d00l;;,;dKXX0d:;,,,lXMMWO:,l0WWWWWWWWWXOl;;lxl;;xNWMMWNKxc;;lKNd;;dNMMMMMMMO:;lKNd;;l0WMWKo;;l0WM\nMMWXkl:;;;;;;;:::;;::;,;:oKWNkc;,;,;;;;;;;;;;dNMMN0o:;;;,;,,,,;cdKWMMWKdc;,,,,;;;;;:l0WMNkl;;;;;;;;,,;;ckXkc;,,;:c:;;:;,;,lXMMWO:,;loooooooolc;;:dKNk:;:ldddol:cc;;lKNd;;dNMMMMMMMO:;lKNd;;dNMMMMXx:;:kN\nMMMMMNKOxdllcclodxOK0d:;:dKWMWKkdllclodkxolllkNMMMMNKkdollcloxOXWMMMMMMWXOxollllodk0NWMMMWN0xdlllllodxOXWMWKkdllcloxOOdlllxXMMM0ollllllllllllodkKWMMWKxdllclodOX0dlxXWOolkNMMMMMMM0olxXWOookNMMMMMNOoloO\nMMMMMMMMMWNNXXNNWMMMMNOoOWMMMMMMWNXXXNWMWNNNNWMMMMMMMMWNNXXNWWMMMMMMMMMMMMWWNXXXNWMMMMMMMMMMMWNXXXNWMMMMMMMMMWNXXXWWMWNNNNNWMMMWNNNNNNNNNNNNNWWMMMMMMMMWNXXXNWMMWNNWWMWNNWMMMMMMMMWNNWWMWNNWMMMMMMMWNNNN", "font-size: 1px") 59 | console.log("Dreamed of building a safer bank?"); 60 | console.log("We are hiring!"); 61 | console.log("To apply: https://foobar-recruit.quoccabank.com"); 62 | } 63 | 64 | init(); 65 | -------------------------------------------------------------------------------- /third_party/easfs/static/styles/easfs.css: -------------------------------------------------------------------------------- 1 | .devsite-section-nav-mobile { 2 | display: block; 3 | max-height: 100% !important; 4 | } 5 | 6 | .devsite-landing-row-multiline .devsite-landing-row-group { 7 | flex-wrap: wrap; 8 | } 9 | 10 | /* 11 | .devsite-banner { 12 | margin-top: 0; 13 | } 14 | */ 15 | 16 | body { 17 | background-color: #151515; 18 | color: white; 19 | } 20 | 21 | .devsite-top-logo-row-wrapper { 22 | background-color: black; 23 | } 24 | 25 | .devsite-product-name .devsite-breadcrumb-link { 26 | color: rgba(255, 255, 255, .54); 27 | } 28 | 29 | .devsite-product-name .devsite-breadcrumb-link:hover, .devsite-product-name .devsite-breadcrumb-link:focus { 30 | color: rgba(255, 255, 255, .87); 31 | } 32 | 33 | .devsite-header-icon-button { 34 | color: rgba(255, 255, 255, .54); 35 | } 36 | 37 | .devsite-header-upper-tabs .devsite-doc-set-nav-tab { 38 | color: rgba(255, 255, 255, .54); 39 | } 40 | 41 | .devsite-header-upper-tabs .devsite-doc-set-nav-active { 42 | color: rgba(255, 255, 255, .87); 43 | } 44 | 45 | .devsite-header-upper-tabs .devsite-doc-set-nav-tab:hover, .devsite-header-upper-tabs .devsite-doc-set-nav-tab:focus { 46 | color: rgba(255, 255, 255, .87); 47 | } 48 | 49 | .devsite-landing-row-cards .devsite-landing-row-item:not(.devsite-background) { 50 | background: #555; 51 | } 52 | 53 | .devsite-section-nav { 54 | background: #2f2f2f; 55 | } 56 | 57 | .devsite-page-nav { 58 | background: #2f2f2f; 59 | } 60 | 61 | .devsite-nav-title { 62 | color: #dedede; 63 | } 64 | 65 | .devsite-high-contrast .devsite-search-active .devsite-search-form, .devsite-search-active .devsite-search-form { 66 | background: #8f8f8f; 67 | } 68 | 69 | .devsite-search-form { 70 | background: #5f5f5f; 71 | } 72 | 73 | .devsite-search-form:hover { 74 | background: #8f8f8f; 75 | } 76 | 77 | .devsite-popout { 78 | background: black; 79 | } 80 | 81 | .devsite-search-active .devsite-search-field { 82 | color: white; 83 | } 84 | 85 | .devsite-history-item a, .devsite-suggest-item a { 86 | color: white; 87 | } 88 | 89 | 90 | .devsite-banner-dogfood,.devsite-banner-dogfood :link,.devsite-banner-dogfood :visited { 91 | background: #eceff1; 92 | color: #546e7a; 93 | } 94 | 95 | .devsite-banner-dogfood .devsite-banner-inner { 96 | padding-left: 60px 97 | } 98 | 99 | .devsite-banner-dogfood .devsite-banner-inner::before { 100 | color: #78909c; 101 | content: 'pets'; 102 | float: left; 103 | font: normal normal normal 24px/1 'Material Icons'; 104 | font-feature-settings: 'liga'; 105 | -moz-osx-font-smoothing: grayscale; 106 | -webkit-font-smoothing: antialiased; 107 | text-rendering: optimizeLegibility; 108 | word-wrap: normal; 109 | margin-left: -36px 110 | } 111 | 112 | @media screen and (max-width: 720px) { 113 | .devsite-banner-dogfood .devsite-banner-inner { 114 | padding-left: 52px 115 | } 116 | } 117 | 118 | .easfs-footer--social-btn, 119 | .easfs-footer__social-btn { 120 | width: 20px; 121 | height: 20px; 122 | 123 | padding: 0; 124 | margin: 0; 125 | 126 | border: none; 127 | } 128 | 129 | .easfs-footer--top-section h5, 130 | .easfs-footer__top-section h5 { 131 | display: inline-flex; 132 | text-transform: uppercase; 133 | color: #212121; 134 | } 135 | 136 | .easfs-footer--social-list, 137 | .easfs-footer__social-list { 138 | list-style: none; 139 | display: inline-flex; 140 | padding-left: 10px; 141 | } 142 | 143 | .easfs-footer--social-list li, 144 | .easfs-footer__social-list li { 145 | float: left; 146 | 147 | margin-bottom: 0; 148 | margin-right: 16px; 149 | } 150 | -------------------------------------------------------------------------------- /third_party/easfs/static/styles/empty.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamyi/CTFProxy/9a2979d925a339d1018e69b883512a18d7d69afb/third_party/easfs/static/styles/empty.css -------------------------------------------------------------------------------- /third_party/easfs/templates/framebox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Adam Yi 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% autoescape off %} 14 | {{ content }} 15 | {% endautoescape %} 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/analytics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/announcement-banner.html: -------------------------------------------------------------------------------- 1 | {% if projectYaml.announcement %} 2 |
4 |
5 | {% autoescape off %}{{ projectYaml.announcement.description }}{% endautoescape %} 6 |
7 |
8 | {% endif %} 9 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/article-license.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/devsite-collapsible-section.html: -------------------------------------------------------------------------------- 1 |
2 | {% if customHeader %} 3 | {{ customHeader|safe }} 4 | {% else %} 5 |
6 |
7 |
8 | {% if headerTitle %} 9 |
    10 |
  • 11 | {{ headerTitle }} 12 |
  • 13 |
14 | {% endif %} 15 | {% if headerDescription %} 16 |
17 | {% autoescape off %}{{ headerDescription }}{% endautoescape %} 18 |
19 | {% endif %} 20 |
21 | {% if headerButtons %} 22 |
23 | {% for button in headerButtons %} 24 | {{ button.label }} 25 | {% endfor %} 26 |
27 | {% endif %} 28 |
29 | 30 | {% include "lower-tabs.html" %} 31 |
32 | {% endif %} 33 |
34 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/devsite-footer-banner.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/devsite-footer-linkboxes.html: -------------------------------------------------------------------------------- 1 | {% if footerLinks %} 2 | 20 | {% endif %} 21 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/devsite-footer-promos.html: -------------------------------------------------------------------------------- 1 | {% if footerPromos %} 2 | 16 | {% endif %} 17 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/devsite-nav-responsive.html: -------------------------------------------------------------------------------- 1 | 48 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/devsite-top-logo-row-wrapper-wrapper.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 6 |
7 | {% if logoRowIcon %} 8 | 9 | 10 | 11 | {% endif %} 12 | 13 | 20 | 21 |
22 | {% include "upper-tabs.html" %} 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/dogfood-banner.html: -------------------------------------------------------------------------------- 1 | {% if dogfood %} 2 |
3 |
4 | CONFIDENTIAL - This is a dogfood build. Internal testing only. 5 |
6 |
7 | {% endif %} 8 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/landing-page-content-row.html: -------------------------------------------------------------------------------- 1 |
6 | 7 | {% if item.CustomHTML %} 8 | {{ item.CustomHTML|safe }} 9 | {% endif %} 10 | 11 | {% if item.YoutubeID %} 12 |
13 |
14 | 16 |
17 |
18 | {% endif %} 19 | 20 | {% if item.CustomImage %} 21 |
24 | {% if item.Path %}{% endif %} 25 |
26 | {{ item.CustomImage.IconName }} 27 |
28 | {% if item.Path %}
{% endif %} 29 | 30 |
31 | {% endif %} 32 | 33 | {% if item.Icon %} 34 | {% if item.Path %} 35 | 36 | {% endif %} 37 |
38 | {% if item.Icon.IconName %} 39 |
40 | {{ item.Icon.IconName }} 41 |
42 | {% endif %} 43 | {% if item.Icon.Path %} 44 | 45 | {% endif %} 46 |
47 | {% if item.Path %} 48 |
49 | {% endif %} 50 | {% endif %} 51 | 52 | 53 | {% if item.ImagePath %} 54 |
55 | 56 |
57 | {% endif %} 58 | 59 | 60 | {% if item.Description %} 61 |
62 | {% if item.Heading %} 63 | {% if item.Path %} 64 | 65 | {% endif %} 66 |

{{item.Heading|safe}}

67 | {% if item.Path %} 68 |
69 | {% endif %} 70 | {% endif %} 71 |
72 | {{ item.Description|safe }} 73 |
74 | 75 | {% if item.Buttons %} 76 |
77 | {% for button in item.Buttons %} 78 | 80 | {{ button.Label }} 81 | 82 | {% endfor %} 83 |
84 | {% endif %} 85 | 86 |
87 | {% endif %} 88 |
89 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/landing-page-content.html: -------------------------------------------------------------------------------- 1 | {% for row in rows %} 2 |
5 | {% if row.Heading %} 6 |
7 |
8 |

{{ row.Heading|safe }}

9 | {% if row.Description %} 10 |

11 | {{ row.Description|safe }} 12 |

13 | {% endif %} 14 |
15 |
16 | {% endif %} 17 | {% if row.CustomHTML %} 18 | {{ row.CustomHTML|safe }} 19 | {% else %} 20 |
21 | {% if row.Columns %} 22 | {% for col in row.Columns %} 23 |
24 | {% for item in col.Items %} 25 | {% include "landing-page-content-row.html" %} 26 | {% endfor %} 27 |
28 | {% endfor %} 29 | {% else %} 30 | {% for item in row.Items %} 31 | {% include "landing-page-content-row.html" %} 32 | {% endfor %} 33 | {% endif %} 34 |
35 | {% endif %} 36 |
37 | {% endfor %} 38 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/lower-tabs.html: -------------------------------------------------------------------------------- 1 | {% if lowerTabs and lowerTabs|length > 1 %} 2 |
3 | 18 |
19 | {% endif %} 20 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/page-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for meta in metaTags %} 4 | 5 | {% endfor %} 6 | 7 | 8 | 9 | {{ pageTitle }} 10 | {% if customCSSPath %} 11 | 12 | {% endif %} 13 | 14 | 15 | {% if customJSPath %} 16 | 17 | {% endif %} 18 | {% if projectYaml.SocialMedia.Image.Path %} 19 | 20 | {% if projectYaml.SocialMedia.Image.Height %}{% endif %} 21 | {% if projectYaml.SocialMedia.Image.Width %}{% endif %} 22 | {% endif %} 23 | 24 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/page-nav-responsive-sidebar.html: -------------------------------------------------------------------------------- 1 | {% if renderedLeftNav %} 2 | 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/page-nav.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/page-nav.html.bak: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/ratings-container.html: -------------------------------------------------------------------------------- 1 | 40 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/searchbar.html: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/section-nav.html: -------------------------------------------------------------------------------- 1 | {% if renderedLeftNav %} 2 | 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/survey.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /third_party/easfs/templates/includes/upper-tabs.html: -------------------------------------------------------------------------------- 1 |
2 | 17 |
18 | -------------------------------------------------------------------------------- /third_party/easfs/templates/page-article.html: -------------------------------------------------------------------------------- 1 | {% extends "page-base.html" %} 2 | 3 | {% block content %} 4 | {% if full_width %} 5 | {#% include "includes/ratings-container.html" %#} 6 |
7 | {% autoescape off %} 8 | {{ content }} 9 | {% endautoescape %} 10 |
11 | {% else %} 12 | {% include "includes/section-nav.html" %} 13 | {% include "includes/page-nav.html" %} 14 |
15 |
16 | {#% include "includes/ratings-container.html" %#} 17 |
18 | {% autoescape off %} 19 | {{ content }} 20 | {% endautoescape %} 21 |
22 | {% include "includes/article-license.html" %} 23 |
24 |
25 | {% endif %} 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /third_party/easfs/templates/page-base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "includes/page-head.html" %} 5 | 6 | 11 |
12 |
13 | 17 |
18 |
19 |
20 | {% include "includes/devsite-nav-responsive.html" %} 21 | 22 |
23 | {% include "includes/dogfood-banner.html" %} 24 | {% include "includes/announcement-banner.html" %} 25 | {% block content %} 26 | {% endblock %} 27 |
28 | {% include "includes/devsite-footer-promos.html" %} 29 | {% include "includes/devsite-footer-linkboxes.html" %} 30 | {% include "includes/devsite-footer-banner.html" %} 31 |
32 |
33 | 34 | 35 | 37 | 39 | 40 | 54 | {% if isProd %} 55 | {% include "includes/analytics.html" %} 56 | {% include "includes/survey.html" %} 57 | {% endif %} 58 | 59 | 60 | -------------------------------------------------------------------------------- /third_party/easfs/templates/page-landing.html: -------------------------------------------------------------------------------- 1 | {% extends "page-base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 | {% include "includes/landing-page-content.html" %} 7 |
8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /third_party/eddsa/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["ed25519.go"], 6 | importpath = "github.com/adamyi/CTFProxy/third_party/eddsa", 7 | visibility = ["//visibility:public"], 8 | deps = ["@com_github_dgrijalva_jwt_go//:go_default_library"], 9 | ) 10 | -------------------------------------------------------------------------------- /third_party/eddsa/ed25519.go: -------------------------------------------------------------------------------- 1 | package eddsa 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/x509" 6 | "encoding/pem" 7 | 8 | "github.com/dgrijalva/jwt-go" 9 | ) 10 | 11 | type SigningMethodEd25519 struct{} 12 | 13 | var ( 14 | SigningMethodEdDSA *SigningMethodEd25519 15 | ) 16 | 17 | func init() { 18 | SigningMethodEdDSA = &SigningMethodEd25519{} 19 | jwt.RegisterSigningMethod(SigningMethodEdDSA.Alg(), func() jwt.SigningMethod { 20 | return SigningMethodEdDSA 21 | }) 22 | } 23 | 24 | func (m *SigningMethodEd25519) Alg() string { 25 | return "EdDSA" 26 | } 27 | 28 | func (m *SigningMethodEd25519) Verify(signingString, signature string, key interface{}) error { 29 | var err error 30 | var ed25519Key *ed25519.PublicKey 31 | var ok bool 32 | 33 | if ed25519Key, ok = key.(*ed25519.PublicKey); !ok { 34 | return jwt.ErrInvalidKeyType 35 | } 36 | 37 | if len(*ed25519Key) != ed25519.PublicKeySize { 38 | return jwt.ErrInvalidKey 39 | } 40 | 41 | var sig []byte 42 | if sig, err = jwt.DecodeSegment(signature); err != nil { 43 | return err 44 | } 45 | 46 | if !ed25519.Verify(*ed25519Key, []byte(signingString), sig) { 47 | return jwt.ErrSignatureInvalid 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func (m *SigningMethodEd25519) Sign(signingString string, key interface{}) (string, error) { 54 | var ed25519Key *ed25519.PrivateKey 55 | var ok bool 56 | 57 | if ed25519Key, ok = key.(*ed25519.PrivateKey); !ok { 58 | return "", jwt.ErrInvalidKeyType 59 | } 60 | 61 | // ed25519.Sign panics if private key not equal to ed25519.PrivateKeySize 62 | // this allows to avoid recover usage 63 | if len(*ed25519Key) != ed25519.PrivateKeySize { 64 | return "", jwt.ErrInvalidKey 65 | } 66 | 67 | sig := ed25519.Sign(*ed25519Key, []byte(signingString)) 68 | return jwt.EncodeSegment(sig), nil 69 | } 70 | 71 | func ParseEdPublicKeyFromPEM(key []byte) (*ed25519.PublicKey, error) { 72 | var block *pem.Block 73 | if block, _ = pem.Decode(key); block == nil { 74 | return nil, jwt.ErrKeyMustBePEMEncoded 75 | } 76 | pub, err := x509.ParsePKIXPublicKey(block.Bytes) 77 | if err != nil { 78 | return nil, err 79 | } 80 | epub, ok := pub.(ed25519.PublicKey) 81 | if !ok { 82 | return nil, jwt.ErrInvalidKeyType 83 | } 84 | return &epub, nil 85 | } 86 | 87 | func ParseEdPrivateKeyFromPEM(key []byte) (*ed25519.PrivateKey, error) { 88 | var block *pem.Block 89 | if block, _ = pem.Decode(key); block == nil { 90 | return nil, jwt.ErrKeyMustBePEMEncoded 91 | } 92 | priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) 93 | if err != nil { 94 | return nil, err 95 | } 96 | epriv, ok := priv.(ed25519.PrivateKey) 97 | if !ok { 98 | return nil, jwt.ErrInvalidKeyType 99 | } 100 | return &epriv, nil 101 | } 102 | -------------------------------------------------------------------------------- /third_party/python/html/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_python//python:defs.bzl", "py_library") 2 | 3 | py_library( 4 | name = "html", 5 | srcs = ["__init__.py"], 6 | visibility = ["//visibility:public"], 7 | ) 8 | -------------------------------------------------------------------------------- /third_party/python/html/__init__.py: -------------------------------------------------------------------------------- 1 | # NOTES(adamyi@): 2 | # port python 3.8 html escape to python 2 cuz it's such a pain to set up python 3 3 | # from https://github.com/python/cpython/blob/3.8/Lib/html/__init__.py 4 | def escape(s, quote=True): 5 | """ 6 | Replace special characters "&", "<" and ">" to HTML-safe sequences. 7 | If the optional flag quote is true (the default), the quotation mark 8 | characters, both double quote (") and single quote (') characters are also 9 | translated. 10 | """ 11 | s = s.replace("&", "&") # Must be done first! 12 | s = s.replace("<", "<") 13 | s = s.replace(">", ">") 14 | if quote: 15 | s = s.replace('"', """) 16 | s = s.replace('\'', "'") 17 | return s 18 | -------------------------------------------------------------------------------- /tools/BUILD: -------------------------------------------------------------------------------- 1 | exports_files(["challengeslist.bzl"]) 2 | 3 | exports_files(["challenge.bzl"]) 4 | 5 | exports_files(["sffe.bzl"]) 6 | 7 | exports_files(["generatechallengeslist.py"]) 8 | 9 | exports_files(["nogoconfig.json"]) 10 | -------------------------------------------------------------------------------- /tools/PRESUBMIT.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | if [[ $(bazel run @go_sdk//:bin/gofmt -- -s -d .) ]]; then 4 | echo "ERROR: go code not formatted, please run \`bazel run @go_sdk//:bin/gofmt -- -s -w -l .\`" 5 | exit 1 6 | fi 7 | tools/format_jsonnet_check.sh || (echo "ERROR: jsonnet files not formatted, please run \`tools/format_jsonnet.sh\`" >&2; exit 1) 8 | yapf --recursive --parallel --quiet . || (echo "ERROR: py files not formatted, please run \`yapf --in-place --recursive --parallel .\`" >&2; exit 1) 9 | bazel run //:gazelle -- --mode=diff || (echo "ERROR: Bazel files out-of-date, please run \`bazel run //:gazelle\`" >&2; exit 1) 10 | bazel run //tools/ctflark -- -mode=check || (echo "ERROR: Bazel files out-of-date, please run \`bazel run //tools/ctflark\`" >&2; exit 1) 11 | bazel run //:buildifier_check || (echo "ERROR: Bazel files not formatted, please run \`bazel run //:buildifier\`" >&2; exit 1) 12 | bazel build //infra/jsonnet:route53 || (echo "ERROR: Bazel build route53 failed" >&2; exit 1) 13 | bazel build //infra/jsonnet:all-docker-compose || (echo "ERROR: Bazel build docker-compose_all failed" >&2; exit 1) 14 | bazel build //infra/jsonnet:cluster-master-docker-compose || (echo "ERROR: Bazel build docker-compose_master failed" >&2; exit 1) 15 | bazel build //infra/jsonnet:cluster-team-docker-compose || (echo "ERROR: Bazel build docker-compose_team failed" >&2; exit 1) 16 | bazel build //infra/jsonnet:k8s || (echo "ERROR: Bazel build k8s yaml failed" >&2; exit 1) 17 | bazel build //:all_containers || (echo "ERROR: Bazel build all_containers failed" >&2; exit 1) 18 | -------------------------------------------------------------------------------- /tools/challenge.bzl: -------------------------------------------------------------------------------- 1 | """ 2 | register all config of a ctf challenge 3 | author: adamyi 4 | """ 5 | 6 | load("@io_bazel_rules_jsonnet//jsonnet:jsonnet.bzl", "jsonnet_library", "jsonnet_to_json") 7 | 8 | def ctf_challenge(): 9 | jsonnet_library( 10 | name = "challenge", 11 | srcs = [ 12 | "challenge.libsonnet", 13 | ], 14 | visibility = ["//challenges:__pkg__", "//infra:__pkg__"], 15 | ) 16 | jsonnet_to_json( 17 | name = "clisffe", 18 | src = "//infra/jsonnet:cli-static-sffe.jsonnet", 19 | outs = ["clisffe.json"], 20 | tla_code_files = { 21 | "challenge.libsonnet": "challenge", 22 | }, 23 | ) 24 | -------------------------------------------------------------------------------- /tools/challenges_list.bzl: -------------------------------------------------------------------------------- 1 | """ 2 | a list of challenge 3 | author: adamyi 4 | """ 5 | 6 | load("@io_bazel_rules_jsonnet//jsonnet:jsonnet.bzl", "jsonnet_library") 7 | 8 | def challenges_list(name, deps, visibility): 9 | """a list of challenges 10 | 11 | Args: 12 | name: name 13 | deps: dependencies 14 | visibility: visibility 15 | """ 16 | cmd = "./$(location //tools:generatechallengeslist.py)" 17 | for src in deps: 18 | cmd += " " + src 19 | cmd += "> \"$@\"" 20 | 21 | native.genrule( 22 | name = name + "_file", 23 | outs = ["challenges.libsonnet"], 24 | srcs = deps, 25 | tools = ["//tools:generatechallengeslist.py"], 26 | cmd = cmd, 27 | ) 28 | 29 | jsonnet_library( 30 | name = name, 31 | srcs = [ 32 | ":" + name + "_file", 33 | ], 34 | deps = deps, 35 | visibility = visibility, 36 | ) 37 | -------------------------------------------------------------------------------- /tools/ctflark/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 2 | load("//:config.bzl", "CONTAINER_REGISTRY") 3 | 4 | go_library( 5 | name = "go_default_library", 6 | srcs = ["main.go"], 7 | importpath = "github.com/adamyi/CTFProxy/tools/ctflark", 8 | visibility = ["//visibility:private"], 9 | x_defs = { 10 | "GCR_PREFIX": CONTAINER_REGISTRY + "/", 11 | }, 12 | deps = [ 13 | "@com_github_bazelbuild_buildtools//build:go_default_library", 14 | "@com_github_bmatcuk_doublestar//:go_default_library", 15 | ], 16 | ) 17 | 18 | go_binary( 19 | name = "ctflark", 20 | embed = [":go_default_library"], 21 | visibility = ["//visibility:public"], 22 | ) 23 | -------------------------------------------------------------------------------- /tools/format.sh: -------------------------------------------------------------------------------- 1 | bazel run @go_sdk//:bin/gofmt -- -s -w -l . 2 | tools/format_jsonnet.sh 3 | yapf --in-place --recursive --parallel . 4 | bazel run //:gazelle 5 | bazel run //tools/ctflark 6 | bazel run //:buildifier 7 | -------------------------------------------------------------------------------- /tools/format_jsonnet.sh: -------------------------------------------------------------------------------- 1 | for i in $(find -name \*.jsonnet -or -name \*.libsonnet); do 2 | echo $i 3 | bazel run --ui_event_filters=-INFO --noshow_progress @jsonnet_go//cmd/jsonnetfmt -- -i $PWD/$i 4 | done 5 | -------------------------------------------------------------------------------- /tools/format_jsonnet_check.sh: -------------------------------------------------------------------------------- 1 | for i in $(find -name \*.jsonnet -or -name \*.libsonnet); do 2 | bazel run --ui_event_filters=-INFO --noshow_progress @jsonnet_go//cmd/jsonnetfmt -- $PWD/$i > format_check_tmp.jsonnet 3 | if cmp -s $i format_check_tmp.jsonnet; then 4 | echo OK $i 5 | else 6 | echo NOT OK $i 7 | rm format_check_tmp.jsonnet 8 | exit 1 9 | fi 10 | done 11 | rm format_check_tmp.jsonnet 12 | -------------------------------------------------------------------------------- /tools/gcr_delete_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright [2018] [lahsivjar] 4 | # Copyright [2019] [adamyi] 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | set -eou pipefail 19 | 20 | USAGE="$(basename "$0") [-h] [-r] -- Delete all untagged images in a project for GCR repository 21 | where: 22 | -h get help (quite a paradox :D) 23 | -r GCR repository, for example: gcr.io/" 24 | 25 | delete_untagged() { 26 | echo " |-Deleting untagged images for $1" 27 | while read digest; do 28 | gcloud container images delete $1@$digest --force-delete-tags --quiet 2>&1 | sed 's/^/ /' 29 | done < <(gcloud container images list-tags $1 --format='get(digest)' --limit=unlimited) 30 | } 31 | 32 | delete_for_each_repo() { 33 | echo "|-Will delete all untagged images in $1" 34 | while read repo; do 35 | delete_untagged $repo 36 | delete_for_each_repo $repo 37 | done < <(gcloud container images list --repository $1 --format="value(name)") 38 | } 39 | 40 | while getopts ':hr:' option; do 41 | case "$option" in 42 | h) echo "$USAGE" 43 | exit 44 | ;; 45 | r) REPOSITORY=$OPTARG 46 | ;; 47 | *) printf "invalid usage please provide the repository name with -r flag\n" >&2 48 | echo "$USAGE" 49 | exit 1 50 | ;; 51 | esac 52 | done 53 | shift $((OPTIND-1)) 54 | 55 | if [ -z ${REPOSITORY-} ]; then 56 | printf "invalid usage please provide the repository name with -r flag\n" >&2 57 | echo "$USAGE"; 58 | exit 1; 59 | fi 60 | 61 | delete_for_each_repo $REPOSITORY 62 | -------------------------------------------------------------------------------- /tools/gcr_delete_untagged.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright [2018] [lahsivjar] 4 | # Copyright [2019] [adamyi] 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | 18 | set -eou pipefail 19 | 20 | USAGE="$(basename "$0") [-h] [-r] -- Delete all untagged images in a project for GCR repository 21 | where: 22 | -h get help (quite a paradox :D) 23 | -r GCR repository, for example: gcr.io/" 24 | 25 | delete_untagged() { 26 | echo " |-Deleting untagged images for $1" 27 | while read digest; do 28 | gcloud container images delete $1@$digest --quiet 2>&1 | sed 's/^/ /' 29 | done < <(gcloud container images list-tags $1 --filter='-tags:*' --format='get(digest)' --limit=unlimited) 30 | } 31 | 32 | delete_for_each_repo() { 33 | echo "|-Will delete all untagged images in $1" 34 | while read repo; do 35 | delete_untagged $repo 36 | delete_for_each_repo $repo 37 | done < <(gcloud container images list --repository $1 --format="value(name)") 38 | } 39 | 40 | while getopts ':hr:' option; do 41 | case "$option" in 42 | h) echo "$USAGE" 43 | exit 44 | ;; 45 | r) REPOSITORY=$OPTARG 46 | ;; 47 | *) printf "invalid usage please provide the repository name with -r flag\n" >&2 48 | echo "$USAGE" 49 | exit 1 50 | ;; 51 | esac 52 | done 53 | shift $((OPTIND-1)) 54 | 55 | if [ -z ${REPOSITORY-} ]; then 56 | printf "invalid usage please provide the repository name with -r flag\n" >&2 57 | echo "$USAGE"; 58 | exit 1; 59 | fi 60 | 61 | delete_for_each_repo $REPOSITORY 62 | -------------------------------------------------------------------------------- /tools/generatechallengeslist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import string 5 | 6 | print("# THIS FILE IS AUTO-GENERATED") 7 | 8 | chals = [] 9 | 10 | for f in sys.argv[1:]: 11 | p = f.split(":")[0][2:] 12 | cn = p.split("/")[-1].replace('-', '_') 13 | chals.append(cn) 14 | print("local %s = import '%s/challenge.libsonnet';" % (cn, p)) 15 | 16 | print("[%s]" % string.join(chals, ", ")) 17 | -------------------------------------------------------------------------------- /tools/lint_jsonnet.sh: -------------------------------------------------------------------------------- 1 | for i in $(find -name \*.jsonnet -or -name \*.libsonnet); do 2 | bazel run --ui_event_filters=-INFO --noshow_progress @jsonnet_go//linter/jsonnet-lint $PWD/$i 3 | done 4 | -------------------------------------------------------------------------------- /tools/nogoconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "shadow": { 3 | "exclude_files": { 4 | "external/.*": "enforce this only on first-party code", 5 | "infra/ctfproxy/config.go": "err shadow permitted", 6 | "infra/ctfproxy/statusz.go": "err shadow permitted", 7 | "infra/ctfproxy/websocket.go": "err shadow permitted" 8 | } 9 | }, 10 | "composites": { 11 | "exclude_files": { 12 | "external/.*": "enforce this only on first-party code" 13 | } 14 | }, 15 | "nilness": { 16 | "exclude_files": { 17 | "external/.*": "enforce this only on first-party code" 18 | } 19 | }, 20 | "unreachable": { 21 | "exclude_files": { 22 | "external/.*": "enforce this only on first-party code" 23 | } 24 | }, 25 | "copylocks": { 26 | "exclude_files": { 27 | "external/.*": "enforce this only on first-party code" 28 | } 29 | }, 30 | "stringintconv": { 31 | "exclude_files": { 32 | "external/.*": "enforce this only on first-party code" 33 | } 34 | }, 35 | "assign": { 36 | "exclude_files": { 37 | "external/.*": "enforce this only on first-party code" 38 | } 39 | }, 40 | "deepequalerrors": { 41 | "exclude_files": { 42 | "external/.*": "enforce this only on first-party code" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tools/rsync.sh: -------------------------------------------------------------------------------- 1 | rsync -a data/ /data/ 2 | -------------------------------------------------------------------------------- /tools/sffe.bzl: -------------------------------------------------------------------------------- 1 | """ 2 | group sffe files 3 | author: adamyi 4 | """ 5 | 6 | def sffe_files(files, name = "sffe"): 7 | native.filegroup( 8 | name = name, 9 | srcs = files, 10 | visibility = ["//infra/sffe:__pkg__"], 11 | ) 12 | -------------------------------------------------------------------------------- /tools/tarscript.bzl: -------------------------------------------------------------------------------- 1 | """ 2 | run a script with argv[1] as output, and tar the output 3 | author: adamyi 4 | """ 5 | 6 | def tarscript(name, src): 7 | cmd = "rm -rf $(@D)/%s; mkdir $(@D)/%s; bash -c \"$(location %s) $(@D)/%s\"; tar -C $(@D)/%s -cf $(@D)/%s.tar .; rm -rf $(@D)/%s" % (name, name, src, name, name, name, name) 8 | native.genrule( 9 | name = name, 10 | outs = ["%s.tar" % name], 11 | srcs = [src], 12 | cmd = cmd, 13 | ) 14 | -------------------------------------------------------------------------------- /tools/unsed_go_repos.sh: -------------------------------------------------------------------------------- 1 | # Get the unique dep names in WORKSPACE 2 | awk '/^go_repository/ {gsub("\"",""); gsub(",",""); print}' FS="\n" RS="" WORKSPACE | \ 3 | awk '/name/ {print $3}' | \ 4 | sort | uniq > /tmp/workspace.deps 5 | 6 | # Get the deps in use 7 | bazel query 'kind(go_library, deps(//...)) -//...' | 8 | awk '{split($0, part, "//");gsub("@","", part[1]); print part[1]}' | \ 9 | sort | uniq > /tmp/in-use.deps 10 | 11 | # Find go_repositories to delete 12 | grep -f /tmp/in-use.deps -v /tmp/workspace.deps 13 | -------------------------------------------------------------------------------- /tools/workspace-status.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script will be run by bazel when the build process starts to 4 | # generate key-value information that represents the status of the 5 | # workspace. The output should be like 6 | # 7 | # KEY1 VALUE1 8 | # KEY2 VALUE2 9 | # 10 | # If the script exits with non-zero code, it's considered as a failure 11 | # and the output will be discarded. 12 | 13 | function rev() { 14 | cd $1; git describe --always --match "v[0-9].*" --dirty 15 | } 16 | 17 | echo STABLE_GIT_COMMIT $(git rev-parse HEAD) 18 | echo STABLE_GIT_VERSION $(rev .) 19 | echo STABLE_BUILDER $(id -un)@$(hostname -f):$(pwd) 20 | 21 | --------------------------------------------------------------------------------