jsonrpc_debugger/
app.rs

1use ratatui::widgets::TableState;
2use std::collections::HashMap;
3use tokio::sync::{mpsc, oneshot};
4
5#[derive(Debug, Clone)]
6pub struct JsonRpcMessage {
7    pub id: Option<serde_json::Value>,
8    pub method: Option<String>,
9    pub params: Option<serde_json::Value>,
10    pub result: Option<serde_json::Value>,
11    pub error: Option<serde_json::Value>,
12    pub timestamp: std::time::SystemTime,
13    pub direction: MessageDirection,
14    pub transport: TransportType,
15    pub headers: Option<HashMap<String, String>>,
16}
17
18#[derive(Debug, Clone)]
19pub struct JsonRpcExchange {
20    pub id: Option<serde_json::Value>,
21    pub method: Option<String>,
22    pub request: Option<JsonRpcMessage>,
23    pub response: Option<JsonRpcMessage>,
24    #[allow(dead_code)] // Used in UI for duration calculation
25    pub timestamp: std::time::SystemTime,
26    pub transport: TransportType,
27}
28
29#[derive(Debug, Clone)]
30pub enum MessageDirection {
31    Request,
32    Response,
33}
34
35#[derive(Debug, Clone)]
36pub enum TransportType {
37    Http,
38    #[allow(dead_code)] // Used in tests and UI display
39    WebSocket,
40}
41
42#[derive(Debug, Clone, PartialEq)]
43pub enum InputMode {
44    Normal,
45    EditingTarget,
46}
47
48#[derive(Debug, Clone, PartialEq)]
49pub enum AppMode {
50    Normal,       // Regular proxy mode
51    Paused,       // All requests paused
52    Intercepting, // Inspecting a specific request
53}
54
55#[derive(Debug)]
56pub enum ProxyDecision {
57    Allow(Option<serde_json::Value>, Option<HashMap<String, String>>), // Allow with optional modified JSON and headers
58    Block,                                                             // Block the request
59    Complete(serde_json::Value), // Complete with custom response
60}
61
62#[allow(dead_code)]
63pub struct PendingRequest {
64    pub id: String,
65    pub original_request: JsonRpcMessage,
66    pub modified_request: Option<String>, // JSON string for editing
67    pub modified_headers: Option<HashMap<String, String>>, // Modified headers
68    pub decision_sender: oneshot::Sender<ProxyDecision>,
69}
70
71#[allow(dead_code)]
72pub struct App {
73    pub exchanges: Vec<JsonRpcExchange>,
74    pub selected_exchange: usize,
75    pub table_state: TableState,
76    pub details_scroll: usize,
77    pub intercept_details_scroll: usize, // New field for intercept details scrolling
78    pub proxy_config: ProxyConfig,
79    pub is_running: bool,
80    pub message_receiver: Option<mpsc::UnboundedReceiver<JsonRpcMessage>>,
81    pub input_mode: InputMode,
82    pub input_buffer: String,
83    pub app_mode: AppMode,                     // New field
84    pub pending_requests: Vec<PendingRequest>, // New field
85    pub selected_pending: usize,               // New field
86    pub request_editor_buffer: String,         // New field
87}
88
89#[derive(Debug)]
90#[allow(dead_code)]
91pub struct ProxyConfig {
92    pub listen_port: u16,
93    pub target_url: String,
94    pub transport: TransportType,
95}
96
97impl Default for App {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103impl App {
104    pub fn new() -> Self {
105        let mut table_state = TableState::default();
106        table_state.select(Some(0));
107
108        Self {
109            exchanges: Vec::new(),
110            selected_exchange: 0,
111            table_state,
112            details_scroll: 0,
113            intercept_details_scroll: 0,
114            proxy_config: ProxyConfig {
115                listen_port: 8080,
116                target_url: "".to_string(),
117                transport: TransportType::Http,
118            },
119            is_running: true,
120            message_receiver: None,
121            input_mode: InputMode::Normal,
122            input_buffer: String::new(),
123            app_mode: AppMode::Normal,
124            pending_requests: Vec::new(),
125            selected_pending: 0,
126            request_editor_buffer: String::new(),
127        }
128    }
129
130    pub fn new_with_receiver(receiver: mpsc::UnboundedReceiver<JsonRpcMessage>) -> Self {
131        let mut table_state = TableState::default();
132        table_state.select(Some(0));
133
134        Self {
135            exchanges: Vec::new(),
136            selected_exchange: 0,
137            table_state,
138            details_scroll: 0,
139            intercept_details_scroll: 0,
140            proxy_config: ProxyConfig {
141                listen_port: 8080,
142                target_url: "".to_string(),
143                transport: TransportType::Http,
144            },
145            is_running: true,
146            message_receiver: Some(receiver),
147            input_mode: InputMode::Normal,
148            input_buffer: String::new(),
149            app_mode: AppMode::Normal,
150            pending_requests: Vec::new(),
151            selected_pending: 0,
152            request_editor_buffer: String::new(),
153        }
154    }
155
156    pub fn check_for_new_messages(&mut self) {
157        if let Some(receiver) = &mut self.message_receiver {
158            let mut new_messages = Vec::new();
159            while let Ok(message) = receiver.try_recv() {
160                new_messages.push(message);
161            }
162            for message in new_messages {
163                self.add_message(message);
164            }
165        }
166    }
167
168    pub fn add_message(&mut self, mut message: JsonRpcMessage) {
169        // Sanitize message content to prevent UI corruption
170        if let Some(ref mut error) = message.error {
171            if let Some(data) = error.get_mut("data") {
172                if let Some(data_str) = data.as_str() {
173                    let sanitized = data_str
174                        .chars()
175                        .filter(|c| c.is_ascii() && (!c.is_control() || *c == '\n' || *c == '\t'))
176                        .take(500)
177                        .collect::<String>();
178                    *data = serde_json::Value::String(sanitized);
179                }
180            }
181        }
182
183        match message.direction {
184            MessageDirection::Request => {
185                // Create a new exchange for the request
186                let exchange = JsonRpcExchange {
187                    id: message.id.clone(),
188                    method: message.method.clone(),
189                    request: Some(message.clone()),
190                    response: None,
191                    timestamp: message.timestamp,
192                    transport: message.transport.clone(),
193                };
194                self.exchanges.push(exchange);
195            }
196            MessageDirection::Response => {
197                // Find matching request by ID and add response
198                if let Some(exchange) = self
199                    .exchanges
200                    .iter_mut()
201                    .rev()
202                    .find(|e| e.id == message.id && e.response.is_none())
203                {
204                    exchange.response = Some(message);
205                } else {
206                    // No matching request found, create exchange with just response
207                    let exchange = JsonRpcExchange {
208                        id: message.id.clone(),
209                        method: None,
210                        request: None,
211                        response: Some(message.clone()),
212                        timestamp: message.timestamp,
213                        transport: message.transport.clone(),
214                    };
215                    self.exchanges.push(exchange);
216                }
217            }
218        }
219    }
220
221    pub fn get_selected_exchange(&self) -> Option<&JsonRpcExchange> {
222        self.exchanges.get(self.selected_exchange)
223    }
224
225    pub fn select_next(&mut self) {
226        if !self.exchanges.is_empty() {
227            self.selected_exchange = (self.selected_exchange + 1) % self.exchanges.len();
228            self.table_state.select(Some(self.selected_exchange));
229            self.reset_details_scroll();
230        }
231    }
232
233    pub fn select_previous(&mut self) {
234        if !self.exchanges.is_empty() {
235            self.selected_exchange = if self.selected_exchange == 0 {
236                self.exchanges.len() - 1
237            } else {
238                self.selected_exchange - 1
239            };
240            self.table_state.select(Some(self.selected_exchange));
241            self.reset_details_scroll();
242        }
243    }
244
245    pub fn toggle_proxy(&mut self) {
246        self.is_running = !self.is_running;
247    }
248
249    pub fn scroll_details_up(&mut self) {
250        if self.details_scroll > 0 {
251            self.details_scroll -= 1;
252        }
253    }
254
255    pub fn scroll_details_down(&mut self, max_lines: usize, visible_lines: usize) {
256        if max_lines > visible_lines && self.details_scroll < max_lines - visible_lines {
257            self.details_scroll += 1;
258        }
259    }
260
261    pub fn reset_details_scroll(&mut self) {
262        self.details_scroll = 0;
263    }
264
265    // Intercept details scrolling methods
266    pub fn scroll_intercept_details_up(&mut self) {
267        if self.intercept_details_scroll > 0 {
268            self.intercept_details_scroll -= 1;
269        }
270    }
271
272    pub fn scroll_intercept_details_down(&mut self, max_lines: usize, visible_lines: usize) {
273        if max_lines > visible_lines && self.intercept_details_scroll < max_lines - visible_lines {
274            self.intercept_details_scroll += 1;
275        }
276    }
277
278    pub fn reset_intercept_details_scroll(&mut self) {
279        self.intercept_details_scroll = 0;
280    }
281
282    pub fn page_down_intercept_details(&mut self, visible_lines: usize) {
283        let page_size = visible_lines / 2; // Half page
284        self.intercept_details_scroll += page_size;
285    }
286
287    pub fn page_up_intercept_details(&mut self) {
288        let page_size = 10; // Half page
289        self.intercept_details_scroll = self.intercept_details_scroll.saturating_sub(page_size);
290    }
291
292    pub fn goto_top_intercept_details(&mut self) {
293        self.intercept_details_scroll = 0;
294    }
295
296    pub fn goto_bottom_intercept_details(&mut self, max_lines: usize, visible_lines: usize) {
297        if max_lines > visible_lines {
298            self.intercept_details_scroll = max_lines - visible_lines;
299        }
300    }
301
302    // Enhanced details scrolling with vim-style page jumps
303    pub fn page_down_details(&mut self, visible_lines: usize) {
304        let page_size = visible_lines / 2; // Half page
305        self.details_scroll += page_size;
306    }
307
308    pub fn page_up_details(&mut self) {
309        let page_size = 10; // Half page
310        self.details_scroll = self.details_scroll.saturating_sub(page_size);
311    }
312
313    pub fn goto_top_details(&mut self) {
314        self.details_scroll = 0;
315    }
316
317    pub fn goto_bottom_details(&mut self, max_lines: usize, visible_lines: usize) {
318        if max_lines > visible_lines {
319            self.details_scroll = max_lines - visible_lines;
320        }
321    }
322
323    // Get content lines for proper scrolling calculations
324    // Target editing methods
325    pub fn start_editing_target(&mut self) {
326        self.input_mode = InputMode::EditingTarget;
327        self.input_buffer.clear();
328    }
329
330    pub fn cancel_editing(&mut self) {
331        self.input_mode = InputMode::Normal;
332        self.input_buffer.clear();
333    }
334
335    pub fn confirm_target_edit(&mut self) {
336        if !self.input_buffer.trim().is_empty() {
337            self.proxy_config.target_url = self.input_buffer.trim().to_string();
338        }
339        self.input_mode = InputMode::Normal;
340        self.input_buffer.clear();
341    }
342
343    pub fn handle_input_char(&mut self, c: char) {
344        if self.input_mode == InputMode::EditingTarget {
345            self.input_buffer.push(c);
346        }
347    }
348
349    pub fn handle_backspace(&mut self) {
350        if self.input_mode == InputMode::EditingTarget {
351            self.input_buffer.pop();
352        }
353    }
354
355    pub fn get_details_content_lines(&self) -> usize {
356        if let Some(exchange) = self.get_selected_exchange() {
357            let mut line_count = 0;
358
359            // Basic info lines
360            line_count += 3; // Transport, Method, ID
361
362            // Request section
363            if let Some(request) = &exchange.request {
364                line_count += 2; // Empty line + "REQUEST:" header
365
366                if let Some(headers) = &request.headers {
367                    line_count += 2; // Empty line + "HTTP Headers:"
368                    line_count += headers.len();
369                }
370
371                line_count += 2; // Empty line + "JSON-RPC Request:"
372
373                // Estimate JSON lines (rough calculation)
374                let mut request_json = serde_json::Map::new();
375                request_json.insert(
376                    "jsonrpc".to_string(),
377                    serde_json::Value::String("2.0".to_string()),
378                );
379                if let Some(id) = &request.id {
380                    request_json.insert("id".to_string(), id.clone());
381                }
382                if let Some(method) = &request.method {
383                    request_json.insert(
384                        "method".to_string(),
385                        serde_json::Value::String(method.clone()),
386                    );
387                }
388                if let Some(params) = &request.params {
389                    request_json.insert("params".to_string(), params.clone());
390                }
391
392                if let Ok(json_str) =
393                    serde_json::to_string_pretty(&serde_json::Value::Object(request_json))
394                {
395                    line_count += json_str.lines().count();
396                }
397            }
398
399            // Response section
400            if let Some(response) = &exchange.response {
401                line_count += 2; // Empty line + "RESPONSE:" header
402
403                if let Some(headers) = &response.headers {
404                    line_count += 2; // Empty line + "HTTP Headers:"
405                    line_count += headers.len();
406                }
407
408                line_count += 2; // Empty line + "JSON-RPC Response:"
409
410                // Estimate JSON lines
411                let mut response_json = serde_json::Map::new();
412                response_json.insert(
413                    "jsonrpc".to_string(),
414                    serde_json::Value::String("2.0".to_string()),
415                );
416                if let Some(id) = &response.id {
417                    response_json.insert("id".to_string(), id.clone());
418                }
419                if let Some(result) = &response.result {
420                    response_json.insert("result".to_string(), result.clone());
421                }
422                if let Some(error) = &response.error {
423                    response_json.insert("error".to_string(), error.clone());
424                }
425
426                if let Ok(json_str) =
427                    serde_json::to_string_pretty(&serde_json::Value::Object(response_json))
428                {
429                    line_count += json_str.lines().count();
430                }
431            } else {
432                line_count += 2; // Empty line + "RESPONSE: Pending..."
433            }
434
435            line_count
436        } else {
437            1 // "No exchange selected"
438        }
439    }
440
441    // Pause/Intercept functionality
442    pub fn toggle_pause_mode(&mut self) {
443        self.app_mode = match self.app_mode {
444            AppMode::Normal => AppMode::Paused,
445            AppMode::Paused => AppMode::Normal,
446            AppMode::Intercepting => AppMode::Normal,
447        };
448    }
449
450    pub fn select_next_pending(&mut self) {
451        if !self.pending_requests.is_empty() {
452            self.selected_pending = (self.selected_pending + 1) % self.pending_requests.len();
453            self.reset_intercept_details_scroll();
454        }
455    }
456
457    pub fn select_previous_pending(&mut self) {
458        if !self.pending_requests.is_empty() {
459            self.selected_pending = if self.selected_pending == 0 {
460                self.pending_requests.len() - 1
461            } else {
462                self.selected_pending - 1
463            };
464            self.reset_intercept_details_scroll();
465        }
466    }
467
468    pub fn get_selected_pending(&self) -> Option<&PendingRequest> {
469        self.pending_requests.get(self.selected_pending)
470    }
471
472    pub fn allow_selected_request(&mut self) {
473        if self.selected_pending < self.pending_requests.len() {
474            let pending = self.pending_requests.remove(self.selected_pending);
475            if self.selected_pending > 0 && self.selected_pending >= self.pending_requests.len() {
476                self.selected_pending -= 1;
477            }
478
479            // Send decision to proxy
480            let decision = if let Some(ref modified_json) = pending.modified_request {
481                if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(modified_json) {
482                    ProxyDecision::Allow(Some(parsed), pending.modified_headers.clone())
483                } else {
484                    ProxyDecision::Allow(None, pending.modified_headers.clone())
485                    // Fallback to original if modified JSON is invalid
486                }
487            } else {
488                ProxyDecision::Allow(None, pending.modified_headers.clone()) // Use original request
489            };
490
491            let _ = pending.decision_sender.send(decision);
492        }
493    }
494
495    pub fn block_selected_request(&mut self) {
496        if self.selected_pending < self.pending_requests.len() {
497            let pending = self.pending_requests.remove(self.selected_pending);
498            if self.selected_pending > 0 && self.selected_pending >= self.pending_requests.len() {
499                self.selected_pending -= 1;
500            }
501
502            // Send block decision to proxy
503            let _ = pending.decision_sender.send(ProxyDecision::Block);
504        }
505    }
506
507    pub fn resume_all_requests(&mut self) {
508        for pending in self.pending_requests.drain(..) {
509            let _ = pending
510                .decision_sender
511                .send(ProxyDecision::Allow(None, None));
512        }
513        self.selected_pending = 0;
514        self.app_mode = AppMode::Normal;
515    }
516
517    pub fn get_pending_request_json(&self) -> Option<String> {
518        if let Some(pending) = self.get_selected_pending() {
519            // Get the original request JSON and format it nicely
520            let json_value = serde_json::json!({
521                "jsonrpc": "2.0",
522                "method": pending.original_request.method,
523                "params": pending.original_request.params,
524                "id": pending.original_request.id
525            });
526
527            // Pretty print the JSON for editing
528            serde_json::to_string_pretty(&json_value).ok()
529        } else {
530            None
531        }
532    }
533
534    pub fn apply_edited_json(&mut self, edited_json: String) -> Result<(), String> {
535        if self.selected_pending >= self.pending_requests.len() {
536            return Err("No pending request selected".to_string());
537        }
538
539        // Parse the edited JSON
540        let parsed: serde_json::Value =
541            serde_json::from_str(&edited_json).map_err(|e| format!("Invalid JSON: {}", e))?;
542
543        // Validate it's a proper JSON-RPC request
544        if parsed.get("jsonrpc") != Some(&serde_json::Value::String("2.0".to_string())) {
545            return Err("Missing or invalid 'jsonrpc' field".to_string());
546        }
547
548        if parsed.get("method").is_none() {
549            return Err("Missing 'method' field".to_string());
550        }
551
552        // Store the modified request
553        self.pending_requests[self.selected_pending].modified_request = Some(edited_json);
554
555        Ok(())
556    }
557
558    pub fn get_pending_request_headers(&self) -> Option<String> {
559        if let Some(pending) = self.get_selected_pending() {
560            // Get headers (modified if available, otherwise original)
561            let headers = pending
562                .modified_headers
563                .as_ref()
564                .or(pending.original_request.headers.as_ref());
565
566            if let Some(headers) = headers {
567                // Format headers as key: value pairs for editing
568                let mut header_lines = Vec::new();
569                for (key, value) in headers {
570                    header_lines.push(format!("{}: {}", key, value));
571                }
572                Some(header_lines.join("\n"))
573            } else {
574                Some(
575                    "# No headers\n# Add headers in the format:\n# header-name: header-value"
576                        .to_string(),
577                )
578            }
579        } else {
580            None
581        }
582    }
583
584    pub fn apply_edited_headers(&mut self, edited_headers: String) -> Result<(), String> {
585        if self.selected_pending >= self.pending_requests.len() {
586            return Err("No pending request selected".to_string());
587        }
588
589        let mut headers = HashMap::new();
590
591        for line in edited_headers.lines() {
592            let line = line.trim();
593
594            // Skip empty lines and comments
595            if line.is_empty() || line.starts_with('#') {
596                continue;
597            }
598
599            // Parse header line (key: value)
600            if let Some(colon_pos) = line.find(':') {
601                let key = line[..colon_pos].trim().to_string();
602                let value = line[colon_pos + 1..].trim().to_string();
603
604                if !key.is_empty() {
605                    headers.insert(key, value);
606                }
607            } else {
608                return Err(format!(
609                    "Invalid header format: '{}'. Use 'key: value' format.",
610                    line
611                ));
612            }
613        }
614
615        // Store the modified headers
616        self.pending_requests[self.selected_pending].modified_headers = Some(headers);
617
618        Ok(())
619    }
620
621    pub fn get_pending_response_template(&self) -> Option<String> {
622        if let Some(pending) = self.get_selected_pending() {
623            // Create a template JSON-RPC response with simple string result
624            let response_template = serde_json::json!({
625                "jsonrpc": "2.0",
626                "id": pending.original_request.id,
627                "result": "custom response"
628            });
629
630            // Pretty print the JSON for editing
631            serde_json::to_string_pretty(&response_template).ok()
632        } else {
633            None
634        }
635    }
636
637    pub fn complete_selected_request(&mut self, response_json: String) -> Result<(), String> {
638        if self.selected_pending >= self.pending_requests.len() {
639            return Err("No pending request selected".to_string());
640        }
641
642        // Parse the response JSON
643        let parsed: serde_json::Value =
644            serde_json::from_str(&response_json).map_err(|e| format!("Invalid JSON: {}", e))?;
645
646        // Validate it's a proper JSON-RPC response
647        if parsed.get("jsonrpc") != Some(&serde_json::Value::String("2.0".to_string())) {
648            return Err("Missing or invalid 'jsonrpc' field".to_string());
649        }
650
651        if parsed.get("id").is_none() {
652            return Err("Missing 'id' field".to_string());
653        }
654
655        // Must have either result or error, but not both
656        let has_result = parsed.get("result").is_some();
657        let has_error = parsed.get("error").is_some();
658
659        if !has_result && !has_error {
660            return Err("Response must have either 'result' or 'error' field".to_string());
661        }
662
663        if has_result && has_error {
664            return Err("Response cannot have both 'result' and 'error' fields".to_string());
665        }
666
667        // Remove the pending request and send the completion decision
668        let pending = self.pending_requests.remove(self.selected_pending);
669        if self.selected_pending > 0 && self.selected_pending >= self.pending_requests.len() {
670            self.selected_pending -= 1;
671        }
672
673        let _ = pending
674            .decision_sender
675            .send(ProxyDecision::Complete(parsed));
676
677        Ok(())
678    }
679
680    pub async fn send_new_request(&self, request_json: String) -> Result<(), String> {
681        // Parse the request JSON
682        let parsed: serde_json::Value =
683            serde_json::from_str(&request_json).map_err(|e| format!("Invalid JSON: {}", e))?;
684
685        // Validate it's a proper JSON-RPC request
686        if parsed.get("jsonrpc") != Some(&serde_json::Value::String("2.0".to_string())) {
687            return Err("Missing or invalid 'jsonrpc' field".to_string());
688        }
689
690        if parsed.get("method").is_none() {
691            return Err("Missing 'method' field".to_string());
692        }
693
694        // Check if target URL is empty
695        if self.proxy_config.target_url.trim().is_empty() {
696            return Err("Target URL is not set. Press 't' to set a target URL first.".to_string());
697        }
698
699        let client = reqwest::Client::new();
700
701        // If we're in paused mode, send directly to target to avoid interception
702        // Otherwise, send through proxy for normal logging
703        let url = if matches!(self.app_mode, AppMode::Paused | AppMode::Intercepting) {
704            &self.proxy_config.target_url
705        } else {
706            // Send through proxy for normal logging
707            &format!("http://localhost:{}", self.proxy_config.listen_port)
708        };
709
710        let response = client
711            .post(url)
712            .header("Content-Type", "application/json")
713            .body(request_json)
714            .send()
715            .await
716            .map_err(|e| format!("Failed to send request: {}", e))?;
717
718        if !response.status().is_success() {
719            return Err(format!("Request failed with status: {}", response.status()));
720        }
721
722        Ok(())
723    }
724}