├── .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 |
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 |
4 |
--------------------------------------------------------------------------------
/third_party/easfs/static/images/redesign-14/button-down-grey.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/third_party/easfs/static/images/redesign-14/nav-status-experimental.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
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 |
89 |
--------------------------------------------------------------------------------
/third_party/easfs/templates/includes/landing-page-content.html:
--------------------------------------------------------------------------------
1 | {% for row in rows %}
2 |
5 | {% if row.Heading %}
6 |
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 |
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 |
3 | {{renderedLeftNav|safe}}
4 |
5 | {% endif %}
6 |
--------------------------------------------------------------------------------
/third_party/easfs/templates/includes/page-nav.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/third_party/easfs/templates/includes/page-nav.html.bak:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 | Contents
6 |
7 |
8 | {% autoescape off %}
9 | {{ renderedTOC }}
10 | {% endautoescape %}
11 |
12 |
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 |
3 | {{renderedLeftNav|safe}}
4 |
5 | {% endif %}
6 |
--------------------------------------------------------------------------------
/third_party/easfs/templates/includes/survey.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/third_party/easfs/templates/includes/upper-tabs.html:
--------------------------------------------------------------------------------
1 |
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 |
14 | {% include "includes/devsite-top-logo-row-wrapper-wrapper.html" %}
15 | {% include "includes/devsite-collapsible-section.html" %}
16 |
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 |
--------------------------------------------------------------------------------