stem_rs/interpreter/
help.rs

1//! Help system for the interpreter prompt.
2//!
3//! This module provides help text and usage information for interpreter
4//! commands and Tor control protocol commands.
5//!
6//! # Overview
7//!
8//! The help system provides documentation for:
9//! - Interpreter commands (`/help`, `/events`, `/info`, `/python`, `/quit`)
10//! - Tor control commands (`GETINFO`, `GETCONF`, `SETCONF`, `SIGNAL`, etc.)
11//!
12//! For some commands (like `GETINFO` and `GETCONF`), the help system queries
13//! Tor to provide a complete list of available options.
14//!
15//! # Usage
16//!
17//! Help is accessed via the `/help` interpreter command:
18//!
19//! ```text
20//! /help           # General help overview
21//! /help GETINFO   # Help for GETINFO command
22//! /help signal    # Help for SIGNAL command (case-insensitive)
23//! ```
24//!
25//! # Example
26//!
27//! ```rust,no_run
28//! use stem_rs::Controller;
29//! use stem_rs::interpreter::help;
30//!
31//! # async fn example() -> Result<(), stem_rs::Error> {
32//! let mut controller = Controller::from_port("127.0.0.1:9051".parse()?).await?;
33//! controller.authenticate(None).await?;
34//!
35//! // Get general help
36//! let general = help::response(&mut controller, "").await;
37//! println!("{}", general);
38//!
39//! // Get help for a specific command
40//! let signal_help = help::response(&mut controller, "SIGNAL").await;
41//! println!("{}", signal_help);
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! # Python Stem Equivalent
47//!
48//! This module corresponds to Python Stem's `stem.interpreter.help` module.
49
50use crate::controller::Controller;
51
52/// Returns help text for the given topic.
53///
54/// If no topic is provided (empty string), returns general help with an
55/// overview of all available commands. Otherwise, returns detailed help
56/// for the specified command.
57///
58/// # Arguments
59///
60/// * `controller` - An authenticated controller connection (used to query
61///   available options for some commands)
62/// * `arg` - The help topic (command name), or empty for general help
63///
64/// # Returns
65///
66/// Help text as a string. If the topic is not recognized, returns an
67/// error message.
68///
69/// # Example
70///
71/// ```rust,no_run
72/// use stem_rs::Controller;
73/// use stem_rs::interpreter::help;
74///
75/// # async fn example() -> Result<(), stem_rs::Error> {
76/// # let mut controller = Controller::from_port("127.0.0.1:9051".parse()?).await?;
77/// # controller.authenticate(None).await?;
78/// // General help
79/// let help_text = help::response(&mut controller, "").await;
80/// assert!(help_text.contains("/help"));
81///
82/// // Command-specific help
83/// let signal_help = help::response(&mut controller, "SIGNAL").await;
84/// assert!(signal_help.contains("NEWNYM"));
85///
86/// // Unknown topic
87/// let unknown = help::response(&mut controller, "UNKNOWN").await;
88/// assert!(unknown.contains("No help information"));
89/// # Ok(())
90/// # }
91/// ```
92pub async fn response(controller: &mut Controller, arg: &str) -> String {
93    let arg = normalize(arg);
94
95    if arg.is_empty() {
96        return general_help();
97    }
98
99    match arg.as_str() {
100        "HELP" => help_help(),
101        "EVENTS" => help_events(),
102        "INFO" => help_info(),
103        "PYTHON" => help_python(),
104        "QUIT" => help_quit(),
105        "GETINFO" => help_getinfo(controller).await,
106        "GETCONF" => help_getconf(controller).await,
107        "SETCONF" => help_setconf(),
108        "RESETCONF" => help_resetconf(),
109        "SIGNAL" => help_signal(),
110        "SETEVENTS" => help_setevents(controller).await,
111        "USEFEATURE" => help_usefeature(controller).await,
112        "SAVECONF" => help_saveconf(),
113        "LOADCONF" => help_loadconf(),
114        "MAPADDRESS" => help_mapaddress(),
115        "POSTDESCRIPTOR" => help_postdescriptor(),
116        "EXTENDCIRCUIT" => help_extendcircuit(),
117        "SETCIRCUITPURPOSE" => help_setcircuitpurpose(),
118        "CLOSECIRCUIT" => help_closecircuit(),
119        "ATTACHSTREAM" => help_attachstream(),
120        "REDIRECTSTREAM" => help_redirectstream(),
121        "CLOSESTREAM" => help_closestream(),
122        "ADD_ONION" => help_add_onion(),
123        "DEL_ONION" => help_del_onion(),
124        "HSFETCH" => help_hsfetch(),
125        "HSPOST" => help_hspost(),
126        "RESOLVE" => help_resolve(),
127        "TAKEOWNERSHIP" => help_takeownership(),
128        "PROTOCOLINFO" => help_protocolinfo(),
129        _ => format!("No help information available for '{}'...", arg),
130    }
131}
132
133/// Normalizes a help topic argument.
134///
135/// Converts to uppercase, takes only the first word, and strips leading `/`.
136fn normalize(arg: &str) -> String {
137    let arg = arg.to_uppercase();
138    let arg = arg.split_whitespace().next().unwrap_or("");
139    arg.trim_start_matches('/').to_string()
140}
141
142/// Returns the general help overview listing all commands.
143fn general_help() -> String {
144    r#"Interpreter commands include:
145  /help   - provides information for interpreter and tor commands
146  /events - prints events that we've received
147  /info   - general information for a relay
148  /python - enable or disable support for running python commands
149  /quit   - shuts down the interpreter
150
151Tor commands include:
152  GETINFO - queries information from tor
153  GETCONF, SETCONF, RESETCONF - show or edit a configuration option
154  SIGNAL - issues control signal to the process (for resetting, stopping, etc)
155  SETEVENTS - configures the events tor will notify us of
156
157  USEFEATURE - enables custom behavior for the controller
158  SAVECONF - writes tor's current configuration to our torrc
159  LOADCONF - loads the given input like it was part of our torrc
160  MAPADDRESS - replaces requests for one address with another
161  POSTDESCRIPTOR - adds a relay descriptor to our cache
162  EXTENDCIRCUIT - create or extend a tor circuit
163  SETCIRCUITPURPOSE - configures the purpose associated with a circuit
164  CLOSECIRCUIT - closes the given circuit
165  ATTACHSTREAM - associates an application's stream with a tor circuit
166  REDIRECTSTREAM - sets a stream's destination
167  CLOSESTREAM - closes the given stream
168  ADD_ONION - create a new hidden service
169  DEL_ONION - delete a hidden service that was created with ADD_ONION
170  HSFETCH - retrieve a hidden service descriptor
171  HSPOST - uploads a hidden service descriptor
172  RESOLVE - issues an asynchronous dns or rdns request over tor
173  TAKEOWNERSHIP - instructs tor to quit when this control connection is closed
174  PROTOCOLINFO - queries version and controller authentication information
175  QUIT - disconnect the control connection
176
177For more information use '/help [OPTION]'."#
178        .to_string()
179}
180
181/// Returns help for the `/help` command.
182fn help_help() -> String {
183    r#"/help [OPTION]
184
185Provides usage information for the given interpreter, tor command, or tor
186configuration option.
187
188Example:
189  /help info        # provides a description of the '/info' option
190  /help GETINFO     # usage information for tor's GETINFO controller option"#
191        .to_string()
192}
193
194/// Returns help for the `/events` command.
195fn help_events() -> String {
196    r#"/events [types]
197
198Provides events that we've received belonging to the given event types. If
199no types are specified then this provides all the messages that we've
200received.
201
202You can also run '/events clear' to clear the backlog of events we've
203received."#
204        .to_string()
205}
206
207/// Returns help for the `/info` command.
208fn help_info() -> String {
209    r#"/info [relay fingerprint, nickname, or IP address]
210
211Provides information for a relay that's currently in the consensus. If no
212relay is specified then this provides information on ourselves."#
213        .to_string()
214}
215
216/// Returns help for the `/python` command.
217fn help_python() -> String {
218    r#"/python [enable,disable]
219
220Enables or disables support for running python commands. This determines how
221we treat commands this interpreter doesn't recognize...
222
223* If enabled then unrecognized commands are executed as python.
224* If disabled then unrecognized commands are passed along to tor."#
225        .to_string()
226}
227
228/// Returns help for the `/quit` command.
229fn help_quit() -> String {
230    "/quit\n\nTerminates the interpreter.".to_string()
231}
232
233/// Returns help for the `GETINFO` command, including available options.
234async fn help_getinfo(controller: &mut Controller) -> String {
235    let mut output =
236        "GETINFO OPTION\n\nQueries the tor process for information. Options are...\n\n".to_string();
237
238    if let Ok(results) = controller.get_info("info/names").await {
239        for line in results.lines() {
240            if let Some((opt, summary)) = line.split_once(" -- ") {
241                output.push_str(&format!("{:<33} - {}\n", opt, summary));
242            }
243        }
244    }
245
246    output
247}
248
249/// Returns help for the `GETCONF` command, including available options.
250async fn help_getconf(controller: &mut Controller) -> String {
251    let mut output = "GETCONF OPTION\n\nProvides the current value for a given configuration value. Options include...\n\n".to_string();
252
253    if let Ok(results) = controller.get_info("config/names").await {
254        let options: Vec<&str> = results
255            .lines()
256            .filter_map(|line| line.split_whitespace().next())
257            .collect();
258
259        for chunk in options.chunks(2) {
260            let line = chunk
261                .iter()
262                .map(|s| format!("{:<42}", s))
263                .collect::<Vec<_>>()
264                .join("");
265            output.push_str(&format!("{}\n", line.trim_end()));
266        }
267    }
268
269    output
270}
271
272/// Returns help for the `SETCONF` command.
273fn help_setconf() -> String {
274    r#"SETCONF PARAM[=VALUE]
275
276Sets the given configuration parameters. Values can be quoted or non-quoted
277strings, and reverts the option to 0 or NULL if not provided.
278
279Examples:
280  * Sets a contact address and resets our family to NULL
281    SETCONF MyFamily ContactInfo=foo@bar.com
282
283  * Sets an exit policy that only includes port 80/443
284    SETCONF ExitPolicy="accept *:80, accept *:443, reject *:*""#
285        .to_string()
286}
287
288/// Returns help for the `RESETCONF` command.
289fn help_resetconf() -> String {
290    r#"RESETCONF PARAM[=VALUE]
291
292Reverts the given configuration options to their default values. If a value
293is provided then this behaves in the same way as SETCONF.
294
295Examples:
296  * Returns both of our accounting parameters to their defaults
297    RESETCONF AccountingMax AccountingStart
298
299  * Uses the default exit policy and sets our nickname to be 'Goomba'
300    RESETCONF ExitPolicy Nickname=Goomba"#
301        .to_string()
302}
303
304/// Returns help for the `SIGNAL` command.
305fn help_signal() -> String {
306    r#"SIGNAL SIG
307
308Issues a signal that tells the tor process to reload its torrc, dump its
309stats, halt, etc.
310
311RELOAD / HUP      - reload our torrc
312SHUTDOWN / INT    - gracefully shut down, waiting 30 seconds if we're a relay
313DUMP / USR1       - logs information about open connections and circuits
314DEBUG / USR2      - makes us log at the DEBUG runlevel
315HALT / TERM       - immediately shut down
316CLEARDNSCACHE     - clears any cached DNS results
317NEWNYM            - clears the DNS cache and uses new circuits for future connections"#
318        .to_string()
319}
320
321/// Returns help for the `SETEVENTS` command, including available events.
322async fn help_setevents(controller: &mut Controller) -> String {
323    let mut output = r#"SETEVENTS [EXTENDED] [EVENTS]
324
325Sets the events that we will receive. This turns off any events that aren't
326listed so sending 'SETEVENTS' without any values will turn off all event reporting.
327
328Events include...
329
330"#
331    .to_string();
332
333    if let Ok(results) = controller.get_info("events/names").await {
334        let entries: Vec<&str> = results.split_whitespace().collect();
335        for chunk in entries.chunks(4) {
336            let line = chunk
337                .iter()
338                .map(|s| format!("{:<20}", s))
339                .collect::<Vec<_>>()
340                .join("");
341            output.push_str(&format!("{}\n", line.trim_end()));
342        }
343    }
344
345    output
346}
347
348/// Returns help for the `USEFEATURE` command, including available features.
349async fn help_usefeature(controller: &mut Controller) -> String {
350    let mut output =
351        "USEFEATURE OPTION\n\nCustomizes the behavior of the control port. Options include...\n\n"
352            .to_string();
353
354    if let Ok(results) = controller.get_info("features/names").await {
355        output.push_str(&results);
356        output.push('\n');
357    }
358
359    output
360}
361
362/// Returns help for the `SAVECONF` command.
363fn help_saveconf() -> String {
364    "SAVECONF\n\nWrites Tor's current configuration to its torrc.".to_string()
365}
366
367/// Returns help for the `LOADCONF` command.
368fn help_loadconf() -> String {
369    r#"LOADCONF...
370
371Reads the given text like it belonged to our torrc.
372
373Example:
374  +LOADCONF
375  # sets our exit policy to just accept ports 80 and 443
376  ExitPolicy accept *:80
377  ExitPolicy accept *:443
378  ExitPolicy reject *:*
379  .
380
381Multi-line control options like this are not yet implemented."#
382        .to_string()
383}
384
385/// Returns help for the `MAPADDRESS` command.
386fn help_mapaddress() -> String {
387    r#"MAPADDRESS SOURCE_ADDR=DESTINATION_ADDR
388
389Replaces future requests for one address with another.
390
391Example:
392  MAPADDRESS 0.0.0.0=torproject.org 1.2.3.4=tor.freehaven.net"#
393        .to_string()
394}
395
396/// Returns help for the `POSTDESCRIPTOR` command.
397fn help_postdescriptor() -> String {
398    r#"POSTDESCRIPTOR [purpose=general/controller/bridge] [cache=yes/no]...
399
400Simulates getting a new relay descriptor.
401
402Multi-line control options like this are not yet implemented."#
403        .to_string()
404}
405
406/// Returns help for the `EXTENDCIRCUIT` command.
407fn help_extendcircuit() -> String {
408    r#"EXTENDCIRCUIT CircuitID [PATH] [purpose=general/controller]
409
410Extends the given circuit or create a new one if the CircuitID is zero. The
411PATH is a comma separated list of fingerprints. If it isn't set then this
412uses Tor's normal path selection."#
413        .to_string()
414}
415
416/// Returns help for the `SETCIRCUITPURPOSE` command.
417fn help_setcircuitpurpose() -> String {
418    "SETCIRCUITPURPOSE CircuitID purpose=general/controller\n\nSets the purpose attribute for a circuit.".to_string()
419}
420
421/// Returns help for the `CLOSECIRCUIT` command.
422fn help_closecircuit() -> String {
423    r#"CLOSECIRCUIT CircuitID [IfUnused]
424
425Closes the given circuit. If "IfUnused" is included then this only closes
426the circuit if it isn't currently being used."#
427        .to_string()
428}
429
430/// Returns help for the `ATTACHSTREAM` command.
431fn help_attachstream() -> String {
432    r#"ATTACHSTREAM StreamID CircuitID [HOP=HopNum]
433
434Attaches a stream with the given built circuit (tor picks one on its own if
435CircuitID is zero). If HopNum is given then this hop is used to exit the
436circuit, otherwise the last relay is used."#
437        .to_string()
438}
439
440/// Returns help for the `REDIRECTSTREAM` command.
441fn help_redirectstream() -> String {
442    r#"REDIRECTSTREAM StreamID Address [Port]
443
444Sets the destination for a given stream. This can only be done after a
445stream is created but before it's attached to a circuit."#
446        .to_string()
447}
448
449/// Returns help for the `CLOSESTREAM` command.
450fn help_closestream() -> String {
451    r#"CLOSESTREAM StreamID Reason [Flag]
452
453Closes the given stream, the reason being an integer matching a reason as
454per section 6.3 of the tor-spec."#
455        .to_string()
456}
457
458/// Returns help for the `ADD_ONION` command.
459fn help_add_onion() -> String {
460    r#"KeyType:KeyBlob [Flags=Flag] (Port=Port [,Target])...
461
462Creates a new hidden service. Unlike 'SETCONF HiddenServiceDir...' this
463doesn't persist the service to disk."#
464        .to_string()
465}
466
467/// Returns help for the `DEL_ONION` command.
468fn help_del_onion() -> String {
469    "DEL_ONION ServiceID\n\nDelete a hidden service that was created with ADD_ONION.".to_string()
470}
471
472/// Returns help for the `HSFETCH` command.
473fn help_hsfetch() -> String {
474    r#"HSFETCH (HSAddress/v2-DescId) [SERVER=Server]...
475
476Retrieves the descriptor for a hidden service. This is an asynchronous
477request, with the descriptor provided by a HS_DESC_CONTENT event."#
478        .to_string()
479}
480
481/// Returns help for the `HSPOST` command.
482fn help_hspost() -> String {
483    "HSPOST [SERVER=Server] DESCRIPTOR\n\nUploads a descriptor to a hidden service directory."
484        .to_string()
485}
486
487/// Returns help for the `RESOLVE` command.
488fn help_resolve() -> String {
489    r#"RESOLVE [mode=reverse] address
490
491Performs IPv4 DNS resolution over tor, doing a reverse lookup instead if
492"mode=reverse" is included. This request is processed in the background and
493results in a ADDRMAP event with the response."#
494        .to_string()
495}
496
497/// Returns help for the `TAKEOWNERSHIP` command.
498fn help_takeownership() -> String {
499    "TAKEOWNERSHIP\n\nInstructs Tor to gracefully shut down when this control connection is closed."
500        .to_string()
501}
502
503/// Returns help for the `PROTOCOLINFO` command.
504fn help_protocolinfo() -> String {
505    r#"PROTOCOLINFO [ProtocolVersion]
506
507Provides bootstrapping information that a controller might need when first
508starting, like Tor's version and controller authentication. This can be done
509before authenticating to the control port."#
510        .to_string()
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516
517    #[test]
518    fn test_normalize_uppercase() {
519        assert_eq!(normalize("getinfo"), "GETINFO");
520    }
521
522    #[test]
523    fn test_normalize_strips_slash() {
524        assert_eq!(normalize("/help"), "HELP");
525    }
526
527    #[test]
528    fn test_normalize_takes_first_word() {
529        assert_eq!(normalize("GETINFO version"), "GETINFO");
530    }
531
532    #[test]
533    fn test_normalize_empty() {
534        assert_eq!(normalize(""), "");
535    }
536
537    #[test]
538    fn test_general_help_contains_commands() {
539        let help = general_help();
540        assert!(help.contains("/help"));
541        assert!(help.contains("/events"));
542        assert!(help.contains("/info"));
543        assert!(help.contains("/python"));
544        assert!(help.contains("/quit"));
545        assert!(help.contains("GETINFO"));
546        assert!(help.contains("GETCONF"));
547        assert!(help.contains("SETCONF"));
548        assert!(help.contains("SIGNAL"));
549    }
550
551    #[test]
552    fn test_help_help() {
553        let help = help_help();
554        assert!(help.contains("/help"));
555        assert!(help.contains("Example"));
556    }
557
558    #[test]
559    fn test_help_events() {
560        let help = help_events();
561        assert!(help.contains("/events"));
562        assert!(help.contains("clear"));
563    }
564
565    #[test]
566    fn test_help_signal() {
567        let help = help_signal();
568        assert!(help.contains("RELOAD"));
569        assert!(help.contains("SHUTDOWN"));
570        assert!(help.contains("NEWNYM"));
571    }
572
573    #[test]
574    fn test_help_info() {
575        let help = help_info();
576        assert!(help.contains("/info"));
577        assert!(help.contains("fingerprint"));
578        assert!(help.contains("nickname"));
579    }
580
581    #[test]
582    fn test_help_python() {
583        let help = help_python();
584        assert!(help.contains("/python"));
585        assert!(help.contains("enable"));
586        assert!(help.contains("disable"));
587    }
588
589    #[test]
590    fn test_help_quit() {
591        let help = help_quit();
592        assert!(help.contains("/quit"));
593        assert!(help.contains("Terminates"));
594    }
595
596    #[test]
597    fn test_help_setconf() {
598        let help = help_setconf();
599        assert!(help.contains("SETCONF"));
600        assert!(help.contains("Example"));
601        assert!(help.contains("MyFamily"));
602    }
603
604    #[test]
605    fn test_help_resetconf() {
606        let help = help_resetconf();
607        assert!(help.contains("RESETCONF"));
608        assert!(help.contains("default"));
609        assert!(help.contains("Example"));
610    }
611
612    #[test]
613    fn test_help_saveconf() {
614        let help = help_saveconf();
615        assert!(help.contains("SAVECONF"));
616        assert!(help.contains("torrc"));
617    }
618
619    #[test]
620    fn test_help_loadconf() {
621        let help = help_loadconf();
622        assert!(help.contains("LOADCONF"));
623        assert!(help.contains("Multi-line"));
624    }
625
626    #[test]
627    fn test_help_mapaddress() {
628        let help = help_mapaddress();
629        assert!(help.contains("MAPADDRESS"));
630        assert!(help.contains("Example"));
631    }
632
633    #[test]
634    fn test_help_postdescriptor() {
635        let help = help_postdescriptor();
636        assert!(help.contains("POSTDESCRIPTOR"));
637        assert!(help.contains("Multi-line"));
638    }
639
640    #[test]
641    fn test_help_extendcircuit() {
642        let help = help_extendcircuit();
643        assert!(help.contains("EXTENDCIRCUIT"));
644        assert!(help.contains("CircuitID"));
645        assert!(help.contains("PATH"));
646    }
647
648    #[test]
649    fn test_help_setcircuitpurpose() {
650        let help = help_setcircuitpurpose();
651        assert!(help.contains("SETCIRCUITPURPOSE"));
652        assert!(help.contains("purpose"));
653    }
654
655    #[test]
656    fn test_help_closecircuit() {
657        let help = help_closecircuit();
658        assert!(help.contains("CLOSECIRCUIT"));
659        assert!(help.contains("IfUnused"));
660    }
661
662    #[test]
663    fn test_help_attachstream() {
664        let help = help_attachstream();
665        assert!(help.contains("ATTACHSTREAM"));
666        assert!(help.contains("StreamID"));
667        assert!(help.contains("CircuitID"));
668    }
669
670    #[test]
671    fn test_help_redirectstream() {
672        let help = help_redirectstream();
673        assert!(help.contains("REDIRECTSTREAM"));
674        assert!(help.contains("Address"));
675    }
676
677    #[test]
678    fn test_help_closestream() {
679        let help = help_closestream();
680        assert!(help.contains("CLOSESTREAM"));
681        assert!(help.contains("Reason"));
682    }
683
684    #[test]
685    fn test_help_add_onion() {
686        let help = help_add_onion();
687        assert!(help.contains("KeyType"));
688        assert!(help.contains("hidden service"));
689    }
690
691    #[test]
692    fn test_help_del_onion() {
693        let help = help_del_onion();
694        assert!(help.contains("DEL_ONION"));
695        assert!(help.contains("ServiceID"));
696    }
697
698    #[test]
699    fn test_help_hsfetch() {
700        let help = help_hsfetch();
701        assert!(help.contains("HSFETCH"));
702        assert!(help.contains("descriptor"));
703    }
704
705    #[test]
706    fn test_help_hspost() {
707        let help = help_hspost();
708        assert!(help.contains("HSPOST"));
709        assert!(help.contains("DESCRIPTOR"));
710    }
711
712    #[test]
713    fn test_help_resolve() {
714        let help = help_resolve();
715        assert!(help.contains("RESOLVE"));
716        assert!(help.contains("DNS"));
717        assert!(help.contains("reverse"));
718    }
719
720    #[test]
721    fn test_help_takeownership() {
722        let help = help_takeownership();
723        assert!(help.contains("TAKEOWNERSHIP"));
724        assert!(help.contains("shut down"));
725    }
726
727    #[test]
728    fn test_help_protocolinfo() {
729        let help = help_protocolinfo();
730        assert!(help.contains("PROTOCOLINFO"));
731        assert!(help.contains("authentication"));
732    }
733
734    #[test]
735    fn test_normalize_mixed_case() {
736        assert_eq!(normalize("GetInfo"), "GETINFO");
737        assert_eq!(normalize("setConf"), "SETCONF");
738    }
739
740    #[test]
741    fn test_normalize_with_multiple_spaces() {
742        assert_eq!(normalize("GETINFO   version   extra"), "GETINFO");
743    }
744
745    #[test]
746    fn test_normalize_slash_command() {
747        assert_eq!(normalize("/EVENTS"), "EVENTS");
748        assert_eq!(normalize("/events"), "EVENTS");
749    }
750}