@hummingbirdworks/proxy 0.3.0 → 0.3.1
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.
- package/LICENSE +1 -1
- package/bin/hummingbird-proxy.js +14 -9
- package/hummingbird_proxy/addon.py +9 -1
- package/hummingbird_proxy/config.py +5 -0
- package/hummingbird_proxy/serving.py +45 -2
- package/package.json +13 -1
- package/powerapp_dev_proxy.py +0 -2
package/LICENSE
CHANGED
package/bin/hummingbird-proxy.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const { spawn, spawnSync } = require("node:child_process");
|
|
@@ -95,15 +95,20 @@ function main() {
|
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
//
|
|
98
|
+
// Only the first token can be the config path, and only when it isn't a
|
|
99
|
+
// flag; everything else is forwarded to mitmdump so a flag's value (e.g. the
|
|
100
|
+
// `8888` in `-p 8888`) is never mistaken for the config path.
|
|
99
101
|
let configArg;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
let passthrough;
|
|
103
|
+
if (argv.length > 0 && !argv[0].startsWith("-")) {
|
|
104
|
+
configArg = argv[0];
|
|
105
|
+
passthrough = argv.slice(1);
|
|
106
|
+
} else {
|
|
107
|
+
passthrough = argv.slice();
|
|
108
|
+
}
|
|
109
|
+
// Drop a leading `--` separator; the rest already goes to mitmdump.
|
|
110
|
+
if (passthrough[0] === "--") {
|
|
111
|
+
passthrough.shift();
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
const configPath = path.resolve(process.cwd(), configArg || DEFAULT_CONFIG);
|
|
@@ -84,6 +84,7 @@ class DataverseProxy:
|
|
|
84
84
|
item_type,
|
|
85
85
|
item["name"],
|
|
86
86
|
item["_base_dir"],
|
|
87
|
+
root=item["folder"],
|
|
87
88
|
)
|
|
88
89
|
return
|
|
89
90
|
|
|
@@ -95,5 +96,12 @@ class DataverseProxy:
|
|
|
95
96
|
if is_css:
|
|
96
97
|
parts.append("css")
|
|
97
98
|
parts.extend(segment for segment in relative.split("/") if segment)
|
|
98
|
-
serving.serve_file(
|
|
99
|
+
serving.serve_file(
|
|
100
|
+
flow,
|
|
101
|
+
os.path.join(*parts),
|
|
102
|
+
item_type,
|
|
103
|
+
item["name"],
|
|
104
|
+
item["_base_dir"],
|
|
105
|
+
root=item["folder"],
|
|
106
|
+
)
|
|
99
107
|
return
|
|
@@ -124,6 +124,11 @@ def validate_config(config: list[dict]) -> list[dict]:
|
|
|
124
124
|
unknown = item.keys() - (required | _OPTIONAL_KEYS | _INTERNAL_KEYS)
|
|
125
125
|
if unknown:
|
|
126
126
|
raise ValueError(f"{label} (type={item_type!r}): unknown keys {sorted(unknown)}")
|
|
127
|
+
# Prefix matching relies on a trailing slash to mark the name boundary,
|
|
128
|
+
# so normalize it rather than silently mismatching a sibling resource.
|
|
129
|
+
if item_type in ("folder", "devserver") and isinstance(item.get("name"), str):
|
|
130
|
+
if not item["name"].endswith("/"):
|
|
131
|
+
item["name"] += "/"
|
|
127
132
|
if not item.get("disabled", False):
|
|
128
133
|
enabled.append(item)
|
|
129
134
|
return enabled
|
|
@@ -3,11 +3,27 @@
|
|
|
3
3
|
from urllib.parse import urlsplit
|
|
4
4
|
|
|
5
5
|
import mimetypes
|
|
6
|
+
import os
|
|
6
7
|
|
|
7
8
|
from mitmproxy import ctx, http
|
|
8
9
|
|
|
9
10
|
from hummingbird_proxy.config import resolve_path
|
|
10
11
|
|
|
12
|
+
# Explicit types for common web assets; the OS mimetypes DB is unreliable here
|
|
13
|
+
# (e.g. Windows maps .js to text/plain, which browsers refuse to execute).
|
|
14
|
+
_CONTENT_TYPES = {
|
|
15
|
+
".js": "text/javascript",
|
|
16
|
+
".mjs": "text/javascript",
|
|
17
|
+
".cjs": "text/javascript",
|
|
18
|
+
".css": "text/css",
|
|
19
|
+
".html": "text/html",
|
|
20
|
+
".htm": "text/html",
|
|
21
|
+
".json": "application/json",
|
|
22
|
+
".map": "application/json",
|
|
23
|
+
".svg": "image/svg+xml",
|
|
24
|
+
".wasm": "application/wasm",
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
|
|
12
28
|
def _log_redirect(flow: http.HTTPFlow, rule_type: str, rule_name: str, destination: str) -> None:
|
|
13
29
|
ctx.log.info(
|
|
@@ -15,8 +31,32 @@ def _log_redirect(flow: http.HTTPFlow, rule_type: str, rule_name: str, destinati
|
|
|
15
31
|
)
|
|
16
32
|
|
|
17
33
|
|
|
18
|
-
def
|
|
34
|
+
def _is_within(root: str, path: str) -> bool:
|
|
35
|
+
"""True if PATH is ROOT itself or nested under it (after normalization)."""
|
|
36
|
+
try:
|
|
37
|
+
return os.path.commonpath([root, path]) == root
|
|
38
|
+
except ValueError:
|
|
39
|
+
# Raised when the paths live on different drives (Windows).
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def serve_file(
|
|
44
|
+
flow: http.HTTPFlow,
|
|
45
|
+
filepath: str,
|
|
46
|
+
rule_type: str,
|
|
47
|
+
rule_name: str,
|
|
48
|
+
base_dir: str,
|
|
49
|
+
root: str | None = None,
|
|
50
|
+
) -> None:
|
|
19
51
|
absolute_path = resolve_path(filepath, base_dir)
|
|
52
|
+
if root is not None and not _is_within(resolve_path(root, base_dir), absolute_path):
|
|
53
|
+
ctx.log.warn(f"Blocked path traversal for {rule_type}:{rule_name} -> {absolute_path}")
|
|
54
|
+
flow.response = http.Response.make(
|
|
55
|
+
403,
|
|
56
|
+
b"dataverseproxy: path escapes the configured folder",
|
|
57
|
+
{"Content-Type": "text/plain"},
|
|
58
|
+
)
|
|
59
|
+
return
|
|
20
60
|
try:
|
|
21
61
|
with open(absolute_path, "rb") as handle:
|
|
22
62
|
content = handle.read()
|
|
@@ -28,7 +68,10 @@ def serve_file(flow: http.HTTPFlow, filepath: str, rule_type: str, rule_name: st
|
|
|
28
68
|
{"Content-Type": "text/plain"},
|
|
29
69
|
)
|
|
30
70
|
return
|
|
31
|
-
|
|
71
|
+
ext = os.path.splitext(absolute_path)[1].lower()
|
|
72
|
+
content_type = (
|
|
73
|
+
_CONTENT_TYPES.get(ext) or mimetypes.guess_type(absolute_path)[0] or "application/octet-stream"
|
|
74
|
+
)
|
|
32
75
|
flow.response = http.Response.make(200, content, {"Content-Type": content_type})
|
|
33
76
|
_log_redirect(flow, rule_type, rule_name, absolute_path)
|
|
34
77
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hummingbirdworks/proxy",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "mitmproxy-based dev proxy that redirects Dataverse / Dynamics 365 web resources and PCF control assets to local dev builds or a running dev server.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hummingbird-proxy": "bin/hummingbird-proxy.js"
|
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
},
|
|
13
13
|
"./package.json": "./package.json"
|
|
14
14
|
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/ps-Hummingbird/powerapps-mitm-proxy-addon.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/ps-Hummingbird/powerapps-mitm-proxy-addon/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/ps-Hummingbird/powerapps-mitm-proxy-addon#readme",
|
|
23
|
+
"author": {
|
|
24
|
+
"name": "Hummingbird",
|
|
25
|
+
"url": "https://www.hummingbirdworks.ai/"
|
|
26
|
+
},
|
|
15
27
|
"files": [
|
|
16
28
|
"bin/",
|
|
17
29
|
"vite/",
|
package/powerapp_dev_proxy.py
CHANGED
|
@@ -8,8 +8,6 @@ during load, so the sibling ``hummingbird_proxy`` package resolves here. All
|
|
|
8
8
|
implementation lives in that package; this module is just the entrypoint.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
# Version 2026-08-21
|
|
12
|
-
|
|
13
11
|
from hummingbird_proxy import DataverseProxy, config_path
|
|
14
12
|
|
|
15
13
|
addons = [DataverseProxy(config_path())]
|