@piecemaker-legal/piecemaker 1.1.1 → 2.0.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/dist/assets/{abnfDiagram-VCTEODGH-DEXWh3LH.js → abnfDiagram-VCTEODGH-C2JDmlZx.js} +1 -1
- package/dist/assets/{arc-CoTRLXKW.js → arc-XhSg5Mrj.js} +1 -1
- package/dist/assets/{architectureDiagram-5GKGNRK7-Cg-jO9oR.js → architectureDiagram-5GKGNRK7-DKREuXxk.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-ChXwQSIq.js → blockDiagram-I7D4REHJ-CNK0Tp5d.js} +1 -1
- package/dist/assets/{c4Diagram-7LVT6UL2-Bw2VGggd.js → c4Diagram-7LVT6UL2-C8fJzWVp.js} +1 -1
- package/dist/assets/channel-DO5hoFdU.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-C1WdqrLP.js → chunk-2Q5K7J3B-DlE8nLJo.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BerUZBa1.js → chunk-5VM5RSS4-CSm-2TZ4.js} +1 -1
- package/dist/assets/{chunk-F27PBJKO-5W7ax2gB.js → chunk-F27PBJKO-CSgnXT2c.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-JgDN1h4T.js → chunk-IMKFNOWR-C6bkt6C1.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-ulNHnEdQ.js → chunk-JWPE2WC7-jUR52MBO.js} +1 -1
- package/dist/assets/{chunk-POPQ4Y6H-D4aTDC2T.js → chunk-POPQ4Y6H-D0UAL7O7.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG-BA5zX4XF.js → chunk-SVP7TREG-C9dtOEii.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-DhOULVhX.js → chunk-TICWLB2K-BEzWKT39.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CkpCgrVg.js → chunk-XXDRQBXY-uD1oklVY.js} +1 -1
- package/dist/assets/classDiagram-ZZMXUADV-BsQtwznI.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-BsQtwznI.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-0xUSfjfV.js → cose-bilkent-JH36ORCC-aGjlSdMe.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BE6fNu2_.js → cynefin-OW5HDTMX-D3G4OYOn.js} +1 -1
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-rFMnZ9b3.js → cynefinDiagram-5FMLGOSQ-9swRDUnu.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-C1p4ffd7.js → dagre-GXQ25YYZ-lvaAGdqY.js} +1 -1
- package/dist/assets/{diagram-S7CK7UJ4-BV_3Xj4O.js → diagram-S7CK7UJ4-BaZ6YIkR.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-Bb_Evkrm.js → diagram-UQ7AKVKN-COJ2Gb38.js} +1 -1
- package/dist/assets/{diagram-VSXAHHWV-BzgupjxD.js → diagram-VSXAHHWV-CYZEkbzP.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-D1c67z76.js → diagram-VX7I27RA-fFeQJ4ae.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-DsJAJGKE.js → diagram-Z3DM3KII-B7knszDV.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-B1ge_obq.js → ebnfDiagram-PWID7BFC-Dk0l329Y.js} +1 -1
- package/dist/assets/{erDiagram-RLTQ6QDP-DrV-tggw.js → erDiagram-RLTQ6QDP-CtpYxui8.js} +1 -1
- package/dist/assets/{flowDiagram-HODETNUW-CVzDiSQ3.js → flowDiagram-HODETNUW-aVWueRJD.js} +1 -1
- package/dist/assets/{ganttDiagram-EL5Y4UJY-CGRdSN9D.js → ganttDiagram-EL5Y4UJY-DfrX-B7p.js} +1 -1
- package/dist/assets/{gitGraphDiagram-WWUBYQGX-CAfvFXzL.js → gitGraphDiagram-WWUBYQGX-BFB1ZPF3.js} +1 -1
- package/dist/assets/{index-DaxH81ZJ.js → index-DqRygn9E.js} +170 -169
- package/dist/assets/index-Ds4HaAjz.css +1 -0
- package/dist/assets/{infoDiagram-27XIBGKW-DTshEkGK.js → infoDiagram-27XIBGKW-CuYkzm43.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-DkbshVxp.js → ishikawaDiagram-5VMMS53U-EMiw8Akh.js} +1 -1
- package/dist/assets/{journeyDiagram-3NMN7TZE-v5qE8xpn.js → journeyDiagram-3NMN7TZE-DCoLV1f8.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-a9CNMFLt.js → kanban-definition-UXKFOSKX-CyA4Yinu.js} +1 -1
- package/dist/assets/{layout-DrW4R1TH.js → layout-BlaB0grA.js} +1 -1
- package/dist/assets/{linear-Cs3qpXT_.js → linear-DLX3dJDH.js} +1 -1
- package/dist/assets/{mermaid.core-C7Ti-Fty.js → mermaid.core-Cy51CT89.js} +6 -6
- package/dist/assets/{mindmap-definition-YA3MSWOX-B0VDdIcG.js → mindmap-definition-YA3MSWOX-OBXjfL7I.js} +1 -1
- package/dist/assets/{pegDiagram-XKGWAZYB-CV1L7MjP.js → pegDiagram-XKGWAZYB-C_BJjUh1.js} +1 -1
- package/dist/assets/{pieDiagram-E7YTZNPT-DSCuzVTV.js → pieDiagram-E7YTZNPT-jIpbr3Fi.js} +1 -1
- package/dist/assets/{quadrantDiagram-AXDQQJYC-hzX-9AKo.js → quadrantDiagram-AXDQQJYC-C1A15xI5.js} +1 -1
- package/dist/assets/{railroadDiagram-O6MQD6OU-BbmzIqcp.js → railroadDiagram-O6MQD6OU-Rslzn1pI.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-CQCRRBSL.js → requirementDiagram-BXWQKSXE-LlfURO5-.js} +1 -1
- package/dist/assets/{sankeyDiagram-P5KCCOFB-kiaLcW6X.js → sankeyDiagram-P5KCCOFB-B5BPF4-O.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-Bgx8ST7Q.js → sequenceDiagram-WJ2MYXX4-orYZe9zX.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-BTjJ-ygN.js → sizeCapture-INFHLROL-Co-K5Cfb.js} +1 -1
- package/dist/assets/{stateDiagram-D77RDMKH-D48RR1FL.js → stateDiagram-D77RDMKH-COEx1aH2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-MP3YSRHH-B-XwSR-z.js → stateDiagram-v2-MP3YSRHH-DaeTZvmy.js} +1 -1
- package/dist/assets/{swimlanes-42K2YHIH-BhE-QuEZ.js → swimlanes-42K2YHIH-BHQaDhfZ.js} +1 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-BHW8q5M5.js +8 -0
- package/dist/assets/{timeline-definition-24CTP7MA-Cm7EUWWt.js → timeline-definition-24CTP7MA-COqCRwGX.js} +1 -1
- package/dist/assets/{vennDiagram-4TSXK5OY-X-nC9wZ0.js → vennDiagram-4TSXK5OY-nym5XSnr.js} +1 -1
- package/dist/assets/{wardleyDiagram-VM6X3IG4-D33YvEQZ.js → wardleyDiagram-VM6X3IG4-DEPPey_W.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-lziwBKsM.js → xychartDiagram-S5SC5T6Z-uSJ404kL.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-server/server/index.js +3 -1
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/piecemaker/index.js +7 -3
- package/dist-server/server/piecemaker/index.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/index.js +13 -6
- package/dist-server/server/piecemaker/knowledge/index.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/local-routes.js +55 -0
- package/dist-server/server/piecemaker/knowledge/local-routes.js.map +1 -0
- package/dist-server/server/piecemaker/knowledge/scan-jobs.js +3 -1
- package/dist-server/server/piecemaker/knowledge/scan-jobs.js.map +1 -1
- package/dist-server/server/piecemaker/library/index.js +14 -41
- package/dist-server/server/piecemaker/library/index.js.map +1 -1
- package/dist-server/server/piecemaker/library/store.js +11 -3
- package/dist-server/server/piecemaker/library/store.js.map +1 -1
- package/dist-server/server/piecemaker/library/workspace-installation.js +26 -3
- package/dist-server/server/piecemaker/library/workspace-installation.js.map +1 -1
- package/dist-server/server/piecemaker/timesheet/service.js +8 -7
- package/dist-server/server/piecemaker/timesheet/service.js.map +1 -1
- package/dist-server/server/piecemaker/timesheet/store.js +5 -3
- package/dist-server/server/piecemaker/timesheet/store.js.map +1 -1
- package/package.json +1 -1
- package/product.config.json +2 -0
- package/scripts/piecemaker/build-app-icon.py +88 -0
- package/scripts/piecemaker/cli/lib/piecemaker-anonymizer.mjs +2 -2
- package/scripts/piecemaker/hooks/proxy-guard.mjs +19 -54
- package/scripts/release/assert-piecemaker-version.mjs +25 -0
- package/server/index.ts +4 -1
- package/server/piecemaker/anonymizer/client-config.cjs +45 -162
- package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +127 -0
- package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.lock +2396 -0
- package/server/piecemaker/anonymizer/hudsucker-proxy/Cargo.toml +14 -0
- package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +501 -0
- package/server/piecemaker/anonymizer/providers.cjs +14 -135
- package/server/piecemaker/anonymizer/rewriter-bridge.cjs +221 -0
- package/server/piecemaker/anonymizer/service.cjs +144 -28
- package/server/piecemaker/index.ts +8 -3
- package/server/piecemaker/knowledge/index.ts +14 -6
- package/server/piecemaker/knowledge/local-routes.ts +72 -0
- package/server/piecemaker/knowledge/scan-jobs.ts +4 -1
- package/server/piecemaker/library/index.ts +14 -34
- package/server/piecemaker/library/store.ts +11 -3
- package/server/piecemaker/library/workspace-installation.ts +20 -3
- package/server/piecemaker/timesheet/service.ts +9 -7
- package/server/piecemaker/timesheet/store.ts +5 -3
- package/server/piecemaker/vendor/installer/assets/claude-hooks/piecemaker-guard-secrets.mjs +0 -1
- package/server/piecemaker/vendor/installer/assets/piecemaker.icns +0 -0
- package/server/piecemaker/vendor/installer/assets/piecemaker.ico +0 -0
- package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +56 -206
- package/server/piecemaker/vendor/installer/lib/commandes.mjs +2 -4
- package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +74 -0
- package/server/piecemaker/vendor/installer/lib/service.mjs +4 -358
- package/server/piecemaker/vendor/installer/lib/state.mjs +0 -6
- package/server/piecemaker/vendor/installer/steps/03-python-gliner.mjs +4 -4
- package/server/piecemaker/vendor/installer/steps/09-claude-assets.mjs +0 -31
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/statusline.mjs +3 -4
- package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +1 -49
- package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +14 -12
- package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +17 -476
- package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +2 -2
- package/server/piecemaker/vendor/websocket-server/scripts/mineru_improved_1.py +11 -2
- package/server/piecemaker/vendor/websocket-server/scripts/presidio-gliner/scanner_worker.py +13 -37
- package/server/piecemaker/vendor/websocket-server/scripts/requirements.txt +1 -7
- package/server/piecemaker/vendor/websocket-server/scripts/warmup.py +14 -85
- package/dist/assets/channel-DdzrPZ6Y.js +0 -1
- package/dist/assets/classDiagram-ZZMXUADV-Dim2o8E5.js +0 -1
- package/dist/assets/classDiagram-v2-VYDZK3BY-Dim2o8E5.js +0 -1
- package/dist/assets/index-D_Dc_nQv.css +0 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-DR525d5V.js +0 -8
- package/server/piecemaker/anonymizer/proxy.cjs +0 -365
- package/server/piecemaker/vendor/installer/assets/PieceMakerApp.swift +0 -189
- package/server/piecemaker/vendor/installer/lib/desktop-launcher.mjs +0 -305
- package/server/piecemaker/vendor/installer/steps/05-certificats.mjs +0 -171
- package/server/piecemaker/vendor/installer/steps/14-mxc-sandbox.mjs +0 -271
- package/server/piecemaker/vendor/installer/steps/15-pwa-desktop.mjs +0 -64
- package/server/piecemaker/vendor/installer/templates/root-CLAUDE.md +0 -56
- package/server/piecemaker/vendor/installer/templates/workspace-CLAUDE.md +0 -52
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "piecemaker-hudsucker"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[dependencies]
|
|
7
|
+
bytes = "1"
|
|
8
|
+
futures = "0.3"
|
|
9
|
+
http-body-util = "0.1"
|
|
10
|
+
hudsucker = { version = "0.25", default-features = false, features = ["decoder", "http2", "rcgen-ca", "rustls-client"] }
|
|
11
|
+
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream"] }
|
|
12
|
+
serde_json = "1"
|
|
13
|
+
tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] }
|
|
14
|
+
tokio-stream = "0.1"
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
use std::collections::{HashMap, HashSet};
|
|
2
|
+
use std::net::SocketAddr;
|
|
3
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
4
|
+
use std::sync::Arc;
|
|
5
|
+
|
|
6
|
+
use bytes::Bytes;
|
|
7
|
+
use futures::StreamExt;
|
|
8
|
+
use http_body_util::{BodyExt, BodyStream};
|
|
9
|
+
use hudsucker::certificate_authority::RcgenAuthority;
|
|
10
|
+
use hudsucker::hyper::{Request, Response, StatusCode, Uri};
|
|
11
|
+
use hudsucker::rcgen::{Issuer, KeyPair};
|
|
12
|
+
use hudsucker::rustls::crypto::aws_lc_rs;
|
|
13
|
+
use hudsucker::tokio_tungstenite::tungstenite::Message;
|
|
14
|
+
use hudsucker::{
|
|
15
|
+
Body, HttpContext, HttpHandler, Proxy, RequestOrResponse, WebSocketContext, WebSocketHandler,
|
|
16
|
+
};
|
|
17
|
+
use tokio::sync::mpsc;
|
|
18
|
+
use tokio_stream::wrappers::ReceiverStream;
|
|
19
|
+
|
|
20
|
+
const MAX_BODY: usize = 64 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
static WS_IDS: AtomicU64 = AtomicU64::new(1);
|
|
23
|
+
|
|
24
|
+
#[derive(Clone)]
|
|
25
|
+
struct Handler {
|
|
26
|
+
rewriter: String,
|
|
27
|
+
hosts: Arc<HashSet<String>>,
|
|
28
|
+
upstream: Arc<HashMap<String, String>>,
|
|
29
|
+
http: reqwest::Client,
|
|
30
|
+
provider: String,
|
|
31
|
+
ws_id: Option<String>,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn arg(name: &str) -> String {
|
|
35
|
+
let mut args = std::env::args().skip(1);
|
|
36
|
+
while let Some(key) = args.next() {
|
|
37
|
+
if key == name {
|
|
38
|
+
return args.next().unwrap_or_default();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
String::new()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn host_of(value: &str) -> String {
|
|
45
|
+
value
|
|
46
|
+
.split(':')
|
|
47
|
+
.next()
|
|
48
|
+
.unwrap_or(value)
|
|
49
|
+
.trim()
|
|
50
|
+
.to_ascii_lowercase()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn request_host(req: &Request<Body>) -> String {
|
|
54
|
+
if let Some(host) = req.uri().host() {
|
|
55
|
+
return host.to_ascii_lowercase();
|
|
56
|
+
}
|
|
57
|
+
if let Some(authority) = req.uri().authority() {
|
|
58
|
+
return authority.host().to_ascii_lowercase();
|
|
59
|
+
}
|
|
60
|
+
req.headers()
|
|
61
|
+
.get("host")
|
|
62
|
+
.and_then(|value| value.to_str().ok())
|
|
63
|
+
.map(host_of)
|
|
64
|
+
.unwrap_or_default()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn provider_for(host: &str) -> &'static str {
|
|
68
|
+
match host {
|
|
69
|
+
"chatgpt.com" => "codex",
|
|
70
|
+
"api.openai.com" => "opencode",
|
|
71
|
+
_ => "claude",
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
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()
|
|
85
|
+
.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")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn failure() -> Response<Body> {
|
|
107
|
+
Response::builder()
|
|
108
|
+
.status(StatusCode::BAD_GATEWAY)
|
|
109
|
+
.header("content-type", "application/json")
|
|
110
|
+
.body(Body::from(
|
|
111
|
+
"{\"error\":{\"type\":\"piecemaker_proxy\",\"message\":\"Relais d'anonymisation indisponible.\"}}",
|
|
112
|
+
))
|
|
113
|
+
.unwrap()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn uri_path(uri: &Uri) -> String {
|
|
117
|
+
match uri.path_and_query() {
|
|
118
|
+
Some(path) if !path.as_str().is_empty() => path.as_str().to_string(),
|
|
119
|
+
_ => "/".to_string(),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
impl Handler {
|
|
124
|
+
fn allowed(&self, host: &str) -> bool {
|
|
125
|
+
self.hosts.contains(host)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn point_upstream(&self, host: &str, req: &mut Request<Body>) {
|
|
129
|
+
let Some(dest) = self.upstream.get(host) else {
|
|
130
|
+
return;
|
|
131
|
+
};
|
|
132
|
+
let path = uri_path(req.uri());
|
|
133
|
+
let rewritten = format!("http://{dest}{path}");
|
|
134
|
+
if let Ok(uri) = rewritten.parse() {
|
|
135
|
+
*req.uri_mut() = uri;
|
|
136
|
+
}
|
|
137
|
+
if let Ok(value) = dest.parse() {
|
|
138
|
+
req.headers_mut().insert("host", value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async fn post_bytes(&self, path: &str, provider: &str, extra: &[(&str, &str)], body: Bytes) -> Result<Bytes, ()> {
|
|
143
|
+
let mut request = self
|
|
144
|
+
.http
|
|
145
|
+
.post(format!("{}{path}", self.rewriter))
|
|
146
|
+
.header("x-piecemaker-provider", provider)
|
|
147
|
+
.body(body);
|
|
148
|
+
for (name, value) in extra {
|
|
149
|
+
request = request.header(*name, *value);
|
|
150
|
+
}
|
|
151
|
+
let response = request.send().await.map_err(|_| ())?;
|
|
152
|
+
if !response.status().is_success() {
|
|
153
|
+
return Err(());
|
|
154
|
+
}
|
|
155
|
+
response.bytes().await.map_err(|_| ())
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn spawn_sse(&self, provider: String, body: Body) -> Body {
|
|
159
|
+
let http = self.http.clone();
|
|
160
|
+
let url = format!("{}/v1/sse", self.rewriter);
|
|
161
|
+
let (tx, rx) = mpsc::channel::<Result<Bytes, std::io::Error>>(8);
|
|
162
|
+
tokio::spawn(async move {
|
|
163
|
+
let incoming = BodyStream::new(body).filter_map(|frame| async move {
|
|
164
|
+
match frame {
|
|
165
|
+
Ok(frame) => frame.into_data().ok().map(Ok),
|
|
166
|
+
Err(error) => Some(Err(std::io::Error::other(error.to_string()))),
|
|
167
|
+
}
|
|
168
|
+
});
|
|
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 {
|
|
176
|
+
Ok(response) => {
|
|
177
|
+
let mut stream = response.bytes_stream();
|
|
178
|
+
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() {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
Err(error) => {
|
|
186
|
+
let _ = tx.send(Err(std::io::Error::other(error.to_string()))).await;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
Body::from_stream(ReceiverStream::new(rx))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
impl HttpHandler for Handler {
|
|
195
|
+
async fn should_intercept_connect(&mut self, _ctx: &HttpContext, req: &Request<Body>) -> bool {
|
|
196
|
+
self.allowed(&request_host(req))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async fn should_intercept_tls(
|
|
200
|
+
&mut self,
|
|
201
|
+
_ctx: &HttpContext,
|
|
202
|
+
client_hello: hudsucker::rustls::server::ClientHello<'_>,
|
|
203
|
+
) -> bool {
|
|
204
|
+
client_hello
|
|
205
|
+
.server_name()
|
|
206
|
+
.map(|name| self.allowed(&name.to_ascii_lowercase()))
|
|
207
|
+
.unwrap_or(false)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
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
|
+
let host = request_host(&req);
|
|
224
|
+
if !self.allowed(&host) {
|
|
225
|
+
return req.into();
|
|
226
|
+
}
|
|
227
|
+
self.provider = provider_for(&host).to_string();
|
|
228
|
+
|
|
229
|
+
let content_type = header_text(&req, "content-type");
|
|
230
|
+
let (mut parts, body) = req.into_parts();
|
|
231
|
+
parts.headers.remove("accept-encoding");
|
|
232
|
+
|
|
233
|
+
let body = if is_json(&content_type.to_ascii_lowercase()) {
|
|
234
|
+
let collected = match body.collect().await {
|
|
235
|
+
Ok(collected) => collected.to_bytes(),
|
|
236
|
+
Err(_) => return failure().into(),
|
|
237
|
+
};
|
|
238
|
+
if collected.len() > MAX_BODY {
|
|
239
|
+
return failure().into();
|
|
240
|
+
}
|
|
241
|
+
if collected.is_empty() {
|
|
242
|
+
Body::empty()
|
|
243
|
+
} else {
|
|
244
|
+
match self
|
|
245
|
+
.post_bytes("/v1/request", &self.provider.clone(), &[], collected)
|
|
246
|
+
.await
|
|
247
|
+
{
|
|
248
|
+
Ok(rewritten) => {
|
|
249
|
+
if let Ok(length) = rewritten.len().to_string().parse() {
|
|
250
|
+
parts.headers.insert("content-length", length);
|
|
251
|
+
}
|
|
252
|
+
Body::from(rewritten)
|
|
253
|
+
}
|
|
254
|
+
Err(_) => return failure().into(),
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
body
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
let mut req = Request::from_parts(parts, body);
|
|
262
|
+
self.point_upstream(&host, &mut req);
|
|
263
|
+
req.into()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async fn handle_response(&mut self, _ctx: &HttpContext, res: Response<Body>) -> Response<Body> {
|
|
267
|
+
let kind = response_type(&res);
|
|
268
|
+
let provider = self.provider.clone();
|
|
269
|
+
let (mut parts, body) = res.into_parts();
|
|
270
|
+
parts.headers.remove("content-encoding");
|
|
271
|
+
parts.headers.remove("content-length");
|
|
272
|
+
parts.headers.remove("transfer-encoding");
|
|
273
|
+
parts.headers.remove("connection");
|
|
274
|
+
|
|
275
|
+
if is_sse(&kind) {
|
|
276
|
+
parts.headers.remove("content-length");
|
|
277
|
+
parts.headers.insert(
|
|
278
|
+
"content-type",
|
|
279
|
+
"text/event-stream".parse().unwrap(),
|
|
280
|
+
);
|
|
281
|
+
return Response::from_parts(parts, self.spawn_sse(provider, body));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if is_json(&kind) {
|
|
285
|
+
let collected = match body.collect().await {
|
|
286
|
+
Ok(collected) => collected.to_bytes(),
|
|
287
|
+
Err(_) => return failure(),
|
|
288
|
+
};
|
|
289
|
+
if collected.len() > MAX_BODY {
|
|
290
|
+
return failure();
|
|
291
|
+
}
|
|
292
|
+
let rewritten = match self.post_bytes("/v1/response", &provider, &[], collected).await {
|
|
293
|
+
Ok(rewritten) => rewritten,
|
|
294
|
+
Err(_) => return failure(),
|
|
295
|
+
};
|
|
296
|
+
return Response::from_parts(parts, Body::from(rewritten));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
Response::from_parts(parts, body)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
impl WebSocketHandler for Handler {
|
|
304
|
+
async fn handle_message(&mut self, ctx: &WebSocketContext, message: Message) -> Option<Message> {
|
|
305
|
+
let (outbound, uri) = match ctx {
|
|
306
|
+
WebSocketContext::ClientToServer { dst, .. } => (true, dst),
|
|
307
|
+
WebSocketContext::ServerToClient { src, .. } => (false, src),
|
|
308
|
+
};
|
|
309
|
+
let host = uri.host().map(|value| value.to_ascii_lowercase()).unwrap_or_default();
|
|
310
|
+
if !self.allowed(&host) {
|
|
311
|
+
return Some(message);
|
|
312
|
+
}
|
|
313
|
+
let Message::Text(text) = message else {
|
|
314
|
+
return Some(message);
|
|
315
|
+
};
|
|
316
|
+
if self.ws_id.is_none() {
|
|
317
|
+
self.ws_id = Some(format!("ws-{}", WS_IDS.fetch_add(1, Ordering::Relaxed)));
|
|
318
|
+
}
|
|
319
|
+
let id = self.ws_id.clone().unwrap_or_default();
|
|
320
|
+
let direction = if outbound { "out" } else { "in" };
|
|
321
|
+
let provider = provider_for(&host);
|
|
322
|
+
match self
|
|
323
|
+
.post_bytes(
|
|
324
|
+
"/v1/ws",
|
|
325
|
+
provider,
|
|
326
|
+
&[
|
|
327
|
+
("x-piecemaker-ws", id.as_str()),
|
|
328
|
+
("x-piecemaker-direction", direction),
|
|
329
|
+
],
|
|
330
|
+
Bytes::from(text.to_string()),
|
|
331
|
+
)
|
|
332
|
+
.await
|
|
333
|
+
{
|
|
334
|
+
Ok(rewritten) if !rewritten.is_empty() => {
|
|
335
|
+
Some(Message::Text(String::from_utf8_lossy(&rewritten).into_owned().into()))
|
|
336
|
+
}
|
|
337
|
+
Ok(_) => None,
|
|
338
|
+
Err(_) => None,
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async fn handle_websocket(
|
|
343
|
+
mut self,
|
|
344
|
+
ctx: WebSocketContext,
|
|
345
|
+
mut stream: impl futures::Stream<Item = Result<Message, hudsucker::tokio_tungstenite::tungstenite::Error>>
|
|
346
|
+
+ Unpin
|
|
347
|
+
+ Send
|
|
348
|
+
+ 'static,
|
|
349
|
+
mut sink: impl futures::Sink<Message, Error = hudsucker::tokio_tungstenite::tungstenite::Error>
|
|
350
|
+
+ Unpin
|
|
351
|
+
+ Send
|
|
352
|
+
+ 'static,
|
|
353
|
+
) {
|
|
354
|
+
use futures::SinkExt;
|
|
355
|
+
while let Some(message) = stream.next().await {
|
|
356
|
+
match message {
|
|
357
|
+
Ok(message) => {
|
|
358
|
+
let Some(message) = self.handle_message(&ctx, message).await else {
|
|
359
|
+
continue;
|
|
360
|
+
};
|
|
361
|
+
if sink.send(message).await.is_err() {
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
Err(_) => break,
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if let Some(id) = self.ws_id.clone() {
|
|
369
|
+
let host = match &ctx {
|
|
370
|
+
WebSocketContext::ClientToServer { dst, .. } => dst.host().unwrap_or_default().to_string(),
|
|
371
|
+
WebSocketContext::ServerToClient { src, .. } => src.host().unwrap_or_default().to_string(),
|
|
372
|
+
};
|
|
373
|
+
if let Ok(tail) = self
|
|
374
|
+
.post_bytes(
|
|
375
|
+
"/v1/ws-end",
|
|
376
|
+
provider_for(&host_of(&host)),
|
|
377
|
+
&[("x-piecemaker-ws", id.as_str())],
|
|
378
|
+
Bytes::new(),
|
|
379
|
+
)
|
|
380
|
+
.await
|
|
381
|
+
{
|
|
382
|
+
if let Ok(messages) = serde_json_messages(&tail) {
|
|
383
|
+
for message in messages {
|
|
384
|
+
if sink.send(Message::Text(message.into())).await.is_err() {
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
fn serde_json_messages(bytes: &Bytes) -> Result<Vec<String>, ()> {
|
|
395
|
+
let text = std::str::from_utf8(bytes).map_err(|_| ())?;
|
|
396
|
+
let value: serde_json::Value = serde_json::from_str(text).map_err(|_| ())?;
|
|
397
|
+
Ok(value
|
|
398
|
+
.get("messages")
|
|
399
|
+
.and_then(|entry| entry.as_array())
|
|
400
|
+
.map(|entries| {
|
|
401
|
+
entries
|
|
402
|
+
.iter()
|
|
403
|
+
.filter_map(|entry| entry.as_str().map(str::to_string))
|
|
404
|
+
.collect()
|
|
405
|
+
})
|
|
406
|
+
.unwrap_or_default())
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
fn parse_list(value: &str) -> HashSet<String> {
|
|
410
|
+
value
|
|
411
|
+
.split(',')
|
|
412
|
+
.map(|entry| host_of(entry.trim()))
|
|
413
|
+
.filter(|entry| !entry.is_empty())
|
|
414
|
+
.collect()
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
fn parse_map(value: &str) -> HashMap<String, String> {
|
|
418
|
+
value
|
|
419
|
+
.split(',')
|
|
420
|
+
.filter_map(|entry| {
|
|
421
|
+
let (host, dest) = entry.split_once('=')?;
|
|
422
|
+
let host = host_of(host.trim());
|
|
423
|
+
let dest = dest.trim().to_string();
|
|
424
|
+
if host.is_empty() || dest.is_empty() {
|
|
425
|
+
None
|
|
426
|
+
} else {
|
|
427
|
+
Some((host, dest))
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
.collect()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
#[tokio::main]
|
|
434
|
+
async fn main() {
|
|
435
|
+
let listen = arg("--listen");
|
|
436
|
+
let rewriter = arg("--rewriter");
|
|
437
|
+
let ca_cert = arg("--ca-cert");
|
|
438
|
+
let ca_key = arg("--ca-key");
|
|
439
|
+
let hosts = parse_list(&arg("--hosts"));
|
|
440
|
+
let upstream = parse_map(&arg("--upstream-map"));
|
|
441
|
+
if listen.is_empty() || rewriter.is_empty() || ca_cert.is_empty() || ca_key.is_empty() || hosts.is_empty() {
|
|
442
|
+
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");
|
|
443
|
+
std::process::exit(2);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
let key = std::fs::read_to_string(&ca_key).unwrap_or_else(|error| {
|
|
447
|
+
eprintln!("clé d'autorité illisible : {error}");
|
|
448
|
+
std::process::exit(1);
|
|
449
|
+
});
|
|
450
|
+
let cert = std::fs::read_to_string(&ca_cert).unwrap_or_else(|error| {
|
|
451
|
+
eprintln!("certificat d'autorité illisible : {error}");
|
|
452
|
+
std::process::exit(1);
|
|
453
|
+
});
|
|
454
|
+
let key_pair = KeyPair::from_pem(&key).unwrap_or_else(|error| {
|
|
455
|
+
eprintln!("clé d'autorité refusée : {error}");
|
|
456
|
+
std::process::exit(1);
|
|
457
|
+
});
|
|
458
|
+
let issuer = Issuer::from_ca_cert_pem(&cert, key_pair).unwrap_or_else(|error| {
|
|
459
|
+
eprintln!("certificat d'autorité refusé : {error}");
|
|
460
|
+
std::process::exit(1);
|
|
461
|
+
});
|
|
462
|
+
let authority = RcgenAuthority::new(issuer, 1_000, aws_lc_rs::default_provider());
|
|
463
|
+
let addr: SocketAddr = listen.parse().unwrap_or_else(|_| {
|
|
464
|
+
eprintln!("adresse d'écoute invalide");
|
|
465
|
+
std::process::exit(2);
|
|
466
|
+
});
|
|
467
|
+
let http = reqwest::Client::builder()
|
|
468
|
+
.no_proxy()
|
|
469
|
+
.build()
|
|
470
|
+
.unwrap_or_else(|error| {
|
|
471
|
+
eprintln!("client interne : {error}");
|
|
472
|
+
std::process::exit(1);
|
|
473
|
+
});
|
|
474
|
+
let handler = Handler {
|
|
475
|
+
rewriter,
|
|
476
|
+
hosts: Arc::new(hosts),
|
|
477
|
+
upstream: Arc::new(upstream),
|
|
478
|
+
http,
|
|
479
|
+
provider: "claude".to_string(),
|
|
480
|
+
ws_id: None,
|
|
481
|
+
};
|
|
482
|
+
println!("listening {addr}");
|
|
483
|
+
let proxy = match Proxy::builder()
|
|
484
|
+
.with_addr(addr)
|
|
485
|
+
.with_ca(authority)
|
|
486
|
+
.with_rustls_connector(aws_lc_rs::default_provider())
|
|
487
|
+
.with_http_handler(handler.clone())
|
|
488
|
+
.with_websocket_handler(handler)
|
|
489
|
+
.build()
|
|
490
|
+
{
|
|
491
|
+
Ok(proxy) => proxy,
|
|
492
|
+
Err(error) => {
|
|
493
|
+
eprintln!("proxy : {error}");
|
|
494
|
+
std::process::exit(1);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
if let Err(error) = proxy.start().await {
|
|
498
|
+
eprintln!("proxy arrêté : {error}");
|
|
499
|
+
std::process::exit(1);
|
|
500
|
+
}
|
|
501
|
+
}
|