@piecemaker-legal/piecemaker 2.0.9 → 2.0.10

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.
Files changed (165) hide show
  1. package/dist/assets/FindReplaceDialog-KU5TEIBJ-CQ76sPaJ.js +1 -0
  2. package/dist/assets/FootnotePropertiesDialog-W3YHM4BV-BpgYQvvd.js +1 -0
  3. package/dist/assets/HyperlinkDialog-ZYH7ZHRF-qIPBOssF.css +1 -0
  4. package/dist/assets/HyperlinkDialog-ZYH7ZHRF-z08UbYbL.js +144 -0
  5. package/dist/assets/ImagePositionDialog-HEWYR6Q3-CFzOnzAt.js +1 -0
  6. package/dist/assets/ImagePropertiesDialog-RAB47C6M-D-ThGU-b.js +1 -0
  7. package/dist/assets/PageSetupDialog-AIGIXO3E-_PYIO8eb.js +1 -0
  8. package/dist/assets/SplitCellDialog-IFBKYCD6-Cb9uQOB8.js +1 -0
  9. package/dist/assets/TablePropertiesDialog-TMLKGOOW-12vmjb6B.js +1 -0
  10. package/dist/assets/WatermarkDialog-KRL7WMIS-C-d8rzt2.js +1 -0
  11. package/dist/assets/{abnfDiagram-VCTEODGH-WrsSAHOe.js → abnfDiagram-VCTEODGH-B4OsX7eF.js} +1 -1
  12. package/dist/assets/arc-BK1oUIk7.js +1 -0
  13. package/dist/assets/architectureDiagram-5GKGNRK7-CfaFI6L7.js +36 -0
  14. package/dist/assets/{blockDiagram-I7D4REHJ-zGMnF7Ox.js → blockDiagram-I7D4REHJ-DQEI2azY.js} +1 -1
  15. package/dist/assets/{c4Diagram-7LVT6UL2-ClWw3bqt.js → c4Diagram-7LVT6UL2-DbravpVi.js} +1 -1
  16. package/dist/assets/channel-DVyNr2Yg.js +1 -0
  17. package/dist/assets/{chunk-2Q5K7J3B-C-9ICPHv.js → chunk-2Q5K7J3B-BLpZ9huC.js} +1 -1
  18. package/dist/assets/{chunk-5VM5RSS4-Bawy3teT.js → chunk-5VM5RSS4-BLlEdmX9.js} +1 -1
  19. package/dist/assets/{chunk-F27PBJKO-DQ375RQO.js → chunk-F27PBJKO-D-ZNhI6q.js} +1 -1
  20. package/dist/assets/{chunk-IMKFNOWR-CflNF2f6.js → chunk-IMKFNOWR-vzSiKnhh.js} +1 -1
  21. package/dist/assets/{chunk-JWPE2WC7-CIXQv0gc.js → chunk-JWPE2WC7-DfF0k4nb.js} +1 -1
  22. package/dist/assets/{chunk-POPQ4Y6H-Cza5j6Qw.js → chunk-POPQ4Y6H-BnwCYsPP.js} +1 -1
  23. package/dist/assets/{chunk-SVP7TREG-GQ_VvWZR.js → chunk-SVP7TREG-DRxc8fhq.js} +1 -1
  24. package/dist/assets/{chunk-TICWLB2K-BEmU16W5.js → chunk-TICWLB2K-Dd2Q-md-.js} +1 -1
  25. package/dist/assets/{chunk-XXDRQBXY-C1KpkLYH.js → chunk-XXDRQBXY-DBjkEUp_.js} +1 -1
  26. package/dist/assets/classDiagram-ZZMXUADV-2pGPHl5H.js +1 -0
  27. package/dist/assets/classDiagram-v2-VYDZK3BY-2pGPHl5H.js +1 -0
  28. package/dist/assets/{cose-bilkent-JH36ORCC-CZE48TRG.js → cose-bilkent-JH36ORCC-By6PS4oe.js} +1 -1
  29. package/dist/assets/{cynefin-OW5HDTMX-L-tx1eC-.js → cynefin-OW5HDTMX-YxWpjc5W.js} +1 -1
  30. package/dist/assets/{cynefinDiagram-5FMLGOSQ-Coa8oJwR.js → cynefinDiagram-5FMLGOSQ-DrtXzz3p.js} +1 -1
  31. package/dist/assets/{dagre-GXQ25YYZ-Bu_3Y75K.js → dagre-GXQ25YYZ-BDpTLKqZ.js} +1 -1
  32. package/dist/assets/{diagram-S7CK7UJ4-CFiiV4es.js → diagram-S7CK7UJ4-C4LvqeCs.js} +1 -1
  33. package/dist/assets/{diagram-UQ7AKVKN-CCXheGAA.js → diagram-UQ7AKVKN-BTpnMeHp.js} +1 -1
  34. package/dist/assets/{diagram-VSXAHHWV-B70m4lr_.js → diagram-VSXAHHWV-DucHHWev.js} +1 -1
  35. package/dist/assets/{diagram-VX7I27RA-CHkiMOVA.js → diagram-VX7I27RA-DYHZNmmH.js} +1 -1
  36. package/dist/assets/{diagram-Z3DM3KII-_awpc4Bs.js → diagram-Z3DM3KII-BHIH2BGN.js} +1 -1
  37. package/dist/assets/{ebnfDiagram-PWID7BFC-CZzjH86H.js → ebnfDiagram-PWID7BFC-CGiEkwj0.js} +1 -1
  38. package/dist/assets/{erDiagram-RLTQ6QDP-9DgiHZg9.js → erDiagram-RLTQ6QDP-AJMHi2Us.js} +1 -1
  39. package/dist/assets/{flowDiagram-HODETNUW-DHY_503b.js → flowDiagram-HODETNUW-BqzANBh2.js} +1 -1
  40. package/dist/assets/{ganttDiagram-EL5Y4UJY-DPiiYZ_l.js → ganttDiagram-EL5Y4UJY-B0xPDY2_.js} +1 -1
  41. package/dist/assets/{gitGraphDiagram-WWUBYQGX-Ddd_evm7.js → gitGraphDiagram-WWUBYQGX-CzPX5CNO.js} +1 -1
  42. package/dist/assets/{index-tibHq7Qp.js → index-Bk2iXKnJ.js} +140 -140
  43. package/dist/assets/index-Dc9wNuS4.css +1 -0
  44. package/dist/assets/{infoDiagram-27XIBGKW-BPRNhfwb.js → infoDiagram-27XIBGKW-Cjc44H0O.js} +1 -1
  45. package/dist/assets/{ishikawaDiagram-5VMMS53U-BXx6aqkq.js → ishikawaDiagram-5VMMS53U-BWhr2v7r.js} +1 -1
  46. package/dist/assets/{journeyDiagram-3NMN7TZE-BK8TZM29.js → journeyDiagram-3NMN7TZE-Bl4r6RrP.js} +1 -1
  47. package/dist/assets/{kanban-definition-UXKFOSKX-CHioRmxz.js → kanban-definition-UXKFOSKX-FV-T0Vy1.js} +6 -6
  48. package/dist/assets/layout-CTfb1sAF.js +1 -0
  49. package/dist/assets/{linear-CTEhUcKG.js → linear-Dfgi6vlo.js} +1 -1
  50. package/dist/assets/{mermaid.core-DYUb_UUB.js → mermaid.core-DOCGsdlo.js} +7 -7
  51. package/dist/assets/{mindmap-definition-YA3MSWOX-D7ODZ4oS.js → mindmap-definition-YA3MSWOX-CymQ86ir.js} +1 -1
  52. package/dist/assets/{pegDiagram-XKGWAZYB-8J-_2_kw.js → pegDiagram-XKGWAZYB-1VMq_vi0.js} +1 -1
  53. package/dist/assets/{pieDiagram-E7YTZNPT-ebg0yfMi.js → pieDiagram-E7YTZNPT-C5GgXtJU.js} +1 -1
  54. package/dist/assets/processTemplate-GPFDLQMX-B1sVV2O0.js +22 -0
  55. package/dist/assets/{quadrantDiagram-AXDQQJYC-BoWkDiFb.js → quadrantDiagram-AXDQQJYC-C6zpryYf.js} +1 -1
  56. package/dist/assets/{railroadDiagram-O6MQD6OU-BRJ77ldz.js → railroadDiagram-O6MQD6OU-D-sv4SWi.js} +1 -1
  57. package/dist/assets/{requirementDiagram-BXWQKSXE-Z0JJgktA.js → requirementDiagram-BXWQKSXE-CkjY-z4B.js} +1 -1
  58. package/dist/assets/sankeyDiagram-P5KCCOFB-De-d8Q6L.js +40 -0
  59. package/dist/assets/{sequenceDiagram-WJ2MYXX4-BPAjtXwp.js → sequenceDiagram-WJ2MYXX4-DCsGSJa9.js} +1 -1
  60. package/dist/assets/{server.browser-BzI1jrZw.js → server.browser-BY3bL-IX.js} +1 -1
  61. package/dist/assets/{sizeCapture-INFHLROL-BGZPCzM4.js → sizeCapture-INFHLROL-xINZhgER.js} +1 -1
  62. package/dist/assets/stateDiagram-D77RDMKH-CuLUTWSL.js +1 -0
  63. package/dist/assets/stateDiagram-v2-MP3YSRHH-Cnbs-M92.js +1 -0
  64. package/dist/assets/{swimlanes-42K2YHIH-DvuB15qF.js → swimlanes-42K2YHIH-BwxARD7N.js} +1 -1
  65. package/dist/assets/swimlanesDiagram-VR7AAH4N-D7p8qZI2.js +8 -0
  66. package/dist/assets/{timeline-definition-24CTP7MA-ZIziYCIF.js → timeline-definition-24CTP7MA-BIOvnn-l.js} +3 -3
  67. package/dist/assets/{vendor-codemirror-CjW0lHR5.js → vendor-codemirror-CN-sJePf.js} +2 -2
  68. package/dist/assets/{vendor-react-D809iwXR.js → vendor-react-B22wqN3J.js} +5 -5
  69. package/dist/assets/{vennDiagram-4TSXK5OY-C5_B9gWa.js → vennDiagram-4TSXK5OY-BSD6Pfmt.js} +4 -4
  70. package/dist/assets/{wardleyDiagram-VM6X3IG4-DMlH9Afw.js → wardleyDiagram-VM6X3IG4-7Zz2iRkF.js} +1 -1
  71. package/dist/assets/{xychartDiagram-S5SC5T6Z-BldhJt7i.js → xychartDiagram-S5SC5T6Z-Ctx17Y1D.js} +1 -1
  72. package/dist/index.html +4 -4
  73. package/dist/sw.js +1 -1
  74. package/dist-server/server/modules/providers/list/mistral/mistral-models.provider.js +4 -53
  75. package/dist-server/server/modules/providers/list/mistral/mistral-models.provider.js.map +1 -1
  76. package/dist-server/server/modules/providers/list/mistral/mistral-runtime.provider.js +2 -3
  77. package/dist-server/server/modules/providers/list/mistral/mistral-runtime.provider.js.map +1 -1
  78. package/dist-server/server/modules/providers/list/mistral/mistral-vibe-models.js +30 -0
  79. package/dist-server/server/modules/providers/list/mistral/mistral-vibe-models.js.map +1 -0
  80. package/dist-server/server/modules/providers/tests/mistral-vibe-models.test.js +24 -0
  81. package/dist-server/server/modules/providers/tests/mistral-vibe-models.test.js.map +1 -0
  82. package/dist-server/server/piecemaker/anonymizer/lifecycle.js +2 -3
  83. package/dist-server/server/piecemaker/anonymizer/lifecycle.js.map +1 -1
  84. package/dist-server/server/piecemaker/docx-document.js +50 -0
  85. package/dist-server/server/piecemaker/docx-document.js.map +1 -0
  86. package/dist-server/server/piecemaker/index.js +9 -2
  87. package/dist-server/server/piecemaker/index.js.map +1 -1
  88. package/dist-server/server/piecemaker/model-discovery/codex-catalog.js +46 -0
  89. package/dist-server/server/piecemaker/model-discovery/codex-catalog.js.map +1 -0
  90. package/dist-server/server/piecemaker/model-discovery/index.js +42 -0
  91. package/dist-server/server/piecemaker/model-discovery/index.js.map +1 -0
  92. package/dist-server/server/piecemaker/model-discovery/mistral-catalog.js +19 -0
  93. package/dist-server/server/piecemaker/model-discovery/mistral-catalog.js.map +1 -0
  94. package/dist-server/server/piecemaker/model-discovery/stdio-rpc.js +61 -0
  95. package/dist-server/server/piecemaker/model-discovery/stdio-rpc.js.map +1 -0
  96. package/dist-server/server/piecemaker/project-registry.js +29 -0
  97. package/dist-server/server/piecemaker/project-registry.js.map +1 -0
  98. package/package.json +12 -1
  99. package/scripts/piecemaker/cli/disable-anonymizer.mjs +19 -0
  100. package/scripts/piecemaker/cli/lib/config.mjs +0 -1
  101. package/scripts/piecemaker/cli/lib/services.mjs +1 -10
  102. package/scripts/piecemaker/cli/piecemaker.mjs +0 -3
  103. package/server/modules/providers/list/mistral/mistral-models.provider.ts +4 -53
  104. package/server/modules/providers/list/mistral/mistral-runtime.provider.ts +2 -3
  105. package/server/modules/providers/list/mistral/mistral-vibe-models.ts +43 -0
  106. package/server/modules/providers/tests/mistral-vibe-models.test.ts +27 -0
  107. package/server/piecemaker/anonymizer/client-config.cjs +92 -92
  108. package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +9 -16
  109. package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.toml +1 -1
  110. package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +251 -268
  111. package/server/piecemaker/anonymizer/lifecycle.ts +2 -3
  112. package/server/piecemaker/anonymizer/rewriter-bridge.cjs +6 -0
  113. package/server/piecemaker/anonymizer/service.cjs +103 -174
  114. package/server/piecemaker/docx-document.ts +54 -0
  115. package/server/piecemaker/harness/decisions.cjs +1 -1
  116. package/server/piecemaker/harness/index.cjs +2 -3
  117. package/server/piecemaker/harness/verification.cjs +1 -1
  118. package/server/piecemaker/index.ts +11 -3
  119. package/server/piecemaker/model-discovery/codex-catalog.ts +67 -0
  120. package/server/piecemaker/model-discovery/index.ts +69 -0
  121. package/server/piecemaker/model-discovery/mistral-catalog.ts +36 -0
  122. package/server/piecemaker/model-discovery/stdio-rpc.ts +79 -0
  123. package/server/piecemaker/project-registry.ts +34 -0
  124. package/server/piecemaker/router.cjs +6 -7
  125. package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +31 -77
  126. package/server/piecemaker/vendor/installer/lib/codex-skills.mjs +2 -84
  127. package/server/piecemaker/vendor/installer/lib/service.mjs +4 -24
  128. package/server/piecemaker/vendor/installer/steps/06-hooks.mjs +10 -21
  129. package/server/piecemaker/vendor/installer/steps/09-claude-assets.mjs +0 -11
  130. package/server/piecemaker/vendor/installer/steps/09-codex-plugin.mjs +6 -36
  131. package/server/piecemaker/vendor/installer/steps/13-garde-secrets.mjs +30 -12
  132. package/server/piecemaker/vendor/piecemaker-plugin/README.md +2 -9
  133. package/server/piecemaker/vendor/piecemaker-plugin/scripts/billing-track.mjs +5 -5
  134. package/server/piecemaker/vendor/piecemaker-plugin/scripts/classify-ai-documents.mjs +8 -8
  135. package/server/piecemaker/vendor/piecemaker-plugin/scripts/commit-track.mjs +2 -2
  136. package/server/piecemaker/vendor/piecemaker-plugin/scripts/compile-recherche.mjs +4 -6
  137. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folders.cjs +71 -38
  138. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/mapping.cjs +8 -38
  139. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/protection.cjs +82 -61
  140. package/server/piecemaker/vendor/piecemaker-plugin/scripts/protect-originals.mjs +4 -55
  141. package/server/piecemaker/vendor/piecemaker-plugin/scripts/statusline.mjs +24 -117
  142. package/server/piecemaker/vendor/piecemaker-plugin/skills/tamponnage/SKILL.md +2 -2
  143. package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +15 -121
  144. package/server/piecemaker/vendor/websocket-server/case-registry.cjs +16 -32
  145. package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +0 -73
  146. package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +0 -7
  147. package/server/piecemaker/vendor/websocket-server/stamping-routes.cjs +3 -12
  148. package/server/piecemaker/vendor/websocket-server/workspace-paths.cjs +6 -6
  149. package/dist/assets/arc-DAA6wH-C.js +0 -1
  150. package/dist/assets/architectureDiagram-5GKGNRK7-BGNHr7ef.js +0 -36
  151. package/dist/assets/channel-BAQyfMll.js +0 -1
  152. package/dist/assets/classDiagram-ZZMXUADV-BNGCuvrR.js +0 -1
  153. package/dist/assets/classDiagram-v2-VYDZK3BY-BNGCuvrR.js +0 -1
  154. package/dist/assets/index-WwknP0l4.css +0 -1
  155. package/dist/assets/layout-DEnkyfTY.js +0 -1
  156. package/dist/assets/sankeyDiagram-P5KCCOFB-4PW5HTOP.js +0 -40
  157. package/dist/assets/stateDiagram-D77RDMKH-o-vEcKwz.js +0 -1
  158. package/dist/assets/stateDiagram-v2-MP3YSRHH-BsuDaKkN.js +0 -1
  159. package/dist/assets/swimlanesDiagram-VR7AAH4N-DcCAyKlT.js +0 -8
  160. package/scripts/piecemaker/cli/lib/piecemaker-anonymizer.mjs +0 -78
  161. package/scripts/piecemaker/hooks/proxy-guard.mjs +0 -251
  162. package/server/piecemaker/anonymizer/providers.cjs +0 -120
  163. package/server/piecemaker/vendor/installer/steps/08-telegram.mjs +0 -482
  164. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/central-mapping.cjs +0 -0
  165. package/server/piecemaker/vendor/websocket-server/telegram-admin.cjs +0 -602
@@ -1,18 +1,23 @@
1
1
  use std::collections::{HashMap, HashSet};
2
+ use std::io::Read;
2
3
  use std::net::SocketAddr;
3
4
  use std::sync::atomic::{AtomicU64, Ordering};
4
5
  use std::sync::Arc;
6
+ use std::time::Duration;
5
7
 
6
8
  use bytes::Bytes;
7
- use futures::StreamExt;
9
+ use futures::{SinkExt, StreamExt};
8
10
  use http_body_util::{BodyExt, BodyStream};
9
11
  use hudsucker::certificate_authority::RcgenAuthority;
10
- use hudsucker::hyper::{Request, Response, StatusCode, Uri};
12
+ use hudsucker::hyper::{Method, Request, Response, StatusCode, Uri};
11
13
  use hudsucker::rcgen::{Issuer, KeyPair};
12
14
  use hudsucker::rustls::crypto::aws_lc_rs;
13
- use hudsucker::tokio_tungstenite::tungstenite::Message;
15
+ use hudsucker::tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
16
+ use hudsucker::tokio_tungstenite::tungstenite::protocol::CloseFrame;
17
+ use hudsucker::tokio_tungstenite::tungstenite::{Error as WsError, Message};
14
18
  use hudsucker::{
15
- Body, HttpContext, HttpHandler, Proxy, RequestOrResponse, WebSocketContext, WebSocketHandler,
19
+ decode_request, decode_response, Body, HttpContext, HttpHandler, Proxy, RequestOrResponse,
20
+ WebSocketContext, WebSocketHandler,
16
21
  };
17
22
  use tokio::sync::mpsc;
18
23
  use tokio_stream::wrappers::ReceiverStream;
@@ -27,7 +32,7 @@ struct Handler {
27
32
  hosts: Arc<HashSet<String>>,
28
33
  upstream: Arc<HashMap<String, String>>,
29
34
  http: reqwest::Client,
30
- provider: String,
35
+ provider: &'static str,
31
36
  ws_id: Option<String>,
32
37
  }
33
38
 
@@ -54,9 +59,6 @@ fn request_host(req: &Request<Body>) -> String {
54
59
  if let Some(host) = req.uri().host() {
55
60
  return host.to_ascii_lowercase();
56
61
  }
57
- if let Some(authority) = req.uri().authority() {
58
- return authority.host().to_ascii_lowercase();
59
- }
60
62
  req.headers()
61
63
  .get("host")
62
64
  .and_then(|value| value.to_str().ok())
@@ -72,44 +74,25 @@ fn provider_for(host: &str) -> &'static str {
72
74
  }
73
75
  }
74
76
 
75
- fn header_text(req: &Request<Body>, name: &str) -> String {
76
- req.headers()
77
- .get(name)
78
- .and_then(|value| value.to_str().ok())
79
- .unwrap_or("")
80
- .to_string()
81
- }
82
-
83
- fn response_type(res: &Response<Body>) -> String {
84
- res.headers()
77
+ fn content_type<T>(message: &hudsucker::hyper::http::HeaderMap<T>) -> String
78
+ where
79
+ T: AsRef<[u8]>,
80
+ {
81
+ message
85
82
  .get("content-type")
86
- .and_then(|value| value.to_str().ok())
87
- .unwrap_or("")
88
- .to_ascii_lowercase()
89
- }
90
-
91
- fn is_json(value: &str) -> bool {
92
- value.contains("json")
93
- }
94
-
95
- fn is_sse(value: &str) -> bool {
96
- value.contains("text/event-stream")
97
- }
98
-
99
- fn health_request(req: &Request<Body>) -> bool {
100
- let host = request_host(req);
101
- req.method() == hudsucker::hyper::Method::GET
102
- && (req.uri().path() == "/" || req.uri().path().is_empty())
103
- && (host.is_empty() || host == "127.0.0.1" || host == "localhost")
83
+ .map(|value| String::from_utf8_lossy(value.as_ref()).to_ascii_lowercase())
84
+ .unwrap_or_default()
104
85
  }
105
86
 
106
- fn failure() -> Response<Body> {
87
+ fn refusal(status: StatusCode, host: &str, path: &str, cause: &str) -> Response<Body> {
88
+ eprintln!("refus {} {host}{path} : {cause}", status.as_u16());
89
+ let message = serde_json::json!({
90
+ "error": { "type": "piecemaker_proxy", "message": format!("Relais d'anonymisation PieceMaker : {cause}.") }
91
+ });
107
92
  Response::builder()
108
- .status(StatusCode::BAD_GATEWAY)
93
+ .status(status)
109
94
  .header("content-type", "application/json")
110
- .body(Body::from(
111
- "{\"error\":{\"type\":\"piecemaker_proxy\",\"message\":\"Relais d'anonymisation indisponible.\"}}",
112
- ))
95
+ .body(Body::from(message.to_string()))
113
96
  .unwrap()
114
97
  }
115
98
 
@@ -120,18 +103,40 @@ fn uri_path(uri: &Uri) -> String {
120
103
  }
121
104
  }
122
105
 
106
+ async fn collect(body: Body) -> Result<Bytes, &'static str> {
107
+ let bytes = body
108
+ .collect()
109
+ .await
110
+ .map_err(|_| "corps illisible")?
111
+ .to_bytes();
112
+ if bytes.len() > MAX_BODY {
113
+ return Err("corps trop volumineux");
114
+ }
115
+ Ok(bytes)
116
+ }
117
+
123
118
  impl Handler {
124
119
  fn allowed(&self, host: &str) -> bool {
125
120
  self.hosts.contains(host)
126
121
  }
127
122
 
123
+ fn intercepted_host(&self, uri: &Uri) -> Option<String> {
124
+ let host = uri.host().map(str::to_ascii_lowercase).unwrap_or_default();
125
+ if self.allowed(&host) {
126
+ return Some(host);
127
+ }
128
+ let authority = uri.authority()?.as_str();
129
+ self.upstream
130
+ .iter()
131
+ .find(|(_, dest)| dest.as_str() == authority)
132
+ .map(|(host, _)| host.clone())
133
+ }
134
+
128
135
  fn point_upstream(&self, host: &str, req: &mut Request<Body>) {
129
136
  let Some(dest) = self.upstream.get(host) else {
130
137
  return;
131
138
  };
132
- let path = uri_path(req.uri());
133
- let rewritten = format!("http://{dest}{path}");
134
- if let Ok(uri) = rewritten.parse() {
139
+ if let Ok(uri) = format!("http://{dest}{}", uri_path(req.uri())).parse() {
135
140
  *req.uri_mut() = uri;
136
141
  }
137
142
  if let Ok(value) = dest.parse() {
@@ -139,25 +144,47 @@ impl Handler {
139
144
  }
140
145
  }
141
146
 
142
- async fn post_bytes(&self, path: &str, provider: &str, extra: &[(&str, &str)], body: Bytes) -> Result<Bytes, ()> {
147
+ async fn rewrite(&self, path: &str, extra: &[(&str, &str)], body: Bytes) -> Result<Bytes, &'static str> {
143
148
  let mut request = self
144
149
  .http
145
150
  .post(format!("{}{path}", self.rewriter))
146
- .header("x-piecemaker-provider", provider)
151
+ .header("x-piecemaker-provider", self.provider)
147
152
  .body(body);
148
153
  for (name, value) in extra {
149
154
  request = request.header(*name, *value);
150
155
  }
151
- let response = request.send().await.map_err(|_| ())?;
156
+ let response = request.send().await.map_err(|_| "réécrivain injoignable")?;
152
157
  if !response.status().is_success() {
153
- return Err(());
158
+ return Err("réécriture en échec");
154
159
  }
155
- response.bytes().await.map_err(|_| ())
160
+ response.bytes().await.map_err(|_| "réécriture interrompue")
156
161
  }
157
162
 
158
- fn spawn_sse(&self, provider: String, body: Body) -> Body {
159
- let http = self.http.clone();
160
- let url = format!("{}/v1/sse", self.rewriter);
163
+ async fn health(&self) -> Response<Body> {
164
+ let alive = self
165
+ .http
166
+ .get(format!("{}/health", self.rewriter))
167
+ .send()
168
+ .await
169
+ .map(|response| response.status().is_success())
170
+ .unwrap_or(false);
171
+ let (status, text) = if alive {
172
+ (StatusCode::OK, format!("{{\"pid\":{},\"rewriter\":\"ok\"}}", std::process::id()))
173
+ } else {
174
+ (StatusCode::SERVICE_UNAVAILABLE, format!("{{\"pid\":{},\"rewriter\":\"down\"}}", std::process::id()))
175
+ };
176
+ Response::builder()
177
+ .status(status)
178
+ .header("content-type", "application/json")
179
+ .body(Body::from(text))
180
+ .unwrap()
181
+ }
182
+
183
+ fn spawn_sse(&self, body: Body) -> Body {
184
+ let request = self
185
+ .http
186
+ .post(format!("{}/v1/sse", self.rewriter))
187
+ .header("x-piecemaker-provider", self.provider);
161
188
  let (tx, rx) = mpsc::channel::<Result<Bytes, std::io::Error>>(8);
162
189
  tokio::spawn(async move {
163
190
  let incoming = BodyStream::new(body).filter_map(|frame| async move {
@@ -166,29 +193,58 @@ impl Handler {
166
193
  Err(error) => Some(Err(std::io::Error::other(error.to_string()))),
167
194
  }
168
195
  });
169
- let sent = http
170
- .post(url)
171
- .header("x-piecemaker-provider", provider)
172
- .body(reqwest::Body::wrap_stream(incoming))
173
- .send()
174
- .await;
175
- match sent {
196
+ match request.body(reqwest::Body::wrap_stream(incoming)).send().await {
176
197
  Ok(response) => {
177
198
  let mut stream = response.bytes_stream();
178
199
  while let Some(chunk) = stream.next().await {
179
- let mapped = chunk.map_err(|error| std::io::Error::other(error.to_string()));
180
- if tx.send(mapped).await.is_err() {
200
+ if tx.send(chunk.map_err(std::io::Error::other)).await.is_err() {
181
201
  break;
182
202
  }
183
203
  }
184
204
  }
185
205
  Err(error) => {
186
- let _ = tx.send(Err(std::io::Error::other(error.to_string()))).await;
206
+ eprintln!("refus flux SSE : réécrivain injoignable");
207
+ let _ = tx.send(Err(std::io::Error::other(error))).await;
187
208
  }
188
209
  }
189
210
  });
190
211
  Body::from_stream(ReceiverStream::new(rx))
191
212
  }
213
+
214
+ async fn rewrite_ws(&mut self, host: &str, outbound: bool, text: &str) -> Result<Option<Message>, &'static str> {
215
+ let id = self
216
+ .ws_id
217
+ .get_or_insert_with(|| format!("ws-{}", WS_IDS.fetch_add(1, Ordering::Relaxed)))
218
+ .clone();
219
+ self.provider = provider_for(host);
220
+ let direction = if outbound { "out" } else { "in" };
221
+ let rewritten = self
222
+ .rewrite(
223
+ "/v1/ws",
224
+ &[("x-piecemaker-ws", id.as_str()), ("x-piecemaker-direction", direction)],
225
+ Bytes::from(text.to_string()),
226
+ )
227
+ .await?;
228
+ if rewritten.is_empty() {
229
+ return Ok(None);
230
+ }
231
+ Ok(Some(Message::Text(String::from_utf8_lossy(&rewritten).into_owned().into())))
232
+ }
233
+
234
+ async fn flush_ws(&self, host: &str) -> Vec<String> {
235
+ let Some(id) = self.ws_id.clone() else {
236
+ return Vec::new();
237
+ };
238
+ let mut handler = self.clone();
239
+ handler.provider = provider_for(host);
240
+ match handler
241
+ .rewrite("/v1/ws-end", &[("x-piecemaker-ws", id.as_str())], Bytes::new())
242
+ .await
243
+ {
244
+ Ok(tail) => pending_messages(&tail),
245
+ Err(_) => Vec::new(),
246
+ }
247
+ }
192
248
  }
193
249
 
194
250
  impl HttpHandler for Handler {
@@ -208,55 +264,41 @@ impl HttpHandler for Handler {
208
264
  }
209
265
 
210
266
  async fn handle_request(&mut self, _ctx: &HttpContext, req: Request<Body>) -> RequestOrResponse {
211
- if health_request(&req) {
212
- return Response::builder()
213
- .status(StatusCode::OK)
214
- .body(Body::from("ok"))
215
- .unwrap()
216
- .into();
217
- }
218
-
219
- if req.method() == hudsucker::hyper::Method::CONNECT {
220
- return req.into();
221
- }
222
-
223
267
  let host = request_host(&req);
224
- if !self.allowed(&host) {
268
+ if req.method() == Method::GET && req.uri().path() == "/health" && (host == "127.0.0.1" || host == "localhost") {
269
+ return self.health().await.into();
270
+ }
271
+ if req.method() == Method::CONNECT || !self.allowed(&host) {
225
272
  return req.into();
226
273
  }
227
- self.provider = provider_for(&host).to_string();
274
+ self.provider = provider_for(&host);
275
+ let path = uri_path(req.uri());
228
276
 
229
- let content_type = header_text(&req, "content-type");
277
+ let req = match decode_request(req) {
278
+ Ok(req) => req,
279
+ Err(_) => return refusal(StatusCode::UNSUPPORTED_MEDIA_TYPE, &host, &path, "encodage de corps inconnu").into(),
280
+ };
281
+ let kind = content_type(req.headers());
230
282
  let (mut parts, body) = req.into_parts();
231
283
  parts.headers.remove("accept-encoding");
232
284
  parts.headers.remove("sec-websocket-extensions");
233
285
 
234
- let body = if is_json(&content_type.to_ascii_lowercase()) {
235
- let collected = match body.collect().await {
236
- Ok(collected) => collected.to_bytes(),
237
- Err(_) => return failure().into(),
238
- };
239
- if collected.len() > MAX_BODY {
240
- return failure().into();
241
- }
242
- if collected.is_empty() {
243
- Body::empty()
244
- } else {
245
- match self
246
- .post_bytes("/v1/request", &self.provider.clone(), &[], collected)
247
- .await
248
- {
249
- Ok(rewritten) => {
250
- if let Ok(length) = rewritten.len().to_string().parse() {
251
- parts.headers.insert("content-length", length);
252
- }
253
- Body::from(rewritten)
254
- }
255
- Err(_) => return failure().into(),
286
+ let collected = match collect(body).await {
287
+ Ok(bytes) => bytes,
288
+ Err(cause) => return refusal(StatusCode::BAD_GATEWAY, &host, &path, cause).into(),
289
+ };
290
+ let body = if collected.is_empty() {
291
+ Body::empty()
292
+ } else if kind.contains("json") {
293
+ match self.rewrite("/v1/request", &[], collected).await {
294
+ Ok(rewritten) => {
295
+ parts.headers.insert("content-length", rewritten.len().into());
296
+ Body::from(rewritten)
256
297
  }
298
+ Err(cause) => return refusal(StatusCode::BAD_GATEWAY, &host, &path, cause).into(),
257
299
  }
258
300
  } else {
259
- body
301
+ return refusal(StatusCode::UNSUPPORTED_MEDIA_TYPE, &host, &path, "corps non JSON impossible à anonymiser").into();
260
302
  };
261
303
 
262
304
  let mut req = Request::from_parts(parts, body);
@@ -265,144 +307,93 @@ impl HttpHandler for Handler {
265
307
  }
266
308
 
267
309
  async fn handle_response(&mut self, _ctx: &HttpContext, res: Response<Body>) -> Response<Body> {
268
- let kind = response_type(&res);
269
- let provider = self.provider.clone();
310
+ let res = match decode_response(res) {
311
+ Ok(res) => res,
312
+ Err(_) => return refusal(StatusCode::BAD_GATEWAY, self.provider, "", "réponse compressée illisible"),
313
+ };
314
+ let kind = content_type(res.headers());
270
315
  let (mut parts, body) = res.into_parts();
271
- parts.headers.remove("content-encoding");
272
- parts.headers.remove("content-length");
273
- parts.headers.remove("transfer-encoding");
274
- parts.headers.remove("connection");
275
-
276
- if is_sse(&kind) {
277
- parts.headers.remove("content-length");
278
- parts.headers.insert(
279
- "content-type",
280
- "text/event-stream".parse().unwrap(),
281
- );
282
- return Response::from_parts(parts, self.spawn_sse(provider, body));
316
+ for name in ["content-encoding", "content-length", "transfer-encoding", "connection"] {
317
+ parts.headers.remove(name);
283
318
  }
284
319
 
285
- if is_json(&kind) {
286
- let collected = match body.collect().await {
287
- Ok(collected) => collected.to_bytes(),
288
- Err(_) => return failure(),
289
- };
290
- if collected.len() > MAX_BODY {
291
- return failure();
292
- }
293
- let rewritten = match self.post_bytes("/v1/response", &provider, &[], collected).await {
294
- Ok(rewritten) => rewritten,
295
- Err(_) => return failure(),
296
- };
297
- return Response::from_parts(parts, Body::from(rewritten));
320
+ if kind.contains("text/event-stream") {
321
+ return Response::from_parts(parts, self.spawn_sse(body));
298
322
  }
299
-
300
- Response::from_parts(parts, body)
301
- }
302
- }
303
-
304
- impl WebSocketHandler for Handler {
305
- async fn handle_message(&mut self, ctx: &WebSocketContext, message: Message) -> Option<Message> {
306
- let (outbound, uri) = match ctx {
307
- WebSocketContext::ClientToServer { dst, .. } => (true, dst),
308
- WebSocketContext::ServerToClient { src, .. } => (false, src),
309
- };
310
- let host = uri.host().map(|value| value.to_ascii_lowercase()).unwrap_or_default();
311
- if !self.allowed(&host) {
312
- return Some(message);
323
+ if !kind.contains("json") {
324
+ return Response::from_parts(parts, body);
313
325
  }
314
- let Message::Text(text) = message else {
315
- return Some(message);
326
+ let rewritten = match collect(body).await {
327
+ Ok(bytes) => self.rewrite("/v1/response", &[], bytes).await,
328
+ Err(cause) => Err(cause),
316
329
  };
317
- if self.ws_id.is_none() {
318
- self.ws_id = Some(format!("ws-{}", WS_IDS.fetch_add(1, Ordering::Relaxed)));
319
- }
320
- let id = self.ws_id.clone().unwrap_or_default();
321
- let direction = if outbound { "out" } else { "in" };
322
- let provider = provider_for(&host);
323
- match self
324
- .post_bytes(
325
- "/v1/ws",
326
- provider,
327
- &[
328
- ("x-piecemaker-ws", id.as_str()),
329
- ("x-piecemaker-direction", direction),
330
- ],
331
- Bytes::from(text.to_string()),
332
- )
333
- .await
334
- {
335
- Ok(rewritten) if !rewritten.is_empty() => {
336
- Some(Message::Text(String::from_utf8_lossy(&rewritten).into_owned().into()))
337
- }
338
- Ok(_) => None,
339
- Err(_) => None,
330
+ match rewritten {
331
+ Ok(bytes) => Response::from_parts(parts, Body::from(bytes)),
332
+ Err(cause) => refusal(StatusCode::BAD_GATEWAY, self.provider, "", cause),
340
333
  }
341
334
  }
335
+ }
342
336
 
337
+ impl WebSocketHandler for Handler {
343
338
  async fn handle_websocket(
344
339
  mut self,
345
340
  ctx: WebSocketContext,
346
- mut stream: impl futures::Stream<Item = Result<Message, hudsucker::tokio_tungstenite::tungstenite::Error>>
347
- + Unpin
348
- + Send
349
- + 'static,
350
- mut sink: impl futures::Sink<Message, Error = hudsucker::tokio_tungstenite::tungstenite::Error>
351
- + Unpin
352
- + Send
353
- + 'static,
341
+ mut stream: impl futures::Stream<Item = Result<Message, WsError>> + Unpin + Send + 'static,
342
+ mut sink: impl futures::Sink<Message, Error = WsError> + Unpin + Send + 'static,
354
343
  ) {
355
- use futures::SinkExt;
356
- while let Some(message) = stream.next().await {
357
- match message {
358
- Ok(message) => {
359
- let Some(message) = self.handle_message(&ctx, message).await else {
360
- continue;
361
- };
362
- let frames = match &ctx {
363
- WebSocketContext::ServerToClient { .. } => json_line_frames(message),
364
- WebSocketContext::ClientToServer { .. } => vec![message],
365
- };
366
- let mut closed = false;
367
- for frame in frames {
368
- if sink.send(frame).await.is_err() {
369
- closed = true;
370
- break;
371
- }
372
- }
373
- if closed {
374
- break;
344
+ let (outbound, uri) = match &ctx {
345
+ WebSocketContext::ClientToServer { dst, .. } => (true, dst),
346
+ WebSocketContext::ServerToClient { src, .. } => (false, src),
347
+ };
348
+ let target = self.intercepted_host(uri);
349
+ let intercepted = target.is_some();
350
+ let host = target.unwrap_or_default();
351
+
352
+ while let Some(Ok(message)) = stream.next().await {
353
+ let frames = match (intercepted, message) {
354
+ (false, message) => vec![message],
355
+ (true, Message::Text(text)) => match self.rewrite_ws(&host, outbound, &text).await {
356
+ Ok(Some(message)) if outbound => vec![message],
357
+ Ok(Some(message)) => json_line_frames(message),
358
+ Ok(None) => continue,
359
+ Err(cause) => {
360
+ close_ws(&mut sink, &host, cause).await;
361
+ return;
375
362
  }
363
+ },
364
+ (true, Message::Binary(_)) => {
365
+ close_ws(&mut sink, &host, "trame binaire impossible à anonymiser").await;
366
+ return;
367
+ }
368
+ (true, message) => vec![message],
369
+ };
370
+ for frame in frames {
371
+ if sink.send(frame).await.is_err() {
372
+ return;
376
373
  }
377
- Err(_) => break,
378
374
  }
379
375
  }
380
- if let Some(id) = self.ws_id.clone() {
381
- let host = match &ctx {
382
- WebSocketContext::ClientToServer { dst, .. } => dst.host().unwrap_or_default().to_string(),
383
- WebSocketContext::ServerToClient { src, .. } => src.host().unwrap_or_default().to_string(),
384
- };
385
- if let Ok(tail) = self
386
- .post_bytes(
387
- "/v1/ws-end",
388
- provider_for(&host_of(&host)),
389
- &[("x-piecemaker-ws", id.as_str())],
390
- Bytes::new(),
391
- )
392
- .await
393
- {
394
- if let Ok(messages) = serde_json_messages(&tail) {
395
- for message in messages {
396
- if sink.send(Message::Text(message.into())).await.is_err() {
397
- break;
398
- }
399
- }
376
+
377
+ if intercepted && !outbound {
378
+ for message in self.flush_ws(&host).await {
379
+ if sink.send(Message::Text(message.into())).await.is_err() {
380
+ return;
400
381
  }
401
382
  }
402
383
  }
403
384
  }
404
385
  }
405
386
 
387
+ async fn close_ws(sink: &mut (impl futures::Sink<Message, Error = WsError> + Unpin), host: &str, cause: &str) {
388
+ eprintln!("refus websocket {host} : {cause}");
389
+ let frame = CloseFrame {
390
+ code: CloseCode::Error,
391
+ reason: "PieceMaker: anonymisation impossible".into(),
392
+ };
393
+ let _ = sink.send(Message::Close(Some(frame))).await;
394
+ let _ = sink.close().await;
395
+ }
396
+
406
397
  fn json_line_frames(message: Message) -> Vec<Message> {
407
398
  let Message::Text(text) = &message else {
408
399
  return vec![message];
@@ -421,25 +412,23 @@ fn json_line_frames(message: Message) -> Vec<Message> {
421
412
  .collect()
422
413
  }
423
414
 
424
- fn serde_json_messages(bytes: &Bytes) -> Result<Vec<String>, ()> {
425
- let text = std::str::from_utf8(bytes).map_err(|_| ())?;
426
- let value: serde_json::Value = serde_json::from_str(text).map_err(|_| ())?;
427
- Ok(value
428
- .get("messages")
429
- .and_then(|entry| entry.as_array())
415
+ fn pending_messages(bytes: &Bytes) -> Vec<String> {
416
+ serde_json::from_slice::<serde_json::Value>(bytes)
417
+ .ok()
418
+ .and_then(|value| value.get("messages").and_then(|entry| entry.as_array()).cloned())
430
419
  .map(|entries| {
431
420
  entries
432
421
  .iter()
433
422
  .filter_map(|entry| entry.as_str().map(str::to_string))
434
423
  .collect()
435
424
  })
436
- .unwrap_or_default())
425
+ .unwrap_or_default()
437
426
  }
438
427
 
439
428
  fn parse_list(value: &str) -> HashSet<String> {
440
429
  value
441
430
  .split(',')
442
- .map(|entry| host_of(entry.trim()))
431
+ .map(host_of)
443
432
  .filter(|entry| !entry.is_empty())
444
433
  .collect()
445
434
  }
@@ -449,17 +438,30 @@ fn parse_map(value: &str) -> HashMap<String, String> {
449
438
  .split(',')
450
439
  .filter_map(|entry| {
451
440
  let (host, dest) = entry.split_once('=')?;
452
- let host = host_of(host.trim());
453
- let dest = dest.trim().to_string();
454
- if host.is_empty() || dest.is_empty() {
455
- None
456
- } else {
457
- Some((host, dest))
458
- }
441
+ let (host, dest) = (host_of(host), dest.trim().to_string());
442
+ (!host.is_empty() && !dest.is_empty()).then_some((host, dest))
459
443
  })
460
444
  .collect()
461
445
  }
462
446
 
447
+ fn exit_when_parent_dies() {
448
+ std::thread::spawn(|| {
449
+ let mut buffer = [0u8; 256];
450
+ let mut stdin = std::io::stdin();
451
+ loop {
452
+ match stdin.read(&mut buffer) {
453
+ Ok(0) | Err(_) => std::process::exit(0),
454
+ Ok(_) => {}
455
+ }
456
+ }
457
+ });
458
+ }
459
+
460
+ fn fail(message: String) -> ! {
461
+ eprintln!("{message}");
462
+ std::process::exit(1);
463
+ }
464
+
463
465
  #[tokio::main]
464
466
  async fn main() {
465
467
  let listen = arg("--listen");
@@ -469,63 +471,44 @@ async fn main() {
469
471
  let hosts = parse_list(&arg("--hosts"));
470
472
  let upstream = parse_map(&arg("--upstream-map"));
471
473
  if listen.is_empty() || rewriter.is_empty() || ca_cert.is_empty() || ca_key.is_empty() || hosts.is_empty() {
472
- eprintln!("usage: piecemaker-hudsucker --listen 127.0.0.1:4111 --rewriter http://127.0.0.1:port --ca-cert ca.crt --ca-key ca.key --hosts api.anthropic.com,api.openai.com,chatgpt.com");
474
+ eprintln!("usage: piecemaker-hudsucker --listen 127.0.0.1:0 --rewriter http://127.0.0.1:port --ca-cert ca.crt --ca-key ca.key --hosts api.anthropic.com,api.openai.com,chatgpt.com");
473
475
  std::process::exit(2);
474
476
  }
477
+ exit_when_parent_dies();
475
478
 
476
- let key = std::fs::read_to_string(&ca_key).unwrap_or_else(|error| {
477
- eprintln!("clé d'autorité illisible : {error}");
478
- std::process::exit(1);
479
- });
480
- let cert = std::fs::read_to_string(&ca_cert).unwrap_or_else(|error| {
481
- eprintln!("certificat d'autorité illisible : {error}");
482
- std::process::exit(1);
483
- });
484
- let key_pair = KeyPair::from_pem(&key).unwrap_or_else(|error| {
485
- eprintln!("clé d'autorité refusée : {error}");
486
- std::process::exit(1);
487
- });
488
- let issuer = Issuer::from_ca_cert_pem(&cert, key_pair).unwrap_or_else(|error| {
489
- eprintln!("certificat d'autorité refusé : {error}");
490
- std::process::exit(1);
491
- });
479
+ let key = std::fs::read_to_string(&ca_key).unwrap_or_else(|error| fail(format!("clé d'autorité illisible : {error}")));
480
+ let cert = std::fs::read_to_string(&ca_cert).unwrap_or_else(|error| fail(format!("certificat d'autorité illisible : {error}")));
481
+ let key_pair = KeyPair::from_pem(&key).unwrap_or_else(|error| fail(format!("clé d'autorité refusée : {error}")));
482
+ let issuer = Issuer::from_ca_cert_pem(&cert, key_pair).unwrap_or_else(|error| fail(format!("certificat d'autorité refusé : {error}")));
492
483
  let authority = RcgenAuthority::new(issuer, 1_000, aws_lc_rs::default_provider());
493
- let addr: SocketAddr = listen.parse().unwrap_or_else(|_| {
494
- eprintln!("adresse d'écoute invalide");
495
- std::process::exit(2);
496
- });
484
+ let addr: SocketAddr = listen.parse().unwrap_or_else(|_| fail("adresse d'écoute invalide".into()));
485
+ let listener = tokio::net::TcpListener::bind(addr)
486
+ .await
487
+ .unwrap_or_else(|error| fail(format!("écoute impossible sur {addr} : {error}")));
488
+ let bound = listener.local_addr().unwrap_or_else(|error| fail(format!("adresse d'écoute inconnue : {error}")));
497
489
  let http = reqwest::Client::builder()
498
490
  .no_proxy()
491
+ .connect_timeout(Duration::from_secs(2))
499
492
  .build()
500
- .unwrap_or_else(|error| {
501
- eprintln!("client interne : {error}");
502
- std::process::exit(1);
503
- });
493
+ .unwrap_or_else(|error| fail(format!("client interne : {error}")));
504
494
  let handler = Handler {
505
495
  rewriter,
506
496
  hosts: Arc::new(hosts),
507
497
  upstream: Arc::new(upstream),
508
498
  http,
509
- provider: "claude".to_string(),
499
+ provider: "claude",
510
500
  ws_id: None,
511
501
  };
512
- println!("listening {addr}");
513
- let proxy = match Proxy::builder()
514
- .with_addr(addr)
502
+ let proxy = Proxy::builder()
503
+ .with_listener(listener)
515
504
  .with_ca(authority)
516
505
  .with_rustls_connector(aws_lc_rs::default_provider())
517
506
  .with_http_handler(handler.clone())
518
507
  .with_websocket_handler(handler)
519
508
  .build()
520
- {
521
- Ok(proxy) => proxy,
522
- Err(error) => {
523
- eprintln!("proxy : {error}");
524
- std::process::exit(1);
525
- }
526
- };
509
+ .unwrap_or_else(|error| fail(format!("proxy : {error}")));
510
+ println!("listening {bound}");
527
511
  if let Err(error) = proxy.start().await {
528
- eprintln!("proxy arrêté : {error}");
529
- std::process::exit(1);
512
+ fail(format!("proxy arrêté : {error}"));
530
513
  }
531
514
  }