gcs-static-site-proxy 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gcs_static_site_proxy-0.1.0/.github/workflows/ci.yml +46 -0
- gcs_static_site_proxy-0.1.0/.github/workflows/release.yml +42 -0
- gcs_static_site_proxy-0.1.0/.gitignore +6 -0
- gcs_static_site_proxy-0.1.0/LICENSE +21 -0
- gcs_static_site_proxy-0.1.0/PKG-INFO +217 -0
- gcs_static_site_proxy-0.1.0/README.md +188 -0
- gcs_static_site_proxy-0.1.0/pyproject.toml +64 -0
- gcs_static_site_proxy-0.1.0/src/gcs_static_site_proxy/__init__.py +10 -0
- gcs_static_site_proxy-0.1.0/src/gcs_static_site_proxy/__main__.py +4 -0
- gcs_static_site_proxy-0.1.0/src/gcs_static_site_proxy/auth.py +245 -0
- gcs_static_site_proxy-0.1.0/src/gcs_static_site_proxy/catalog.py +131 -0
- gcs_static_site_proxy-0.1.0/src/gcs_static_site_proxy/cli.py +473 -0
- gcs_static_site_proxy-0.1.0/src/gcs_static_site_proxy/gcs.py +123 -0
- gcs_static_site_proxy-0.1.0/src/gcs_static_site_proxy/server.py +674 -0
- gcs_static_site_proxy-0.1.0/src/gcs_static_site_proxy/site_config.py +104 -0
- gcs_static_site_proxy-0.1.0/tests/test_auth.py +145 -0
- gcs_static_site_proxy-0.1.0/tests/test_catalog.py +80 -0
- gcs_static_site_proxy-0.1.0/tests/test_cli.py +160 -0
- gcs_static_site_proxy-0.1.0/tests/test_proxy_integration.py +308 -0
- gcs_static_site_proxy-0.1.0/tests/test_server_helpers.py +240 -0
- gcs_static_site_proxy-0.1.0/tests/test_site_config.py +70 -0
- gcs_static_site_proxy-0.1.0/uv.lock +1158 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
checks:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
strategy:
|
|
16
|
+
matrix:
|
|
17
|
+
python-version: ["3.11", "3.14"]
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Check out repository
|
|
21
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
22
|
+
|
|
23
|
+
- name: Install uv and Python
|
|
24
|
+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
25
|
+
with:
|
|
26
|
+
version: "0.12.4"
|
|
27
|
+
python-version: ${{ matrix.python-version }}
|
|
28
|
+
enable-cache: true
|
|
29
|
+
|
|
30
|
+
- name: Install project
|
|
31
|
+
run: uv sync --locked --dev
|
|
32
|
+
|
|
33
|
+
- name: Test
|
|
34
|
+
run: uv run pytest -q
|
|
35
|
+
|
|
36
|
+
- name: Lint
|
|
37
|
+
run: uv run ruff check .
|
|
38
|
+
|
|
39
|
+
- name: Check formatting
|
|
40
|
+
run: uv run ruff format --check .
|
|
41
|
+
|
|
42
|
+
- name: Type-check
|
|
43
|
+
run: uv run ty check src tests
|
|
44
|
+
|
|
45
|
+
- name: Build distributions
|
|
46
|
+
run: uv build --no-sources
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Publish release to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
environment: pypi
|
|
14
|
+
permissions:
|
|
15
|
+
id-token: write
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- name: Check out repository
|
|
20
|
+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
21
|
+
|
|
22
|
+
- name: Install uv and Python
|
|
23
|
+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
|
24
|
+
with:
|
|
25
|
+
version: "0.12.4"
|
|
26
|
+
python-version: "3.14"
|
|
27
|
+
|
|
28
|
+
- name: Build distributions
|
|
29
|
+
run: uv build --no-sources
|
|
30
|
+
|
|
31
|
+
- name: Smoke-test wheel
|
|
32
|
+
run: >-
|
|
33
|
+
uv run --isolated --no-project --with ./dist/*.whl
|
|
34
|
+
gcs-static-site-proxy --version
|
|
35
|
+
|
|
36
|
+
- name: Smoke-test source distribution
|
|
37
|
+
run: >-
|
|
38
|
+
uv run --isolated --no-project --with ./dist/*.tar.gz
|
|
39
|
+
gcs-static-site-proxy --version
|
|
40
|
+
|
|
41
|
+
- name: Publish to PyPI
|
|
42
|
+
run: uv publish
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexei Ciobanu
|
|
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.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: gcs-static-site-proxy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Serve private GCS static sites securely through a local proxy
|
|
5
|
+
Project-URL: Documentation, https://github.com/alexei-ciobanu/gcs-static-site-proxy#readme
|
|
6
|
+
Project-URL: Issues, https://github.com/alexei-ciobanu/gcs-static-site-proxy/issues
|
|
7
|
+
Project-URL: Source, https://github.com/alexei-ciobanu/gcs-static-site-proxy
|
|
8
|
+
Author: Alexei Ciobanu
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: gcs,google-cloud,proxy,static-site
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: aiohttp<4,>=3.11
|
|
26
|
+
Requires-Dist: google-auth[requests]<3,>=2.38
|
|
27
|
+
Requires-Dist: truststore<1,>=0.10
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# gcs-static-site-proxy
|
|
31
|
+
|
|
32
|
+
Serve one private Google Cloud Storage static-site prefix, or an explicit
|
|
33
|
+
catalog of prefixes, through a renewable, read-only local proxy. The GCS bucket
|
|
34
|
+
remains private: the proxy uses the operator's Application Default Credentials
|
|
35
|
+
(ADC) and exposes only explicitly configured object prefixes.
|
|
36
|
+
|
|
37
|
+
> **Status:** alpha. Review the publication-prefix and network-access security
|
|
38
|
+
> model before using the proxy with protected data.
|
|
39
|
+
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- Python 3.11 or newer
|
|
43
|
+
- [`uv`](https://docs.astral.sh/uv/) for the recommended isolated invocation
|
|
44
|
+
- Google Cloud CLI for initializing or refreshing ADC
|
|
45
|
+
- `storage.objects.get` on the configured objects, normally through
|
|
46
|
+
`roles/storage.objectViewer` or a stronger role
|
|
47
|
+
|
|
48
|
+
Initialize ADC:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
gcloud auth login --update-adc
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Install and run on localhost
|
|
55
|
+
|
|
56
|
+
Run the published package in an isolated environment:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uvx gcs-static-site-proxy@0.1.0 \
|
|
60
|
+
--bucket private-site-bucket \
|
|
61
|
+
--prefix team/sites/my-site
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Alternatively, install it with `pip` and use either the command or Python
|
|
65
|
+
module entry point:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python -m pip install gcs-static-site-proxy==0.1.0
|
|
69
|
+
python -m gcs_static_site_proxy \
|
|
70
|
+
--bucket private-site-bucket \
|
|
71
|
+
--prefix team/sites/my-site
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Run directly from a pinned public Git commit during development:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uvx --from 'git+https://github.com/alexei-ciobanu/gcs-static-site-proxy@COMMIT_SHA' \
|
|
78
|
+
gcs-static-site-proxy \
|
|
79
|
+
--bucket private-site-bucket \
|
|
80
|
+
--prefix team/sites/my-site
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The default listener is `127.0.0.1:8080`. It accepts only `GET` and `HEAD`,
|
|
84
|
+
rejects path traversal, and does not provide a general GCS proxy.
|
|
85
|
+
Outbound authentication and GCS requests use the operating system trust store,
|
|
86
|
+
including root certificate authorities installed through device management.
|
|
87
|
+
|
|
88
|
+
## Multiple sites and a landing page
|
|
89
|
+
|
|
90
|
+
Catalog mode serves a landing-page prefix at `/` and mounts explicitly
|
|
91
|
+
allowlisted site prefixes under `/sites/<slug>/`:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
uvx gcs-static-site-proxy@0.1.0 \
|
|
95
|
+
--bucket private-site-bucket \
|
|
96
|
+
--catalog-prefix team/publication/static-sites
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The catalog prefix must contain this exact object:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
team/publication/static-sites/.gcs-static-site-proxy-sites.json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"version": 1,
|
|
110
|
+
"sites": [
|
|
111
|
+
{
|
|
112
|
+
"slug": "service-review",
|
|
113
|
+
"title": "Service review",
|
|
114
|
+
"prefix": "team/projects/service-review/publication/site"
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The landing page remains under the catalog prefix. Each mount maps only to its
|
|
121
|
+
declared prefix; unknown slugs fail closed, and neither catalog nor site
|
|
122
|
+
configuration objects are browser-accessible. Catalog editors can expose any
|
|
123
|
+
prefix that their proxy operator can read, so treat the catalog as a browser
|
|
124
|
+
publication allowlist and never mount a protected artifact prefix.
|
|
125
|
+
|
|
126
|
+
The proxy loads and validates the catalog and every site's CSP at startup. The
|
|
127
|
+
landing page uses the catalog prefix's `.gcs-static-site-proxy.json`; each
|
|
128
|
+
mounted site uses the same well-known filename under its own prefix. Explicit
|
|
129
|
+
CLI CSP overrides apply to all mounts.
|
|
130
|
+
|
|
131
|
+
Path-mounted sites share a browser origin. Per-response CSPs remain distinct,
|
|
132
|
+
but a script running in one mounted site can make same-origin requests to other
|
|
133
|
+
mounted sites. Use one catalog only for sites approved for the same audience;
|
|
134
|
+
use separate origins when stronger isolation is required.
|
|
135
|
+
|
|
136
|
+
## Site security configuration
|
|
137
|
+
|
|
138
|
+
Unless overridden, the proxy requests this exact object at startup:
|
|
139
|
+
|
|
140
|
+
```text
|
|
141
|
+
<prefix>/.gcs-static-site-proxy.json
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"version": 1,
|
|
149
|
+
"contentSecurityPolicy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
If it is absent, a strict built-in policy is used. An invalid or inaccessible
|
|
154
|
+
configuration fails startup. The active source, generation and SHA-256 are
|
|
155
|
+
printed.
|
|
156
|
+
|
|
157
|
+
Without an explicit CLI mode, each prefix uses its own GCS configuration and
|
|
158
|
+
falls back to the strict built-in policy when that object is absent. The
|
|
159
|
+
mutually exclusive global modes are:
|
|
160
|
+
|
|
161
|
+
- `--csp-override-file PATH` applies one exact local policy to every route;
|
|
162
|
+
- `--strict-csp` applies the strict built-in policy to every route; and
|
|
163
|
+
- `--no-csp` disables CSP on every route.
|
|
164
|
+
|
|
165
|
+
In catalog mode these options affect the landing page and every mounted site.
|
|
166
|
+
For a local override of only one site, run that prefix separately in
|
|
167
|
+
single-site mode.
|
|
168
|
+
|
|
169
|
+
`--no-csp` disables only CSP. The proxy continues to send `no-referrer`,
|
|
170
|
+
`nosniff`, and frame-denial headers.
|
|
171
|
+
|
|
172
|
+
## Network access
|
|
173
|
+
|
|
174
|
+
Loopback is the safe default. Explicit non-loopback binding enables network
|
|
175
|
+
mode:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
uvx gcs-static-site-proxy@0.1.0 \
|
|
179
|
+
--bucket private-site-bucket \
|
|
180
|
+
--prefix team/sites/my-site \
|
|
181
|
+
--bind 0.0.0.0
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Network mode generates a process-lifetime access token. Opening the printed
|
|
185
|
+
tokenized URL exchanges it for an HttpOnly, SameSite session cookie. IP-literal
|
|
186
|
+
Host values are accepted when they match the local destination address;
|
|
187
|
+
machine hostnames are detected automatically. Use `--allow-host HOST` for an
|
|
188
|
+
additional DNS alias.
|
|
189
|
+
|
|
190
|
+
The cookie is intentionally a browser-session cookie rather than a persistent
|
|
191
|
+
credential. If the browser is restarted while the proxy remains running, open
|
|
192
|
+
the printed tokenized URL again.
|
|
193
|
+
|
|
194
|
+
Anyone who has the token and can reach the listener can read the site using
|
|
195
|
+
the operator's GCS permissions. Plain HTTP does not protect content from
|
|
196
|
+
network interception. Supply both options for HTTPS:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
--tls-cert certificate.pem --tls-key private-key.pem
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Remote visitors cannot launch the local ADC reauthentication command.
|
|
203
|
+
|
|
204
|
+
## Local development
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
uv sync --dev
|
|
208
|
+
uv run pytest
|
|
209
|
+
uv run ruff check .
|
|
210
|
+
uv run ruff format --check .
|
|
211
|
+
uv run ty check src
|
|
212
|
+
uv build --no-sources
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# gcs-static-site-proxy
|
|
2
|
+
|
|
3
|
+
Serve one private Google Cloud Storage static-site prefix, or an explicit
|
|
4
|
+
catalog of prefixes, through a renewable, read-only local proxy. The GCS bucket
|
|
5
|
+
remains private: the proxy uses the operator's Application Default Credentials
|
|
6
|
+
(ADC) and exposes only explicitly configured object prefixes.
|
|
7
|
+
|
|
8
|
+
> **Status:** alpha. Review the publication-prefix and network-access security
|
|
9
|
+
> model before using the proxy with protected data.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Python 3.11 or newer
|
|
14
|
+
- [`uv`](https://docs.astral.sh/uv/) for the recommended isolated invocation
|
|
15
|
+
- Google Cloud CLI for initializing or refreshing ADC
|
|
16
|
+
- `storage.objects.get` on the configured objects, normally through
|
|
17
|
+
`roles/storage.objectViewer` or a stronger role
|
|
18
|
+
|
|
19
|
+
Initialize ADC:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gcloud auth login --update-adc
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Install and run on localhost
|
|
26
|
+
|
|
27
|
+
Run the published package in an isolated environment:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uvx gcs-static-site-proxy@0.1.0 \
|
|
31
|
+
--bucket private-site-bucket \
|
|
32
|
+
--prefix team/sites/my-site
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Alternatively, install it with `pip` and use either the command or Python
|
|
36
|
+
module entry point:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python -m pip install gcs-static-site-proxy==0.1.0
|
|
40
|
+
python -m gcs_static_site_proxy \
|
|
41
|
+
--bucket private-site-bucket \
|
|
42
|
+
--prefix team/sites/my-site
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Run directly from a pinned public Git commit during development:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uvx --from 'git+https://github.com/alexei-ciobanu/gcs-static-site-proxy@COMMIT_SHA' \
|
|
49
|
+
gcs-static-site-proxy \
|
|
50
|
+
--bucket private-site-bucket \
|
|
51
|
+
--prefix team/sites/my-site
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The default listener is `127.0.0.1:8080`. It accepts only `GET` and `HEAD`,
|
|
55
|
+
rejects path traversal, and does not provide a general GCS proxy.
|
|
56
|
+
Outbound authentication and GCS requests use the operating system trust store,
|
|
57
|
+
including root certificate authorities installed through device management.
|
|
58
|
+
|
|
59
|
+
## Multiple sites and a landing page
|
|
60
|
+
|
|
61
|
+
Catalog mode serves a landing-page prefix at `/` and mounts explicitly
|
|
62
|
+
allowlisted site prefixes under `/sites/<slug>/`:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uvx gcs-static-site-proxy@0.1.0 \
|
|
66
|
+
--bucket private-site-bucket \
|
|
67
|
+
--catalog-prefix team/publication/static-sites
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The catalog prefix must contain this exact object:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
team/publication/static-sites/.gcs-static-site-proxy-sites.json
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"version": 1,
|
|
81
|
+
"sites": [
|
|
82
|
+
{
|
|
83
|
+
"slug": "service-review",
|
|
84
|
+
"title": "Service review",
|
|
85
|
+
"prefix": "team/projects/service-review/publication/site"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The landing page remains under the catalog prefix. Each mount maps only to its
|
|
92
|
+
declared prefix; unknown slugs fail closed, and neither catalog nor site
|
|
93
|
+
configuration objects are browser-accessible. Catalog editors can expose any
|
|
94
|
+
prefix that their proxy operator can read, so treat the catalog as a browser
|
|
95
|
+
publication allowlist and never mount a protected artifact prefix.
|
|
96
|
+
|
|
97
|
+
The proxy loads and validates the catalog and every site's CSP at startup. The
|
|
98
|
+
landing page uses the catalog prefix's `.gcs-static-site-proxy.json`; each
|
|
99
|
+
mounted site uses the same well-known filename under its own prefix. Explicit
|
|
100
|
+
CLI CSP overrides apply to all mounts.
|
|
101
|
+
|
|
102
|
+
Path-mounted sites share a browser origin. Per-response CSPs remain distinct,
|
|
103
|
+
but a script running in one mounted site can make same-origin requests to other
|
|
104
|
+
mounted sites. Use one catalog only for sites approved for the same audience;
|
|
105
|
+
use separate origins when stronger isolation is required.
|
|
106
|
+
|
|
107
|
+
## Site security configuration
|
|
108
|
+
|
|
109
|
+
Unless overridden, the proxy requests this exact object at startup:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
<prefix>/.gcs-static-site-proxy.json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"version": 1,
|
|
120
|
+
"contentSecurityPolicy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
If it is absent, a strict built-in policy is used. An invalid or inaccessible
|
|
125
|
+
configuration fails startup. The active source, generation and SHA-256 are
|
|
126
|
+
printed.
|
|
127
|
+
|
|
128
|
+
Without an explicit CLI mode, each prefix uses its own GCS configuration and
|
|
129
|
+
falls back to the strict built-in policy when that object is absent. The
|
|
130
|
+
mutually exclusive global modes are:
|
|
131
|
+
|
|
132
|
+
- `--csp-override-file PATH` applies one exact local policy to every route;
|
|
133
|
+
- `--strict-csp` applies the strict built-in policy to every route; and
|
|
134
|
+
- `--no-csp` disables CSP on every route.
|
|
135
|
+
|
|
136
|
+
In catalog mode these options affect the landing page and every mounted site.
|
|
137
|
+
For a local override of only one site, run that prefix separately in
|
|
138
|
+
single-site mode.
|
|
139
|
+
|
|
140
|
+
`--no-csp` disables only CSP. The proxy continues to send `no-referrer`,
|
|
141
|
+
`nosniff`, and frame-denial headers.
|
|
142
|
+
|
|
143
|
+
## Network access
|
|
144
|
+
|
|
145
|
+
Loopback is the safe default. Explicit non-loopback binding enables network
|
|
146
|
+
mode:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
uvx gcs-static-site-proxy@0.1.0 \
|
|
150
|
+
--bucket private-site-bucket \
|
|
151
|
+
--prefix team/sites/my-site \
|
|
152
|
+
--bind 0.0.0.0
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Network mode generates a process-lifetime access token. Opening the printed
|
|
156
|
+
tokenized URL exchanges it for an HttpOnly, SameSite session cookie. IP-literal
|
|
157
|
+
Host values are accepted when they match the local destination address;
|
|
158
|
+
machine hostnames are detected automatically. Use `--allow-host HOST` for an
|
|
159
|
+
additional DNS alias.
|
|
160
|
+
|
|
161
|
+
The cookie is intentionally a browser-session cookie rather than a persistent
|
|
162
|
+
credential. If the browser is restarted while the proxy remains running, open
|
|
163
|
+
the printed tokenized URL again.
|
|
164
|
+
|
|
165
|
+
Anyone who has the token and can reach the listener can read the site using
|
|
166
|
+
the operator's GCS permissions. Plain HTTP does not protect content from
|
|
167
|
+
network interception. Supply both options for HTTPS:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
--tls-cert certificate.pem --tls-key private-key.pem
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Remote visitors cannot launch the local ADC reauthentication command.
|
|
174
|
+
|
|
175
|
+
## Local development
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
uv sync --dev
|
|
179
|
+
uv run pytest
|
|
180
|
+
uv run ruff check .
|
|
181
|
+
uv run ruff format --check .
|
|
182
|
+
uv run ty check src
|
|
183
|
+
uv build --no-sources
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gcs-static-site-proxy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Serve private GCS static sites securely through a local proxy"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Alexei Ciobanu" }]
|
|
13
|
+
keywords = ["gcs", "google-cloud", "proxy", "static-site"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: MacOS",
|
|
19
|
+
"Operating System :: Microsoft :: Windows",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"aiohttp>=3.11,<4",
|
|
30
|
+
"google-auth[requests]>=2.38,<3",
|
|
31
|
+
"truststore>=0.10,<1",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Documentation = "https://github.com/alexei-ciobanu/gcs-static-site-proxy#readme"
|
|
36
|
+
Issues = "https://github.com/alexei-ciobanu/gcs-static-site-proxy/issues"
|
|
37
|
+
Source = "https://github.com/alexei-ciobanu/gcs-static-site-proxy"
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
gcs-static-site-proxy = "gcs_static_site_proxy.cli:main"
|
|
41
|
+
|
|
42
|
+
[dependency-groups]
|
|
43
|
+
dev = [
|
|
44
|
+
"pytest>=8.3,<10",
|
|
45
|
+
"pytest-asyncio>=0.25,<2",
|
|
46
|
+
"ruff>=0.11,<1",
|
|
47
|
+
"ty>=0.0.1a20,<1",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.uv]
|
|
51
|
+
exclude-newer = "7 days"
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.wheel]
|
|
54
|
+
packages = ["src/gcs_static_site_proxy"]
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
asyncio_mode = "auto"
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 88
|
|
61
|
+
target-version = "py311"
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
select = ["ASYNC", "B", "E", "F", "I", "RUF", "SIM", "UP"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Renewable local proxy for a private GCS static-site prefix."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("gcs-static-site-proxy")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "0.0.0"
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|