@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 ps Hummingbird
3
+ Copyright (c) 2026 Hummingbird
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -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
- // First non-flag argument is the config path; everything else is forwarded to mitmdump.
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
- const passthrough = [];
101
- for (const arg of argv) {
102
- if (configArg === undefined && !arg.startsWith("-")) {
103
- configArg = arg;
104
- } else {
105
- passthrough.push(arg);
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(flow, os.path.join(*parts), item_type, item["name"], item["_base_dir"])
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 serve_file(flow: http.HTTPFlow, filepath: str, rule_type: str, rule_name: str, base_dir: str) -> None:
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
- content_type = mimetypes.guess_type(absolute_path)[0] or "application/octet-stream"
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.0",
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/",
@@ -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())]