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