tor-commits
Threads by month
- ----- 2025 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
May 2014
- 25 participants
- 1733 discussions
[meek/master] Stop implicitly taking proxy settings from the environment.
by dcf@torproject.org 25 May '14
by dcf@torproject.org 25 May '14
25 May '14
commit 7f5f32b8fd1d544a355da4fd1c668e4bc7f35679
Author: David Fifield <david(a)bamsoftware.com>
Date: Sat May 24 20:35:56 2014 -0700
Stop implicitly taking proxy settings from the environment.
Before, if no proxy was set, we used http.DefaultTransport, which sets
its proxy to the result of http.ProxyFromEnvironment(). We're going to
plug proxies into the HTTP helper, which doesn't take the proxy from the
environment in this way. I want the behavior to be consistent whether
--helper is used or not.
---
doc/meek-client.1.txt | 2 +-
meek-client/meek-client.go | 12 ++++--------
2 files changed, 5 insertions(+), 9 deletions(-)
diff --git a/doc/meek-client.1.txt b/doc/meek-client.1.txt
index d935e51..5e9e6e3 100644
--- a/doc/meek-client.1.txt
+++ b/doc/meek-client.1.txt
@@ -58,7 +58,7 @@ OPTIONS
**--helper 127.0.0.1:7000**.
**--proxy**=__URL__::
- URL of upstream proxy. If not given, the proxy URL may be taken from the
+ URL of upstream proxy.
The **proxy** SOCKS arg overrides the command line. For
example, **--proxy=http://localhost:8787/**.
diff --git a/meek-client/meek-client.go b/meek-client/meek-client.go
index c48fc69..441120f 100644
--- a/meek-client/meek-client.go
+++ b/meek-client/meek-client.go
@@ -101,23 +101,19 @@ type RequestInfo struct {
// The Host header to put in the HTTP request (optional and may be
// different from the host name in URL).
Host string
- // URL of an upstream proxy to use. If nil, the default net/http
- // library's behavior is used, which is to check the HTTP_PROXY and
- // http_proxy environment for a proxy URL.
+ // URL of an upstream proxy to use. If nil, no proxy is used.
ProxyURL *url.URL
}
// Do an HTTP roundtrip using the payload data in buf and the request metadata
// in info.
func roundTripWithHTTP(buf []byte, info *RequestInfo) (*http.Response, error) {
- tr := http.DefaultTransport
+ tr := new(http.Transport)
if info.ProxyURL != nil {
if info.ProxyURL.Scheme != "http" {
panic(fmt.Sprintf("don't know how to use proxy %s", info.ProxyURL.String()))
}
- tr = &http.Transport{
- Proxy: http.ProxyURL(info.ProxyURL),
- }
+ tr.Proxy = http.ProxyURL(info.ProxyURL)
}
req, err := http.NewRequest("POST", info.URL.String(), bytes.NewReader(buf))
if err != nil {
@@ -325,7 +321,7 @@ func main() {
flag.StringVar(&options.Front, "front", "", "front domain name if no front= SOCKS arg")
flag.StringVar(&helperAddr, "helper", "", "address of HTTP helper (browser extension)")
flag.StringVar(&logFilename, "log", "", "name of log file")
- flag.StringVar(&proxy, "proxy", "", "proxy URL (default from proxy= SOCKS arg or HTTP_PROXY environment variable)")
+ flag.StringVar(&proxy, "proxy", "", "proxy URL if no proxy= SOCKS arg")
flag.StringVar(&options.URL, "url", "", "URL to request if no url= SOCKS arg")
flag.Parse()
1
0
[meek/master] Log that we received a signal only when we received a signal.
by dcf@torproject.org 25 May '14
by dcf@torproject.org 25 May '14
25 May '14
commit 2dd0c3fbb543474f293768f301a7719cba25d727
Author: David Fifield <david(a)bamsoftware.com>
Date: Sat May 24 19:11:45 2014 -0700
Log that we received a signal only when we received a signal.
In the case where there were no active handlers, so we didn't wait for a
second signal, it was printing "got second signal %!s(<nil>)".
---
meek-client/meek-client.go | 4 ++--
meek-server/meek-server.go | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/meek-client/meek-client.go b/meek-client/meek-client.go
index 645d1ef..ae27e11 100644
--- a/meek-client/meek-client.go
+++ b/meek-client/meek-client.go
@@ -390,13 +390,13 @@ func main() {
case n := <-handlerChan:
numHandlers += n
case sig = <-sigChan:
+ log.Printf("got signal %s", sig)
}
}
for _, ln := range listeners {
ln.Close()
}
- log.Printf("got signal %s", sig)
if sig == syscall.SIGTERM {
log.Printf("done")
return
@@ -409,9 +409,9 @@ func main() {
case n := <-handlerChan:
numHandlers += n
case sig = <-sigChan:
+ log.Printf("got second signal %s", sig)
}
}
- log.Printf("got second signal %s", sig)
log.Printf("done")
}
diff --git a/meek-server/meek-server.go b/meek-server/meek-server.go
index c8c8d88..b0c9fa3 100644
--- a/meek-server/meek-server.go
+++ b/meek-server/meek-server.go
@@ -362,13 +362,13 @@ func main() {
case n := <-handlerChan:
numHandlers += n
case sig = <-sigChan:
+ log.Printf("got signal %s", sig)
}
}
for _, ln := range listeners {
ln.Close()
}
- log.Printf("got signal %s", sig)
if sig == syscall.SIGTERM {
log.Printf("done")
return
@@ -381,9 +381,9 @@ func main() {
case n := <-handlerChan:
numHandlers += n
case sig = <-sigChan:
+ log.Printf("got second signal %s", sig)
}
}
- log.Printf("got second signal %s", sig)
log.Printf("done")
}
1
0
25 May '14
commit ae82eaf357f6843d2d76621aa3232f1002aed191
Author: Translation commit bot <translation(a)torproject.org>
Date: Sun May 25 01:45:03 2014 +0000
Update translations for bridgedb
---
tr/LC_MESSAGES/bridgedb.po | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tr/LC_MESSAGES/bridgedb.po b/tr/LC_MESSAGES/bridgedb.po
index 8e2e7f5..5bf401f 100644
--- a/tr/LC_MESSAGES/bridgedb.po
+++ b/tr/LC_MESSAGES/bridgedb.po
@@ -16,7 +16,7 @@ msgstr ""
"Project-Id-Version: The Tor Project\n"
"Report-Msgid-Bugs-To: 'https://trac.torproject.org/projects/tor/newticket?component=BridgeDB&keywo…'\n"
"POT-Creation-Date: 2014-05-16 18:39+0000\n"
-"PO-Revision-Date: 2014-05-25 01:14+0000\n"
+"PO-Revision-Date: 2014-05-25 01:21+0000\n"
"Last-Translator: eromytsatiffird <driffitastymore(a)gmail.com>\n"
"Language-Team: Turkish (http://www.transifex.com/projects/p/torproject/language/tr/)\n"
"MIME-Version: 1.0\n"
1
0
25 May '14
commit ca321715a886a961130a48b8fa0dc432aa82be15
Author: Translation commit bot <translation(a)torproject.org>
Date: Sun May 25 01:15:02 2014 +0000
Update translations for bridgedb
---
tr/LC_MESSAGES/bridgedb.po | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/tr/LC_MESSAGES/bridgedb.po b/tr/LC_MESSAGES/bridgedb.po
index 162670f..8e2e7f5 100644
--- a/tr/LC_MESSAGES/bridgedb.po
+++ b/tr/LC_MESSAGES/bridgedb.po
@@ -3,6 +3,7 @@
# This file is distributed under the same license as the BridgeDB project.
#
# Translators:
+# eromytsatiffird <driffitastymore(a)gmail.com>, 2014
# Tosbaa <conan(a)operamail.com>, 2013
# erg26 <ergungorler(a)gmail.com>, 2012
# idilyuksel <perfectionne(a)gmail.com>, 2014
@@ -15,8 +16,8 @@ msgstr ""
"Project-Id-Version: The Tor Project\n"
"Report-Msgid-Bugs-To: 'https://trac.torproject.org/projects/tor/newticket?component=BridgeDB&keywo…'\n"
"POT-Creation-Date: 2014-05-16 18:39+0000\n"
-"PO-Revision-Date: 2014-05-23 19:52+0000\n"
-"Last-Translator: idilyuksel <perfectionne(a)gmail.com>\n"
+"PO-Revision-Date: 2014-05-25 01:14+0000\n"
+"Last-Translator: eromytsatiffird <driffitastymore(a)gmail.com>\n"
"Language-Team: Turkish (http://www.transifex.com/projects/p/torproject/language/tr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -49,14 +50,14 @@ msgstr "[Bu otomatik bir mesajdır; lütfen cevaplamayınız.]"
#: lib/bridgedb/strings.py:20
msgid "Here are your bridges:"
-msgstr "İşte köprüleriniz:"
+msgstr "İşte bridgeleriniz:"
#: lib/bridgedb/strings.py:22
#, python-format
msgid ""
"You have exceeded the rate limit. Please slow down! The minimum time between\n"
"emails is %s hours. All further emails during this time period will be ignored."
-msgstr "Hız limitini aştınız. Lütfen yavaşlayın! Epostalar arasındaki minimum zaman %s saattir. Bu süre içindeki epostalarınız yok sayılacaktır."
+msgstr "Hız limitini aştınız. Lütfen yavaşlayın! Epostalar arasındaki minimum zaman %s saattir. Bu süre içinde göndereceğiniz epostalarınız yok sayılacaktır."
#: lib/bridgedb/strings.py:25
msgid ""
@@ -123,7 +124,7 @@ msgstr ""
#: lib/bridgedb/strings.py:65
msgid "What are bridges?"
-msgstr "Bridges nelerdir?"
+msgstr "Bridges nedir?"
#: lib/bridgedb/strings.py:66
#, python-format
@@ -132,7 +133,7 @@ msgstr ""
#: lib/bridgedb/strings.py:71
msgid "I need an alternative way of getting bridges!"
-msgstr "Bridges edinmek için alternatif bir yola ihtiyacım var!"
+msgstr "Bridges edinmek için başka bir yola ihtiyacım var!"
#: lib/bridgedb/strings.py:72
#, python-format
@@ -144,7 +145,7 @@ msgstr ""
#: lib/bridgedb/strings.py:79
msgid "My bridges don't work! I need help!"
-msgstr "Köprülerim çalışmıyor! Yardıma ihtiyacım var!"
+msgstr "Bridgelerim çalışmıyor! Yardıma ihtiyacım var!"
#: lib/bridgedb/strings.py:80
#, python-format
@@ -288,7 +289,7 @@ msgstr ""
#: lib/bridgedb/templates/index.html:27
#, python-format
msgid "Get %s bridges %s"
-msgstr "%s köprülerini %s edinin"
+msgstr "%s bridgelerini %s edin"
#: lib/bridgedb/templates/index.html:36
#, python-format
1
0
[meek/master] Use the headless TorBrowser.app.meek-http-helper directory on mac.
by dcf@torproject.org 25 May '14
by dcf@torproject.org 25 May '14
25 May '14
commit 29ad534cc01d52cf076dd961062cdca7e007d105
Author: David Fifield <david(a)bamsoftware.com>
Date: Sat May 24 13:03:31 2014 -0700
Use the headless TorBrowser.app.meek-http-helper directory on mac.
To match tor-browser-bundle changes based on
https://trac.torproject.org/projects/tor/ticket/11429#comment:5.
---
meek-client-torbrowser/mac.go | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/meek-client-torbrowser/mac.go b/meek-client-torbrowser/mac.go
index dbc8c8f..6e00d05 100644
--- a/meek-client-torbrowser/mac.go
+++ b/meek-client-torbrowser/mac.go
@@ -6,6 +6,9 @@
package main
const (
- firefoxPath = "../Contents/MacOS/TorBrowser.app/Contents/MacOS/firefox"
+ // The TorBrowser.app.meek-http-helper directory is a special case for
+ // the mac bundle. It is a copy of TorBrowser.app that has a modified
+ // Info.plist file so that it doesn't show a dock icon.
+ firefoxPath = "../Contents/MacOS/TorBrowser.app.meek-http-helper/Contents/MacOS/firefox"
firefoxProfilePath = "../Data/Browser/profile.meek-http-helper"
)
1
0
commit fb2734b074865ba2af55d18f966a82ae8279f96c
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat May 24 15:10:44 2014 -0700
Correcting setup.py for tor-prompt
Patch from Yawning that corrects two things...
* Doesn't hardcode tor-prompt to install to /usr/bin so the user can customize it.
* Installs the prompt's settings.cfg (without which it won't run).
---
setup.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/setup.py b/setup.py
index 45071e5..200f1d8 100644
--- a/setup.py
+++ b/setup.py
@@ -53,8 +53,8 @@ setup(
provides = ['stem'],
cmdclass = {'build_py': build_py},
keywords = 'tor onion controller',
- data_files = [
- ('/usr/bin', ['tor-prompt'])
- ],
+ scripts = ['tor-prompt'],
+ package_dir = {'stem.interpreter': 'stem/interpreter'},
+ package_data = { 'stem.interpreter': ['settings.cfg']},
)
1
0
commit b4da7f833ded8123d4af709dad4c5c84cccbd147
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat May 24 13:49:49 2014 -0700
'Down the Rabbit Hole' tutorial
Adding a tutorial for our new interpreter, giving an overview of both how to
run it and what it can do. Think this might be my favorite tutorial yet...
---
docs/_static/label/down_the_rabbit_hole.png | Bin 0 -> 2596 bytes
.../label/resources/down_the_rabbit_hole.xcf | Bin 0 -> 6800 bytes
docs/_static/prompt/attach.png | Bin 0 -> 19213 bytes
docs/_static/prompt/events_command.png | Bin 0 -> 13914 bytes
docs/_static/prompt/events_variable.png | Bin 0 -> 17084 bytes
docs/_static/prompt/help.png | Bin 0 -> 41900 bytes
docs/_static/prompt/info.png | Bin 0 -> 23483 bytes
docs/_static/prompt/python.png | Bin 0 -> 63694 bytes
docs/_static/prompt/starting_tor.png | Bin 0 -> 22249 bytes
docs/_static/prompt/tor_commands.png | Bin 0 -> 41184 bytes
docs/_static/section/tutorials/mad_hatter.png | Bin 0 -> 19278 bytes
.../section/tutorials/resources/mad_hatter.xcf | Bin 0 -> 35098 bytes
docs/_templates/layout.html | 1 +
docs/conf.py | 2 +-
docs/contents.rst | 3 +-
docs/tutorials.rst | 19 ++++
docs/tutorials/down_the_rabbit_hole.rst | 111 ++++++++++++++++++++
17 files changed, 134 insertions(+), 2 deletions(-)
diff --git a/docs/_static/label/down_the_rabbit_hole.png b/docs/_static/label/down_the_rabbit_hole.png
new file mode 100644
index 0000000..a3d3587
Binary files /dev/null and b/docs/_static/label/down_the_rabbit_hole.png differ
diff --git a/docs/_static/label/resources/down_the_rabbit_hole.xcf b/docs/_static/label/resources/down_the_rabbit_hole.xcf
new file mode 100644
index 0000000..c4c61ee
Binary files /dev/null and b/docs/_static/label/resources/down_the_rabbit_hole.xcf differ
diff --git a/docs/_static/prompt/attach.png b/docs/_static/prompt/attach.png
new file mode 100644
index 0000000..96add67
Binary files /dev/null and b/docs/_static/prompt/attach.png differ
diff --git a/docs/_static/prompt/events_command.png b/docs/_static/prompt/events_command.png
new file mode 100644
index 0000000..3f058ae
Binary files /dev/null and b/docs/_static/prompt/events_command.png differ
diff --git a/docs/_static/prompt/events_variable.png b/docs/_static/prompt/events_variable.png
new file mode 100644
index 0000000..f104767
Binary files /dev/null and b/docs/_static/prompt/events_variable.png differ
diff --git a/docs/_static/prompt/help.png b/docs/_static/prompt/help.png
new file mode 100644
index 0000000..52eece9
Binary files /dev/null and b/docs/_static/prompt/help.png differ
diff --git a/docs/_static/prompt/info.png b/docs/_static/prompt/info.png
new file mode 100644
index 0000000..e23fa1c
Binary files /dev/null and b/docs/_static/prompt/info.png differ
diff --git a/docs/_static/prompt/python.png b/docs/_static/prompt/python.png
new file mode 100644
index 0000000..ea46114
Binary files /dev/null and b/docs/_static/prompt/python.png differ
diff --git a/docs/_static/prompt/starting_tor.png b/docs/_static/prompt/starting_tor.png
new file mode 100644
index 0000000..3e63792
Binary files /dev/null and b/docs/_static/prompt/starting_tor.png differ
diff --git a/docs/_static/prompt/tor_commands.png b/docs/_static/prompt/tor_commands.png
new file mode 100644
index 0000000..51f2f2b
Binary files /dev/null and b/docs/_static/prompt/tor_commands.png differ
diff --git a/docs/_static/section/tutorials/mad_hatter.png b/docs/_static/section/tutorials/mad_hatter.png
new file mode 100644
index 0000000..0c27756
Binary files /dev/null and b/docs/_static/section/tutorials/mad_hatter.png differ
diff --git a/docs/_static/section/tutorials/resources/mad_hatter.xcf b/docs/_static/section/tutorials/resources/mad_hatter.xcf
new file mode 100644
index 0000000..cb29e62
Binary files /dev/null and b/docs/_static/section/tutorials/resources/mad_hatter.xcf differ
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
index 66e2895..5f60231 100644
--- a/docs/_templates/layout.html
+++ b/docs/_templates/layout.html
@@ -29,6 +29,7 @@
<li><a href="{{ pathto('tutorials/tortoise_and_the_hare') }}">Event Listening</a></li>
<li><a href="{{ pathto('tutorials/mirror_mirror_on_the_wall') }}">Tor Descriptors</a></li>
<li><a href="{{ pathto('tutorials/east_of_the_sun') }}">Utilities</a></li>
+ <li><a href="{{ pathto('tutorials/down_the_rabbit_hole') }}">Interpreter</a></li>
<li><a href="{{ pathto('tutorials/double_double_toil_and_trouble') }}">Examples</a></li>
</ul>
</li>
diff --git a/docs/conf.py b/docs/conf.py
index 801ef91..756bd20 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -142,7 +142,7 @@ html_static_path = ['_static']
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
-#html_use_smartypants = True
+html_use_smartypants = False
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
diff --git a/docs/contents.rst b/docs/contents.rst
index 9555673..6667123 100644
--- a/docs/contents.rst
+++ b/docs/contents.rst
@@ -8,8 +8,9 @@ Contents
tutorials/the_little_relay_that_could
tutorials/to_russia_with_love
tutorials/tortoise_and_the_hare
- tutorials/east_of_the_sun
tutorials/mirror_mirror_on_the_wall
+ tutorials/east_of_the_sun
+ tutorials/down_the_rabbit_hole
tutorials/double_double_toil_and_trouble
tutorials/examples/compare_flags
diff --git a/docs/tutorials.rst b/docs/tutorials.rst
index 2b13b79..40b5439 100644
--- a/docs/tutorials.rst
+++ b/docs/tutorials.rst
@@ -31,6 +31,13 @@ Tutorial
Author: Andrejj
License: CC0 (https://creativecommons.org/publicdomain/zero/1.0/deed.en)
+ * Mad Hatter - mad_hatter.png
+ Source: http://www.krepcio.com/vitreosity/archives/MadHatter-ALL-illus600.jpg
+ Author: John Tenniel
+ License: Public Doman
+ Augmented: Colored by me, and used the card from...
+ https://openclipart.org/detail/1892/mad-hatter-with-label-on-hat-by-nayrhcr…
+
* Double Double Toil and Trouble - cauldron.png
Source: https://openclipart.org/detail/174099/cauldron-by-jarda-174099
Author: Unknown (jarda?)
@@ -92,6 +99,18 @@ feet wet by jumping straight in with some tutorials...
Stem provides several utility modules frequently useful for Tor
controller applications. Here we introduce some of them.
+ * - .. image:: /_static/section/tutorials/mad_hatter.png
+ :target: tutorials/down_the_rabbit_hole.html
+
+ - .. image:: /_static/label/down_the_rabbit_hole.png
+ :target: tutorials/down_the_rabbit_hole.html
+
+ Interactive interpreter for Tor that provides you with direct access to
+ Tor's `control interface
+ <https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_
+ via either python or direct requests. This is an easy way of
+ experimenting with Stem and learning what Tor can do.
+
* - .. image:: /_static/section/tutorials/cauldron.png
:target: tutorials/double_double_toil_and_trouble.html
diff --git a/docs/tutorials/down_the_rabbit_hole.rst b/docs/tutorials/down_the_rabbit_hole.rst
new file mode 100644
index 0000000..2cfbabb
--- /dev/null
+++ b/docs/tutorials/down_the_rabbit_hole.rst
@@ -0,0 +1,111 @@
+Down the Rabbit Hole
+====================
+
+Underneath it all Stem is a Python implementation of Tor's `control
+<https://gitweb.torproject.org/torspec.git/blob/HEAD:/control-spec.txt>`_ and
+`directory specifications
+<https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt>`_.
+Anything you can do with Stem you can also do `with telnet
+<../faq.html#can-i-interact-with-tors-controller-interface-directly>`_ (albeit
+with quite a bit of extra work).
+
+Playing with Tor's control port directly is a great way of learning what
+Tor can and cannot do. This is handy because Stem can take advantage of
+anything the control interface offers, but conversely is also limited by
+things it lacks.
+
+To help Stem offers a control prompt with nice usability improvements over
+telnet...
+
+* Irc-style commands like '**/help**'.
+* Is a **python interpreter** (like IDLE).
+* Tab completion for Tor's controller commands.
+* History scrollback by pressing up/down.
+* Transparently handles Tor authentication at startup.
+* Colorized output for improved readability.
+
+.. _getting-started:
+
+Getting started
+---------------
+
+Getting started with the control prompt is easy. Assuming you have Stem
+installed it will be available under **/usr/bin/tor-prompt**, and can attach
+to either an existing Tor instance or start one of its own.
+
+If Tor's already running `with a control port
+<the_little_relay_that_could.html>`_ then you can attach to it using
+**--interface** or **--socket** (by default it checks on **port 9051**)...
+
+.. image:: /_static/prompt/attach.png
+
+If Tor isn't running this prompt will start a temporary instance of its own.
+Tor will have a minimal non-relaying configuration, and be shut down when
+you're done.
+
+.. image:: /_static/prompt/starting_tor.png
+
+.. _what-can-i-do-with-it:
+
+What can I do with it?
+----------------------
+
+This prompt accepts three types of commands...
+
+* Commands for the interpreter itself, such as **/help** and **/info**. These
+ are handled by the interpreter and always begin with a slash.
+
+* Commands for Tor's control port, such as **GETINFO version** and **GETCONF
+ ExitPolicy**. These are passed along directly to Tor.
+
+* Commands that do not match either of the above are treated as Python.
+
+To get a list of the interpreter and Tor commands run **/help**. You can also
+run **/help [command]** (such as **/help SIGNAL**) to get details on what
+does...
+
+.. image:: /_static/prompt/help.png
+
+Another useful interpreter command is **/info [relay]** which provides
+information about a relay. With this you can look up details about any relay by
+its IP address, fingerprint, or nickname...
+
+.. image:: /_static/prompt/info.png
+
+Tor commands are passed along directly to Tor's control port, providing raw
+responses just as telnet would...
+
+.. image:: /_static/prompt/tor_commands.png
+
+And last but certainly not least this prompt provides a Python interpreter,
+just like IDLE. You start with a :class:`~stem.control.Controller` for you Tor
+instance available as your **controller** variable. This makes it easy to
+experiment with Stem and see what it can do...
+
+.. image:: /_static/prompt/python.png
+
+.. _event-handling:
+
+Event handling
+--------------
+
+As mentioned in an `earlier tutorial <tortoise_and_the_hare.html>`_ you can
+subscribe to receive events from Tor. Stem's :class:`~stem.control.Controller`
+does this with its :func:`~stem.control.Controller.add_event_listener` method,
+but with our raw Tor access we can also subscribe with **SETEVENTS [event
+types]**.
+
+Events we've received are available in two different ways. First, **/events**
+provides a quick dump of the events we've received thus far...
+
+.. image:: /_static/prompt/events_command.png
+
+You can list events of just a certain type by saying which (for instance
+**/events BW**). More useful though is your **events** variable, which is a
+list of :class:`~stem.response.events.Event` instances we've received...
+
+.. image:: /_static/prompt/events_variable.png
+
+To stop receiving events run **SETEVENTS** without any event types, and to
+clear the backlog of events we've received run **/events clear**.
+
1
0
commit 732ecff7d0aa193178f29b1d62cb3413398d7084
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat May 24 13:31:55 2014 -0700
Supporting '/events clear'
Adding a command to clear the backlog of events we've received.
---
stem/interpreter/commands.py | 7 +++++++
stem/interpreter/settings.cfg | 3 +++
2 files changed, 10 insertions(+)
diff --git a/stem/interpreter/commands.py b/stem/interpreter/commands.py
index 28b3e59..0342ba9 100644
--- a/stem/interpreter/commands.py
+++ b/stem/interpreter/commands.py
@@ -126,11 +126,18 @@ class ControlInterpretor(code.InteractiveConsole):
Performs the '/events' operation, dumping the events that we've received
belonging to the given types. If no types are specified then this provides
all buffered events.
+
+ If the user runs '/events clear' then this clears the list of events we've
+ received.
"""
events = self._received_events
event_types = arg.upper().split()
+ if 'CLEAR' in event_types:
+ del self._received_events[:]
+ return format('cleared event backlog', *STANDARD_OUTPUT)
+
if event_types:
events = filter(lambda event: event.type in event_types, events)
diff --git a/stem/interpreter/settings.cfg b/stem/interpreter/settings.cfg
index 0ddf080..5e85a5e 100644
--- a/stem/interpreter/settings.cfg
+++ b/stem/interpreter/settings.cfg
@@ -130,6 +130,9 @@ help.description.events
|Provides events that we've received belonging to the given event types. If
|no types are specified then this provides all the messages that we've
|received.
+|
+|You can also run '/events clear' to clear the backlog of events we've
+|received.
help.description.info
|Provides general information for a relay that's currently in the consensus.
1
0
commit c5a7bbe46555f9a922639942561786c7155ca4f0
Author: Damian Johnson <atagar(a)torproject.org>
Date: Thu May 22 09:15:51 2014 -0700
Fixing interpretor misspelling
Huh. Not sure why my spell checker thought 'interpretor' was ok. Thanks to
Yawning for pointing this out.
---
docs/change_log.rst | 2 +-
setup.py | 2 +-
stem/interpreter/__init__.py | 129 ++++++++++++++
stem/interpreter/arguments.py | 96 +++++++++++
stem/interpreter/autocomplete.py | 112 ++++++++++++
stem/interpreter/commands.py | 299 +++++++++++++++++++++++++++++++++
stem/interpreter/help.py | 142 ++++++++++++++++
stem/interpreter/settings.cfg | 295 ++++++++++++++++++++++++++++++++
stem/interpretor/__init__.py | 129 --------------
stem/interpretor/arguments.py | 96 -----------
stem/interpretor/autocomplete.py | 112 ------------
stem/interpretor/commands.py | 299 ---------------------------------
stem/interpretor/help.py | 142 ----------------
stem/interpretor/settings.cfg | 295 --------------------------------
stem/util/system.py | 2 +-
test/settings.cfg | 8 +-
test/unit/interpreter/__init__.py | 39 +++++
test/unit/interpreter/arguments.py | 57 +++++++
test/unit/interpreter/autocomplete.py | 112 ++++++++++++
test/unit/interpreter/commands.py | 198 ++++++++++++++++++++++
test/unit/interpreter/help.py | 54 ++++++
test/unit/interpretor/__init__.py | 39 -----
test/unit/interpretor/arguments.py | 57 -------
test/unit/interpretor/autocomplete.py | 112 ------------
test/unit/interpretor/commands.py | 198 ----------------------
test/unit/interpretor/help.py | 54 ------
tor-prompt | 4 +-
27 files changed, 1542 insertions(+), 1542 deletions(-)
diff --git a/docs/change_log.rst b/docs/change_log.rst
index f89de76..ef9005d 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -66,7 +66,7 @@ The following are only available within Stem's `git repository
* **Interpretor**
- * Initial release of a Tor interactive interpretor. This included...
+ * Initial release of a Tor interactive interpreter. This included...
* irc-style functions such as '/help' and '/info'
* history scroll-back by pressing up/down
diff --git a/setup.py b/setup.py
index 47eb478..45071e5 100644
--- a/setup.py
+++ b/setup.py
@@ -49,7 +49,7 @@ setup(
author = module_info['author'],
author_email = module_info['contact'],
url = module_info['url'],
- packages = ['stem', 'stem.descriptor', 'stem.interpretor', 'stem.response', 'stem.util'],
+ packages = ['stem', 'stem.descriptor', 'stem.interpreter', 'stem.response', 'stem.util'],
provides = ['stem'],
cmdclass = {'build_py': build_py},
keywords = 'tor onion controller',
diff --git a/stem/interpreter/__init__.py b/stem/interpreter/__init__.py
new file mode 100644
index 0000000..23453a3
--- /dev/null
+++ b/stem/interpreter/__init__.py
@@ -0,0 +1,129 @@
+# Copyright 2014, Damian Johnson and The Tor Project
+# See LICENSE for licensing information
+
+"""
+Interactive interpreter for interacting with Tor directly. This adds usability
+features such as tab completion, history, and IRC-style functions (like /help).
+"""
+
+__all__ = ['arguments', 'autocomplete', 'commands', 'help', 'msg']
+
+import os
+import sys
+
+import stem
+import stem.connection
+import stem.process
+import stem.util.conf
+import stem.util.system
+import stem.util.term
+
+from stem.util.term import RESET, Attr, Color, format
+
+# Our color prompt triggers a bug between raw_input() and readline history,
+# where scrolling through history widens our prompt. Widening our prompt via
+# invisible characters (like resets) seems to sidestep this bug for short
+# inputs. Contrary to the ticket, this still manifests with python 2.7.1...
+#
+# http://bugs.python.org/issue12972
+
+PROMPT = format('>>> ', Color.GREEN, Attr.BOLD) + RESET * 10
+
+STANDARD_OUTPUT = (Color.BLUE, )
+BOLD_OUTPUT = (Color.BLUE, Attr.BOLD)
+HEADER_OUTPUT = (Color.GREEN, )
+HEADER_BOLD_OUTPUT = (Color.GREEN, Attr.BOLD)
+ERROR_OUTPUT = (Attr.BOLD, Color.RED)
+
+settings_path = os.path.join(os.path.dirname(__file__), 'settings.cfg')
+uses_settings = stem.util.conf.uses_settings('stem_interpreter', settings_path)
+
+
+@uses_settings
+def msg(message, config, **attr):
+ return config.get(message).format(**attr)
+
+
+def main():
+ import readline
+
+ import stem.interpreter.arguments
+ import stem.interpreter.autocomplete
+ import stem.interpreter.commands
+
+ try:
+ args = stem.interpreter.arguments.parse(sys.argv[1:])
+ except ValueError as exc:
+ print exc
+ sys.exit(1)
+
+ if args.print_help:
+ print stem.interpreter.arguments.get_help()
+ sys.exit()
+
+ if args.disable_color:
+ global PROMPT
+ stem.util.term.DISABLE_COLOR_SUPPORT = True
+ PROMPT = '>>> '
+
+ # If the user isn't connecting to something in particular then offer to start
+ # tor if it isn't running.
+
+ if not (args.user_provided_port or args.user_provided_socket):
+ is_tor_running = stem.util.system.is_running('tor') or stem.util.system.is_running('tor.real')
+
+ if not is_tor_running:
+ if not stem.util.system.is_available('tor'):
+ print format(msg('msg.tor_unavailable'), *ERROR_OUTPUT)
+ sys.exit(1)
+ else:
+ print format(msg('msg.starting_tor'), *HEADER_OUTPUT)
+
+ stem.process.launch_tor_with_config(
+ config = {
+ 'SocksPort': '0',
+ 'ControlPort': str(args.control_port),
+ 'CookieAuthentication': '1',
+ 'ExitPolicy': 'reject *:*',
+ },
+ completion_percent = 5,
+ take_ownership = True,
+ )
+
+ control_port = None if args.user_provided_socket else (args.control_address, args.control_port)
+ control_socket = None if args.user_provided_port else args.control_socket
+
+ controller = stem.connection.connect(
+ control_port = control_port,
+ control_socket = control_socket,
+ password_prompt = True,
+ )
+
+ if controller is None:
+ sys.exit(1)
+
+ with controller:
+ autocompleter = stem.interpreter.autocomplete.Autocompleter(controller)
+ readline.parse_and_bind('tab: complete')
+ readline.set_completer(autocompleter.complete)
+ readline.set_completer_delims('\n')
+
+ interpreter = stem.interpreter.commands.ControlInterpretor(controller)
+
+ for line in msg('msg.startup_banner').splitlines():
+ line_format = HEADER_BOLD_OUTPUT if line.startswith(' ') else HEADER_OUTPUT
+ print format(line, *line_format)
+
+ print
+
+ while True:
+ try:
+ prompt = '... ' if interpreter.is_multiline_context else PROMPT
+ user_input = raw_input(prompt)
+ response = interpreter.run_command(user_input)
+
+ if response is not None:
+ print response
+ except (KeyboardInterrupt, EOFError, stem.SocketClosed) as exc:
+ print # move cursor to the following line
+ break
diff --git a/stem/interpreter/arguments.py b/stem/interpreter/arguments.py
new file mode 100644
index 0000000..d62a386
--- /dev/null
+++ b/stem/interpreter/arguments.py
@@ -0,0 +1,96 @@
+# Copyright 2014, Damian Johnson and The Tor Project
+# See LICENSE for licensing information
+
+"""
+Commandline argument parsing for our interpreter prompt.
+"""
+
+import collections
+import getopt
+
+import stem.interpreter
+import stem.util.connection
+
+DEFAULT_ARGS = {
+ 'control_address': '127.0.0.1',
+ 'control_port': 9051,
+ 'user_provided_port': False,
+ 'control_socket': '/var/run/tor/control',
+ 'user_provided_socket': False,
+ 'disable_color': False,
+ 'print_help': False,
+}
+
+OPT = 'i:s:h'
+
+OPT_EXPANDED = [
+ 'interface=',
+ 'socket=',
+ 'no-color',
+ 'help',
+]
+
+
+def parse(argv):
+ """
+ Parses our arguments, providing a named tuple with their values.
+
+ :param list argv: input arguments to be parsed
+
+ :returns: a **named tuple** with our parsed arguments
+
+ :raises: **ValueError** if we got an invalid argument
+ """
+
+ args = dict(DEFAULT_ARGS)
+
+ try:
+ getopt_results = getopt.getopt(argv, OPT, OPT_EXPANDED)[0]
+ except getopt.GetoptError as exc:
+ raise ValueError('%s (for usage provide --help)' % exc)
+
+ for opt, arg in getopt_results:
+ if opt in ('-i', '--interface'):
+ if ':' in arg:
+ address, port = arg.split(':', 1)
+ else:
+ address, port = None, arg
+
+ if address is not None:
+ if not stem.util.connection.is_valid_ipv4_address(address):
+ raise ValueError("'%s' isn't a valid IPv4 address" % address)
+
+ args['control_address'] = address
+
+ if not stem.util.connection.is_valid_port(port):
+ raise ValueError("'%s' isn't a valid port number" % port)
+
+ args['control_port'] = int(port)
+ args['user_provided_port'] = True
+ elif opt in ('-s', '--socket'):
+ args['control_socket'] = arg
+ args['user_provided_socket'] = True
+ elif opt == '--no-color':
+ args['disable_color'] = True
+ elif opt in ('-h', '--help'):
+ args['print_help'] = True
+
+ # translates our args dict into a named tuple
+
+ Args = collections.namedtuple('Args', args.keys())
+ return Args(**args)
+
+
+def get_help():
+ """
+ Provides our --help usage information.
+
+ :returns: **str** with our usage information
+ """
+
+ return stem.interpreter.msg(
+ 'msg.help',
+ address = DEFAULT_ARGS['control_address'],
+ port = DEFAULT_ARGS['control_port'],
+ socket = DEFAULT_ARGS['control_socket'],
+ )
diff --git a/stem/interpreter/autocomplete.py b/stem/interpreter/autocomplete.py
new file mode 100644
index 0000000..3a9b40b
--- /dev/null
+++ b/stem/interpreter/autocomplete.py
@@ -0,0 +1,112 @@
+"""
+Tab completion for our interpreter prompt.
+"""
+
+from stem.interpreter import uses_settings
+
+try:
+ # added in python 3.2
+ from functools import lru_cache
+except ImportError:
+ from stem.util.lru_cache import lru_cache
+
+
+@uses_settings
+def _get_commands(controller, config):
+ """
+ Provides commands recognized by tor.
+ """
+
+ commands = config.get('autocomplete', [])
+
+ if controller is None:
+ return commands
+
+ # GETINFO commands. Lines are of the form '[option] -- [description]'. This
+ # strips '*' from options that accept values.
+
+ results = controller.get_info('info/names', None)
+
+ if results:
+ for line in results.splitlines():
+ option = line.split(' ', 1)[0].rstrip('*')
+ commands.append('GETINFO %s' % option)
+ else:
+ commands.append('GETINFO ')
+
+ # GETCONF, SETCONF, and RESETCONF commands. Lines are of the form
+ # '[option] [type]'.
+
+ results = controller.get_info('config/names', None)
+
+ if results:
+ for line in results.splitlines():
+ option = line.split(' ', 1)[0]
+
+ commands.append('GETCONF %s' % option)
+ commands.append('SETCONF %s' % option)
+ commands.append('RESETCONF %s' % option)
+ else:
+ commands += ['GETCONF ', 'SETCONF ', 'RESETCONF ']
+
+ # SETEVENT, USEFEATURE, and SIGNAL commands. For each of these the GETINFO
+ # results are simply a space separated lists of the values they can have.
+
+ options = (
+ ('SETEVENTS ', 'events/names'),
+ ('USEFEATURE ', 'features/names'),
+ ('SIGNAL ', 'signal/names'),
+ )
+
+ for prefix, getinfo_cmd in options:
+ results = controller.get_info(getinfo_cmd, None)
+
+ if results:
+ commands += [prefix + value for value in results.split()]
+ else:
+ commands.append(prefix)
+
+ # Adds /help commands.
+
+ usage_info = config.get('help.usage', {})
+
+ for cmd in usage_info.keys():
+ commands.append('/help ' + cmd)
+
+ return commands
+
+
+class Autocompleter(object):
+ def __init__(self, controller):
+ self._commands = _get_commands(controller)
+
+ @lru_cache()
+ def matches(self, text):
+ """
+ Provides autocompletion matches for the given text.
+
+ :param str text: text to check for autocompletion matches with
+
+ :returns: **list** with possible matches
+ """
+
+ lowercase_text = text.lower()
+ return [cmd for cmd in self._commands if cmd.lower().startswith(lowercase_text)]
+
+ def complete(self, text, state):
+ """
+ Provides case insensetive autocompletion options, acting as a functor for
+ the readlines set_completer function.
+
+ :param str text: text to check for autocompletion matches with
+ :param int state: index of result to be provided, readline fetches matches
+ until this function provides None
+
+ :returns: **str** with the autocompletion match, **None** if eithe none
+ exists or state is higher than our number of matches
+ """
+
+ try:
+ return self.matches(text)[state]
+ except IndexError:
+ return None
diff --git a/stem/interpreter/commands.py b/stem/interpreter/commands.py
new file mode 100644
index 0000000..28b3e59
--- /dev/null
+++ b/stem/interpreter/commands.py
@@ -0,0 +1,299 @@
+"""
+Handles making requests and formatting the responses.
+"""
+
+import code
+
+import stem
+import stem.control
+import stem.interpreter.help
+import stem.util.connection
+import stem.util.tor_tools
+
+from stem.interpreter import STANDARD_OUTPUT, BOLD_OUTPUT, ERROR_OUTPUT, uses_settings, msg
+from stem.util.term import format
+
+
+def _get_fingerprint(arg, controller):
+ """
+ Resolves user input into a relay fingerprint. This accepts...
+
+ * Fingerprints
+ * Nicknames
+ * IPv4 addresses, either with or without an ORPort
+ * Empty input, which is resolved to ourselves if we're a relay
+
+ :param str arg: input to be resolved to a relay fingerprint
+ :param stem.control.Controller controller: tor control connection
+
+ :returns: **str** for the relay fingerprint
+
+ :raises: **ValueError** if we're unable to resolve the input to a relay
+ """
+
+ if not arg:
+ try:
+ return controller.get_info('fingerprint')
+ except:
+ raise ValueError("We aren't a relay, no information to provide")
+ elif stem.util.tor_tools.is_valid_fingerprint(arg):
+ return arg
+ elif stem.util.tor_tools.is_valid_nickname(arg):
+ try:
+ return controller.get_network_status(arg).fingerprint
+ except:
+ raise ValueError("Unable to find a relay with the nickname of '%s'" % arg)
+ elif ':' in arg or stem.util.connection.is_valid_ipv4_address(arg):
+ if ':' in arg:
+ address, port = arg.split(':', 1)
+
+ if not stem.util.connection.is_valid_ipv4_address(address):
+ raise ValueError("'%s' isn't a valid IPv4 address" % address)
+ elif port and not stem.util.connection.is_valid_port(port):
+ raise ValueError("'%s' isn't a valid port" % port)
+
+ port = int(port)
+ else:
+ address, port = arg, None
+
+ matches = {}
+
+ for desc in controller.get_network_statuses():
+ if desc.address == address:
+ if not port or desc.or_port == port:
+ matches[desc.or_port] = desc.fingerprint
+
+ if len(matches) == 0:
+ raise ValueError('No relays found at %s' % arg)
+ elif len(matches) == 1:
+ return matches.values()[0]
+ else:
+ response = "There's multiple relays at %s, include a port to specify which.\n\n" % arg
+
+ for i, or_port in enumerate(matches):
+ response += ' %i. %s:%s, fingerprint: %s\n' % (i + 1, address, or_port, matches[or_port])
+
+ raise ValueError(response)
+ else:
+ raise ValueError("'%s' isn't a fingerprint, nickname, or IP address" % arg)
+
+
+class ControlInterpretor(code.InteractiveConsole):
+ """
+ Handles issuing requests and providing nicely formed responses, with support
+ for special irc style subcommands.
+ """
+
+ def __init__(self, controller):
+ self._received_events = []
+
+ code.InteractiveConsole.__init__(self, {
+ 'stem': stem,
+ 'stem.control': stem.control,
+ 'controller': controller,
+ 'events': self._received_events
+ })
+
+ self._controller = controller
+ self._run_python_commands = True
+
+ # Indicates if we're processing a multiline command, such as conditional
+ # block or loop.
+
+ self.is_multiline_context = False
+
+ # Intercept events our controller hears about at a pretty low level since
+ # the user will likely be requesting them by direct 'SETEVENTS' calls.
+
+ handle_event_real = self._controller._handle_event
+
+ def handle_event_wrapper(event_message):
+ handle_event_real(event_message)
+ self._received_events.append(event_message)
+
+ self._controller._handle_event = handle_event_wrapper
+
+ def do_help(self, arg):
+ """
+ Performs the '/help' operation, giving usage information for the given
+ argument or a general summary if there wasn't one.
+ """
+
+ return stem.interpreter.help.response(self._controller, arg)
+
+ def do_events(self, arg):
+ """
+ Performs the '/events' operation, dumping the events that we've received
+ belonging to the given types. If no types are specified then this provides
+ all buffered events.
+ """
+
+ events = self._received_events
+ event_types = arg.upper().split()
+
+ if event_types:
+ events = filter(lambda event: event.type in event_types, events)
+
+ return '\n'.join([format(str(event), *STANDARD_OUTPUT) for event in events])
+
+ def do_info(self, arg):
+ """
+ Performs the '/info' operation, looking up a relay by fingerprint, IP
+ address, or nickname and printing its descriptor and consensus entries in a
+ pretty fashion.
+ """
+
+ try:
+ fingerprint = _get_fingerprint(arg, self._controller)
+ except ValueError as exc:
+ return format(str(exc), *ERROR_OUTPUT)
+
+ micro_desc = self._controller.get_microdescriptor(fingerprint, None)
+ server_desc = self._controller.get_server_descriptor(fingerprint, None)
+ ns_desc = self._controller.get_network_status(fingerprint, None)
+
+ # We'll mostly rely on the router status entry. Either the server
+ # descriptor or microdescriptor will be missing, so we'll treat them as
+ # being optional.
+
+ if not ns_desc:
+ return format("Unable to find consensus information for %s" % fingerprint, *ERROR_OUTPUT)
+
+ locale = self._controller.get_info('ip-to-country/%s' % ns_desc.address, None)
+ locale_label = ' (%s)' % locale if locale else ''
+
+ if server_desc:
+ exit_policy_label = server_desc.exit_policy.summary()
+ elif micro_desc:
+ exit_policy_label = micro_desc.exit_policy.summary()
+ else:
+ exit_policy_label = 'Unknown'
+
+ lines = [
+ '%s (%s)' % (ns_desc.nickname, fingerprint),
+ format('address: ', *BOLD_OUTPUT) + '%s:%s%s' % (ns_desc.address, ns_desc.or_port, locale_label),
+ format('published: ', *BOLD_OUTPUT) + ns_desc.published.strftime('%H:%M:%S %d/%m/%Y'),
+ ]
+
+ if server_desc:
+ lines.append(format('os: ', *BOLD_OUTPUT) + server_desc.platform.decode('utf-8', 'replace'))
+ lines.append(format('version: ', *BOLD_OUTPUT) + str(server_desc.tor_version))
+
+ lines.append(format('flags: ', *BOLD_OUTPUT) + ', '.join(ns_desc.flags))
+ lines.append(format('exit policy: ', *BOLD_OUTPUT) + exit_policy_label)
+
+ if server_desc:
+ contact = server_desc.contact
+
+ # clears up some highly common obscuring
+
+ for alias in (' at ', ' AT '):
+ contact = contact.replace(alias, '@')
+
+ for alias in (' dot ', ' DOT '):
+ contact = contact.replace(alias, '.')
+
+ lines.append(format('contact: ', *BOLD_OUTPUT) + contact)
+
+ return '\n'.join(lines)
+
+ def do_python(self, arg):
+ """
+ Performs the '/python' operation, toggling if we accept python commands or
+ not.
+ """
+
+ if not arg:
+ status = 'enabled' if self._run_python_commands else 'disabled'
+ return format('Python support is presently %s.' % status, *STANDARD_OUTPUT)
+ elif arg.lower() == 'enable':
+ self._run_python_commands = True
+ elif arg.lower() == 'disable':
+ self._run_python_commands = False
+ else:
+ return format("'%s' is not recognized. Please run either '/python enable' or '/python disable'." % arg, *ERROR_OUTPUT)
+
+ if self._run_python_commands:
+ response = "Python support enabled, we'll now run non-interpreter commands as python."
+ else:
+ response = "Python support disabled, we'll now pass along all commands to tor."
+
+ return format(response, *STANDARD_OUTPUT)
+
+ @uses_settings
+ def run_command(self, command, config):
+ """
+ Runs the given command. Requests starting with a '/' are special commands
+ to the interpreter, and anything else is sent to the control port.
+
+ :param stem.control.Controller controller: tor control connection
+ :param str command: command to be processed
+
+ :returns: **list** out output lines, each line being a list of
+ (msg, format) tuples
+
+ :raises: **stem.SocketClosed** if the control connection has been severed
+ """
+
+ if not self._controller.is_alive():
+ raise stem.SocketClosed()
+
+ # Commands fall into three categories:
+ #
+ # * Interpretor commands. These start with a '/'.
+ #
+ # * Controller commands stem knows how to handle. We use our Controller's
+ # methods for these to take advantage of caching and present nicer
+ # output.
+ #
+ # * Other tor commands. We pass these directly on to the control port.
+
+ cmd, arg = command.strip(), ''
+
+ if ' ' in cmd:
+ cmd, arg = cmd.split(' ', 1)
+
+ output = ''
+
+ if cmd.startswith('/'):
+ cmd = cmd.lower()
+
+ if cmd == '/quit':
+ raise stem.SocketClosed()
+ elif cmd == '/events':
+ output = self.do_events(arg)
+ elif cmd == '/info':
+ output = self.do_info(arg)
+ elif cmd == '/python':
+ output = self.do_python(arg)
+ elif cmd == '/help':
+ output = self.do_help(arg)
+ else:
+ output = format("'%s' isn't a recognized command" % command, *ERROR_OUTPUT)
+ else:
+ cmd = cmd.upper() # makes commands uppercase to match the spec
+
+ if cmd.replace('+', '') in ('LOADCONF', 'POSTDESCRIPTOR'):
+ # provides a notice that multi-line controller input isn't yet implemented
+ output = format(msg('msg.multiline_unimplemented_notice'), *ERROR_OUTPUT)
+ elif cmd == 'QUIT':
+ self._controller.msg(command)
+ raise stem.SocketClosed()
+ else:
+ is_tor_command = cmd in config.get('help.usage', {}) and cmd.lower() != 'events'
+
+ if self._run_python_commands and not is_tor_command:
+ self.is_multiline_context = code.InteractiveConsole.push(self, command)
+ return
+ else:
+ try:
+ output = format(self._controller.msg(command).raw_content().strip(), *STANDARD_OUTPUT)
+ except stem.ControllerError as exc:
+ if isinstance(exc, stem.SocketClosed):
+ raise exc
+ else:
+ output = format(str(exc), *ERROR_OUTPUT)
+
+ output += '\n' # give ourselves an extra line before the next prompt
+
+ return output
diff --git a/stem/interpreter/help.py b/stem/interpreter/help.py
new file mode 100644
index 0000000..e17fe3f
--- /dev/null
+++ b/stem/interpreter/help.py
@@ -0,0 +1,142 @@
+"""
+Provides our /help responses.
+"""
+
+from stem.interpreter import (
+ STANDARD_OUTPUT,
+ BOLD_OUTPUT,
+ ERROR_OUTPUT,
+ msg,
+ uses_settings,
+)
+
+from stem.util.term import format
+
+try:
+ # added in python 3.2
+ from functools import lru_cache
+except ImportError:
+ from stem.util.lru_cache import lru_cache
+
+
+def response(controller, arg):
+ """
+ Provides our /help response.
+
+ :param stem.control.Controller controller: tor control connection
+ :param str arg: controller or interpreter command to provide help output for
+
+ :returns: **str** with our help response
+ """
+
+ # Normalizing inputs first so we can better cache responses.
+
+ return _response(controller, _normalize(arg))
+
+
+def _normalize(arg):
+ arg = arg.upper()
+
+ # If there's multiple arguments then just take the first. This is
+ # particularly likely if they're trying to query a full command (for
+ # instance "/help GETINFO version")
+
+ arg = arg.split(' ')[0]
+
+ # strip slash if someone enters an interpreter command (ex. "/help /help")
+
+ if arg.startswith('/'):
+ arg = arg[1:]
+
+ return arg
+
+
+@lru_cache()
+@uses_settings
+def _response(controller, arg, config):
+ if not arg:
+ return _general_help()
+
+ usage_info = config.get('help.usage', {})
+
+ if not arg in usage_info:
+ return format("No help information available for '%s'..." % arg, *ERROR_OUTPUT)
+
+ output = format(usage_info[arg] + '\n', *BOLD_OUTPUT)
+
+ description = config.get('help.description.%s' % arg.lower(), '')
+
+ for line in description.splitlines():
+ output += format(' ' + line, *STANDARD_OUTPUT) + '\n'
+
+ output += '\n'
+
+ if arg == 'GETINFO':
+ results = controller.get_info('info/names', None)
+
+ if results:
+ for line in results.splitlines():
+ if ' -- ' in line:
+ opt, summary = line.split(' -- ', 1)
+
+ output += format('%-33s' % opt, *BOLD_OUTPUT)
+ output += format(' - %s' % summary, *STANDARD_OUTPUT) + '\n'
+ elif arg == 'GETCONF':
+ results = controller.get_info('config/names', None)
+
+ if results:
+ options = [opt.split(' ', 1)[0] for opt in results.splitlines()]
+
+ for i in range(0, len(options), 2):
+ line = ''
+
+ for entry in options[i:i + 2]:
+ line += '%-42s' % entry
+
+ output += format(line.rstrip(), *STANDARD_OUTPUT) + '\n'
+ elif arg == 'SIGNAL':
+ signal_options = config.get('help.signal.options', {})
+
+ for signal, summary in signal_options.items():
+ output += format('%-15s' % signal, *BOLD_OUTPUT)
+ output += format(' - %s' % summary, *STANDARD_OUTPUT) + '\n'
+ elif arg == 'SETEVENTS':
+ results = controller.get_info('events/names', None)
+
+ if results:
+ entries = results.split()
+
+ # displays four columns of 20 characters
+
+ for i in range(0, len(entries), 4):
+ line = ''
+
+ for entry in entries[i:i + 4]:
+ line += '%-20s' % entry
+
+ output += format(line.rstrip(), *STANDARD_OUTPUT) + '\n'
+ elif arg == 'USEFEATURE':
+ results = controller.get_info('features/names', None)
+
+ if results:
+ output += format(results, *STANDARD_OUTPUT) + '\n'
+ elif arg in ('LOADCONF', 'POSTDESCRIPTOR'):
+ # gives a warning that this option isn't yet implemented
+ output += format(msg('msg.multiline_unimplemented_notice'), *ERROR_OUTPUT) + '\n'
+
+ return output.rstrip()
+
+
+def _general_help():
+ lines = []
+
+ for line in msg('help.general').splitlines():
+ div = line.find(' - ')
+
+ if div != -1:
+ cmd, description = line[:div], line[div:]
+ lines.append(format(cmd, *BOLD_OUTPUT) + format(description, *STANDARD_OUTPUT))
+ else:
+ lines.append(format(line, *BOLD_OUTPUT))
+
+ return '\n'.join(lines)
diff --git a/stem/interpreter/settings.cfg b/stem/interpreter/settings.cfg
new file mode 100644
index 0000000..0ddf080
--- /dev/null
+++ b/stem/interpreter/settings.cfg
@@ -0,0 +1,295 @@
+################################################################################
+#
+# Configuration data used by Stem's interpreter prompt.
+#
+################################################################################
+
+ ##################
+# GENERAL MESSAGES #
+ ##################
+
+msg.multiline_unimplemented_notice Multi-line control options like this are not yet implemented.
+
+msg.help
+|Interactive interpreter for Tor. This provides you with direct access
+|to Tor's control interface via either python or direct requests.
+|
+| -i, --interface [ADDRESS:]PORT change control interface from {address}:{port}
+| -s, --socket SOCKET_PATH attach using unix domain socket if present,
+| SOCKET_PATH defaults to: {socket}
+| --no-color disables colorized output
+| -h, --help presents this help
+|
+
+msg.startup_banner
+|Welcome to Stem's interpreter prompt. This provides you with direct access to
+|Tor's control interface.
+|
+|This acts like a standard python interpreter with a Tor connection available
+|via your 'controller' variable...
+|
+| >>> controller.get_info('version')
+| '0.2.5.1-alpha-dev (git-245ecfff36c0cecc)'
+|
+|You can also issue requests directly to Tor...
+|
+| >>> GETINFO version
+| 250-version=0.2.5.1-alpha-dev (git-245ecfff36c0cecc)
+| 250 OK
+|
+|For more information run '/help'.
+|
+
+msg.tor_unavailable Tor isn't running and the command presently isn't in your PATH.
+
+msg.starting_tor
+|Tor isn't running. Starting a temporary Tor instance for our interpreter to
+|interact with. This will have a minimal non-relaying configuration, and be
+|shut down when you're done.
+|
+|--------------------------------------------------------------------------------
+|
+
+ #################
+# OUTPUT OF /HELP #
+ #################
+
+# Response for the '/help' command without any arguments.
+
+help.general
+|Interpretor commands include:
+| /help - provides information for interpreter and tor commands
+| /events - prints events that we've received
+| /info - general information for a relay
+| /python - enable or disable support for running python commands
+| /quit - shuts down the interpreter
+|
+|Tor commands include:
+| GETINFO - queries information from tor
+| GETCONF, SETCONF, RESETCONF - show or edit a configuration option
+| SIGNAL - issues control signal to the process (for resetting, stopping, etc)
+| SETEVENTS - configures the events tor will notify us of
+|
+| USEFEATURE - enables custom behavior for the controller
+| SAVECONF - writes tor's current configuration to our torrc
+| LOADCONF - loads the given input like it was part of our torrc
+| MAPADDRESS - replaces requests for one address with another
+| POSTDESCRIPTOR - adds a relay descriptor to our cache
+| EXTENDCIRCUIT - create or extend a tor circuit
+| SETCIRCUITPURPOSE - configures the purpose associated with a circuit
+| CLOSECIRCUIT - closes the given circuit
+| ATTACHSTREAM - associates an application's stream with a tor circuit
+| REDIRECTSTREAM - sets a stream's destination
+| CLOSESTREAM - closes the given stream
+| RESOLVE - issues an asynchronous dns or rdns request over tor
+| TAKEOWNERSHIP - instructs tor to quit when this control connection is closed
+| PROTOCOLINFO - queries version and controller authentication information
+| QUIT - disconnect the control connection
+|
+|For more information use '/help [OPTION]'.
+
+# Usage of tor and interpreter commands.
+
+help.usage HELP => /help [OPTION]
+help.usage EVENTS => /events [types]
+help.usage INFO => /info [relay fingerprint, nickname, or IP address]
+help.usage PYTHON => /python [enable,disable]
+help.usage QUIT => /quit
+help.usage GETINFO => GETINFO OPTION
+help.usage GETCONF => GETCONF OPTION
+help.usage SETCONF => SETCONF PARAM[=VALUE]
+help.usage RESETCONF => RESETCONF PARAM[=VALUE]
+help.usage SIGNAL => SIGNAL SIG
+help.usage SETEVENTS => SETEVENTS [EXTENDED] [EVENTS]
+help.usage USEFEATURE => USEFEATURE OPTION
+help.usage SAVECONF => SAVECONF
+help.usage LOADCONF => LOADCONF...
+help.usage MAPADDRESS => MAPADDRESS SOURCE_ADDR=DESTINATION_ADDR
+help.usage POSTDESCRIPTOR => POSTDESCRIPTOR [purpose=general/controller/bridge] [cache=yes/no]...
+help.usage EXTENDCIRCUIT => EXTENDCIRCUIT CircuitID [PATH] [purpose=general/controller]
+help.usage SETCIRCUITPURPOSE => SETCIRCUITPURPOSE CircuitID purpose=general/controller
+help.usage CLOSECIRCUIT => CLOSECIRCUIT CircuitID [IfUnused]
+help.usage ATTACHSTREAM => ATTACHSTREAM StreamID CircuitID [HOP=HopNum]
+help.usage REDIRECTSTREAM => REDIRECTSTREAM StreamID Address [Port]
+help.usage CLOSESTREAM => CLOSESTREAM StreamID Reason [Flag]
+help.usage RESOLVE => RESOLVE [mode=reverse] address
+help.usage TAKEOWNERSHIP => TAKEOWNERSHIP
+help.usage PROTOCOLINFO => PROTOCOLINFO [ProtocolVersion]
+
+# Longer description of what tor and interpreter commands do.
+
+help.description.help
+|Provides usage information for the given interpreter, tor command, or tor
+|configuration option.
+|
+|Example:
+| /help info # provides a description of the '/info' option
+| /help GETINFO # usage information for tor's GETINFO controller option
+
+help.description.events
+|Provides events that we've received belonging to the given event types. If
+|no types are specified then this provides all the messages that we've
+|received.
+
+help.description.info
+|Provides general information for a relay that's currently in the consensus.
+|If no relay is specified then this provides information on ourselves.
+
+help.description.python
+|Enables or disables support for running python commands. This determines how
+|we treat commands this interpreter doesn't recognize...
+|
+|* If enabled then unrecognized commands are executed as python.
+|* If disabled then unrecognized commands are passed along to tor.
+
+help.description.quit
+|Terminates the interpreter.
+
+help.description.getinfo
+|Queries the tor process for information. Options are...
+|
+
+help.description.getconf
+|Provides the current value for a given configuration value. Options include...
+|
+
+help.description.setconf
+|Sets the given configuration parameters. Values can be quoted or non-quoted
+|strings, and reverts the option to 0 or NULL if not provided.
+|
+|Examples:
+| * Sets a contact address and resets our family to NULL
+| SETCONF MyFamily ContactInfo=foo(a)bar.com
+|
+| * Sets an exit policy that only includes port 80/443
+| SETCONF ExitPolicy=\"accept *:80, accept *:443, reject *:*\"\
+
+help.description.resetconf
+|Reverts the given configuration options to their default values. If a value
+|is provided then this behaves in the same way as SETCONF.
+|
+|Examples:
+| * Returns both of our accounting parameters to their defaults
+| RESETCONF AccountingMax AccountingStart
+|
+| * Uses the default exit policy and sets our nickname to be 'Goomba'
+| RESETCONF ExitPolicy Nickname=Goomba
+
+help.description.signal
+|Issues a signal that tells the tor process to reload its torrc, dump its
+|stats, halt, etc.
+
+help.description.setevents
+|Sets the events that we will receive. This turns off any events that aren't
+|listed so sending 'SETEVENTS' without any values will turn off all event reporting.
+|
+|For Tor versions between 0.1.1.9 and 0.2.2.1 adding 'EXTENDED' causes some
+|events to give us additional information. After version 0.2.2.1 this is
+|always on.
+|
+|Events include...
+|
+
+help.description.usefeature
+|Customizes the behavior of the control port. Options include...
+|
+
+help.description.saveconf
+|Writes Tor's current configuration to its torrc.
+
+help.description.loadconf
+|Reads the given text like it belonged to our torrc.
+|
+|Example:
+| +LOADCONF
+| # sets our exit policy to just accept ports 80 and 443
+| ExitPolicy accept *:80
+| ExitPolicy accept *:443
+| ExitPolicy reject *:*
+| .
+
+help.description.mapaddress
+|Replaces future requests for one address with another.
+|
+|Example:
+| MAPADDRESS 0.0.0.0=torproject.org 1.2.3.4=tor.freehaven.net
+
+help.description.postdescriptor
+|Simulates getting a new relay descriptor.
+
+help.description.extendcircuit
+|Extends the given circuit or create a new one if the CircuitID is zero. The
+|PATH is a comma separated list of fingerprints. If it isn't set then this
+|uses Tor's normal path selection.
+
+help.description.setcircuitpurpose
+|Sets the purpose attribute for a circuit.
+
+help.description.closecircuit
+|Closes the given circuit. If "IfUnused" is included then this only closes
+|the circuit if it isn't currently being used.
+
+help.description.attachstream
+|Attaches a stream with the given built circuit (tor picks one on its own if
+|CircuitID is zero). If HopNum is given then this hop is used to exit the
+|circuit, otherwise the last relay is used.
+
+help.description.redirectstream
+|Sets the destination for a given stream. This can only be done after a
+|stream is created but before it's attached to a circuit.
+
+help.description.closestream
+|Closes the given stream, the reason being an integer matching a reason as
+|per section 6.3 of the tor-spec.
+
+help.description.resolve
+|Performs IPv4 DNS resolution over tor, doing a reverse lookup instead if
+|"mode=reverse" is included. This request is processed in the background and
+|results in a ADDRMAP event with the response.
+
+help.description.takeownership
+|Instructs Tor to gracefully shut down when this control connection is closed.
+
+help.description.protocolinfo
+|Provides bootstrapping information that a controller might need when first
+|starting, like Tor's version and controller authentication. This can be done
+|before authenticating to the control port.
+
+help.signal.options RELOAD / HUP => reload our torrc
+help.signal.options SHUTDOWN / INT => gracefully shut down, waiting 30 seconds if we're a relay
+help.signal.options DUMP / USR1 => logs information about open connections and circuits
+help.signal.options DEBUG / USR2 => makes us log at the DEBUG runlevel
+help.signal.options HALT / TERM => immediately shut down
+help.signal.options CLEARDNSCACHE => clears any cached DNS results
+help.signal.options NEWNYM => clears the DNS cache and uses new circuits for future connections
+
+ ################
+# TAB COMPLETION #
+ ################
+
+# Commands we'll autocomplete when the user hits tab. This is just the start of
+# our autocompletion list - more are determined dynamically by checking what
+# tor supports.
+
+autocomplete /help
+autocomplete /events
+autocomplete /info
+autocomplete /quit
+autocomplete SAVECONF
+autocomplete MAPADDRESS
+autocomplete EXTENDCIRCUIT
+autocomplete SETCIRCUITPURPOSE
+autocomplete SETROUTERPURPOSE
+autocomplete ATTACHSTREAM
+#autocomplete +POSTDESCRIPTOR # TODO: needs multi-line support
+autocomplete REDIRECTSTREAM
+autocomplete CLOSESTREAM
+autocomplete CLOSECIRCUIT
+autocomplete QUIT
+autocomplete RESOLVE
+autocomplete PROTOCOLINFO
+#autocomplete +LOADCONF # TODO: needs multi-line support
+autocomplete TAKEOWNERSHIP
+autocomplete AUTHCHALLENGE
+autocomplete DROPGUARDS
+
diff --git a/stem/interpretor/__init__.py b/stem/interpretor/__init__.py
deleted file mode 100644
index 5ecd356..0000000
--- a/stem/interpretor/__init__.py
+++ /dev/null
@@ -1,129 +0,0 @@
-# Copyright 2014, Damian Johnson and The Tor Project
-# See LICENSE for licensing information
-
-"""
-Interactive interpretor for interacting with Tor directly. This adds usability
-features such as tab completion, history, and IRC-style functions (like /help).
-"""
-
-__all__ = ['arguments', 'autocomplete', 'commands', 'help', 'msg']
-
-import os
-import sys
-
-import stem
-import stem.connection
-import stem.process
-import stem.util.conf
-import stem.util.system
-import stem.util.term
-
-from stem.util.term import RESET, Attr, Color, format
-
-# Our color prompt triggers a bug between raw_input() and readline history,
-# where scrolling through history widens our prompt. Widening our prompt via
-# invisible characters (like resets) seems to sidestep this bug for short
-# inputs. Contrary to the ticket, this still manifests with python 2.7.1...
-#
-# http://bugs.python.org/issue12972
-
-PROMPT = format('>>> ', Color.GREEN, Attr.BOLD) + RESET * 10
-
-STANDARD_OUTPUT = (Color.BLUE, )
-BOLD_OUTPUT = (Color.BLUE, Attr.BOLD)
-HEADER_OUTPUT = (Color.GREEN, )
-HEADER_BOLD_OUTPUT = (Color.GREEN, Attr.BOLD)
-ERROR_OUTPUT = (Attr.BOLD, Color.RED)
-
-settings_path = os.path.join(os.path.dirname(__file__), 'settings.cfg')
-uses_settings = stem.util.conf.uses_settings('stem_interpretor', settings_path)
-
-
-@uses_settings
-def msg(message, config, **attr):
- return config.get(message).format(**attr)
-
-
-def main():
- import readline
-
- import stem.interpretor.arguments
- import stem.interpretor.autocomplete
- import stem.interpretor.commands
-
- try:
- args = stem.interpretor.arguments.parse(sys.argv[1:])
- except ValueError as exc:
- print exc
- sys.exit(1)
-
- if args.print_help:
- print stem.interpretor.arguments.get_help()
- sys.exit()
-
- if args.disable_color:
- global PROMPT
- stem.util.term.DISABLE_COLOR_SUPPORT = True
- PROMPT = '>>> '
-
- # If the user isn't connecting to something in particular then offer to start
- # tor if it isn't running.
-
- if not (args.user_provided_port or args.user_provided_socket):
- is_tor_running = stem.util.system.is_running('tor') or stem.util.system.is_running('tor.real')
-
- if not is_tor_running:
- if not stem.util.system.is_available('tor'):
- print format(msg('msg.tor_unavailable'), *ERROR_OUTPUT)
- sys.exit(1)
- else:
- print format(msg('msg.starting_tor'), *HEADER_OUTPUT)
-
- stem.process.launch_tor_with_config(
- config = {
- 'SocksPort': '0',
- 'ControlPort': str(args.control_port),
- 'CookieAuthentication': '1',
- 'ExitPolicy': 'reject *:*',
- },
- completion_percent = 5,
- take_ownership = True,
- )
-
- control_port = None if args.user_provided_socket else (args.control_address, args.control_port)
- control_socket = None if args.user_provided_port else args.control_socket
-
- controller = stem.connection.connect(
- control_port = control_port,
- control_socket = control_socket,
- password_prompt = True,
- )
-
- if controller is None:
- sys.exit(1)
-
- with controller:
- autocompleter = stem.interpretor.autocomplete.Autocompleter(controller)
- readline.parse_and_bind('tab: complete')
- readline.set_completer(autocompleter.complete)
- readline.set_completer_delims('\n')
-
- interpretor = stem.interpretor.commands.ControlInterpretor(controller)
-
- for line in msg('msg.startup_banner').splitlines():
- line_format = HEADER_BOLD_OUTPUT if line.startswith(' ') else HEADER_OUTPUT
- print format(line, *line_format)
-
- print
-
- while True:
- try:
- prompt = '... ' if interpretor.is_multiline_context else PROMPT
- user_input = raw_input(prompt)
- response = interpretor.run_command(user_input)
-
- if response is not None:
- print response
- except (KeyboardInterrupt, EOFError, stem.SocketClosed) as exc:
- print # move cursor to the following line
- break
diff --git a/stem/interpretor/arguments.py b/stem/interpretor/arguments.py
deleted file mode 100644
index 278d2a0..0000000
--- a/stem/interpretor/arguments.py
+++ /dev/null
@@ -1,96 +0,0 @@
-# Copyright 2014, Damian Johnson and The Tor Project
-# See LICENSE for licensing information
-
-"""
-Commandline argument parsing for our interpretor prompt.
-"""
-
-import collections
-import getopt
-
-import stem.interpretor
-import stem.util.connection
-
-DEFAULT_ARGS = {
- 'control_address': '127.0.0.1',
- 'control_port': 9051,
- 'user_provided_port': False,
- 'control_socket': '/var/run/tor/control',
- 'user_provided_socket': False,
- 'disable_color': False,
- 'print_help': False,
-}
-
-OPT = 'i:s:h'
-
-OPT_EXPANDED = [
- 'interface=',
- 'socket=',
- 'no-color',
- 'help',
-]
-
-
-def parse(argv):
- """
- Parses our arguments, providing a named tuple with their values.
-
- :param list argv: input arguments to be parsed
-
- :returns: a **named tuple** with our parsed arguments
-
- :raises: **ValueError** if we got an invalid argument
- """
-
- args = dict(DEFAULT_ARGS)
-
- try:
- getopt_results = getopt.getopt(argv, OPT, OPT_EXPANDED)[0]
- except getopt.GetoptError as exc:
- raise ValueError('%s (for usage provide --help)' % exc)
-
- for opt, arg in getopt_results:
- if opt in ('-i', '--interface'):
- if ':' in arg:
- address, port = arg.split(':', 1)
- else:
- address, port = None, arg
-
- if address is not None:
- if not stem.util.connection.is_valid_ipv4_address(address):
- raise ValueError("'%s' isn't a valid IPv4 address" % address)
-
- args['control_address'] = address
-
- if not stem.util.connection.is_valid_port(port):
- raise ValueError("'%s' isn't a valid port number" % port)
-
- args['control_port'] = int(port)
- args['user_provided_port'] = True
- elif opt in ('-s', '--socket'):
- args['control_socket'] = arg
- args['user_provided_socket'] = True
- elif opt == '--no-color':
- args['disable_color'] = True
- elif opt in ('-h', '--help'):
- args['print_help'] = True
-
- # translates our args dict into a named tuple
-
- Args = collections.namedtuple('Args', args.keys())
- return Args(**args)
-
-
-def get_help():
- """
- Provides our --help usage information.
-
- :returns: **str** with our usage information
- """
-
- return stem.interpretor.msg(
- 'msg.help',
- address = DEFAULT_ARGS['control_address'],
- port = DEFAULT_ARGS['control_port'],
- socket = DEFAULT_ARGS['control_socket'],
- )
diff --git a/stem/interpretor/autocomplete.py b/stem/interpretor/autocomplete.py
deleted file mode 100644
index f42084e..0000000
--- a/stem/interpretor/autocomplete.py
+++ /dev/null
@@ -1,112 +0,0 @@
-"""
-Tab completion for our interpretor prompt.
-"""
-
-from stem.interpretor import uses_settings
-
-try:
- # added in python 3.2
- from functools import lru_cache
-except ImportError:
- from stem.util.lru_cache import lru_cache
-
-
-@uses_settings
-def _get_commands(controller, config):
- """
- Provides commands recognized by tor.
- """
-
- commands = config.get('autocomplete', [])
-
- if controller is None:
- return commands
-
- # GETINFO commands. Lines are of the form '[option] -- [description]'. This
- # strips '*' from options that accept values.
-
- results = controller.get_info('info/names', None)
-
- if results:
- for line in results.splitlines():
- option = line.split(' ', 1)[0].rstrip('*')
- commands.append('GETINFO %s' % option)
- else:
- commands.append('GETINFO ')
-
- # GETCONF, SETCONF, and RESETCONF commands. Lines are of the form
- # '[option] [type]'.
-
- results = controller.get_info('config/names', None)
-
- if results:
- for line in results.splitlines():
- option = line.split(' ', 1)[0]
-
- commands.append('GETCONF %s' % option)
- commands.append('SETCONF %s' % option)
- commands.append('RESETCONF %s' % option)
- else:
- commands += ['GETCONF ', 'SETCONF ', 'RESETCONF ']
-
- # SETEVENT, USEFEATURE, and SIGNAL commands. For each of these the GETINFO
- # results are simply a space separated lists of the values they can have.
-
- options = (
- ('SETEVENTS ', 'events/names'),
- ('USEFEATURE ', 'features/names'),
- ('SIGNAL ', 'signal/names'),
- )
-
- for prefix, getinfo_cmd in options:
- results = controller.get_info(getinfo_cmd, None)
-
- if results:
- commands += [prefix + value for value in results.split()]
- else:
- commands.append(prefix)
-
- # Adds /help commands.
-
- usage_info = config.get('help.usage', {})
-
- for cmd in usage_info.keys():
- commands.append('/help ' + cmd)
-
- return commands
-
-
-class Autocompleter(object):
- def __init__(self, controller):
- self._commands = _get_commands(controller)
-
- @lru_cache()
- def matches(self, text):
- """
- Provides autocompletion matches for the given text.
-
- :param str text: text to check for autocompletion matches with
-
- :returns: **list** with possible matches
- """
-
- lowercase_text = text.lower()
- return [cmd for cmd in self._commands if cmd.lower().startswith(lowercase_text)]
-
- def complete(self, text, state):
- """
- Provides case insensetive autocompletion options, acting as a functor for
- the readlines set_completer function.
-
- :param str text: text to check for autocompletion matches with
- :param int state: index of result to be provided, readline fetches matches
- until this function provides None
-
- :returns: **str** with the autocompletion match, **None** if eithe none
- exists or state is higher than our number of matches
- """
-
- try:
- return self.matches(text)[state]
- except IndexError:
- return None
diff --git a/stem/interpretor/commands.py b/stem/interpretor/commands.py
deleted file mode 100644
index b66c87b..0000000
--- a/stem/interpretor/commands.py
+++ /dev/null
@@ -1,299 +0,0 @@
-"""
-Handles making requests and formatting the responses.
-"""
-
-import code
-
-import stem
-import stem.control
-import stem.interpretor.help
-import stem.util.connection
-import stem.util.tor_tools
-
-from stem.interpretor import STANDARD_OUTPUT, BOLD_OUTPUT, ERROR_OUTPUT, uses_settings, msg
-from stem.util.term import format
-
-
-def _get_fingerprint(arg, controller):
- """
- Resolves user input into a relay fingerprint. This accepts...
-
- * Fingerprints
- * Nicknames
- * IPv4 addresses, either with or without an ORPort
- * Empty input, which is resolved to ourselves if we're a relay
-
- :param str arg: input to be resolved to a relay fingerprint
- :param stem.control.Controller controller: tor control connection
-
- :returns: **str** for the relay fingerprint
-
- :raises: **ValueError** if we're unable to resolve the input to a relay
- """
-
- if not arg:
- try:
- return controller.get_info('fingerprint')
- except:
- raise ValueError("We aren't a relay, no information to provide")
- elif stem.util.tor_tools.is_valid_fingerprint(arg):
- return arg
- elif stem.util.tor_tools.is_valid_nickname(arg):
- try:
- return controller.get_network_status(arg).fingerprint
- except:
- raise ValueError("Unable to find a relay with the nickname of '%s'" % arg)
- elif ':' in arg or stem.util.connection.is_valid_ipv4_address(arg):
- if ':' in arg:
- address, port = arg.split(':', 1)
-
- if not stem.util.connection.is_valid_ipv4_address(address):
- raise ValueError("'%s' isn't a valid IPv4 address" % address)
- elif port and not stem.util.connection.is_valid_port(port):
- raise ValueError("'%s' isn't a valid port" % port)
-
- port = int(port)
- else:
- address, port = arg, None
-
- matches = {}
-
- for desc in controller.get_network_statuses():
- if desc.address == address:
- if not port or desc.or_port == port:
- matches[desc.or_port] = desc.fingerprint
-
- if len(matches) == 0:
- raise ValueError('No relays found at %s' % arg)
- elif len(matches) == 1:
- return matches.values()[0]
- else:
- response = "There's multiple relays at %s, include a port to specify which.\n\n" % arg
-
- for i, or_port in enumerate(matches):
- response += ' %i. %s:%s, fingerprint: %s\n' % (i + 1, address, or_port, matches[or_port])
-
- raise ValueError(response)
- else:
- raise ValueError("'%s' isn't a fingerprint, nickname, or IP address" % arg)
-
-
-class ControlInterpretor(code.InteractiveConsole):
- """
- Handles issuing requests and providing nicely formed responses, with support
- for special irc style subcommands.
- """
-
- def __init__(self, controller):
- self._received_events = []
-
- code.InteractiveConsole.__init__(self, {
- 'stem': stem,
- 'stem.control': stem.control,
- 'controller': controller,
- 'events': self._received_events
- })
-
- self._controller = controller
- self._run_python_commands = True
-
- # Indicates if we're processing a multiline command, such as conditional
- # block or loop.
-
- self.is_multiline_context = False
-
- # Intercept events our controller hears about at a pretty low level since
- # the user will likely be requesting them by direct 'SETEVENTS' calls.
-
- handle_event_real = self._controller._handle_event
-
- def handle_event_wrapper(event_message):
- handle_event_real(event_message)
- self._received_events.append(event_message)
-
- self._controller._handle_event = handle_event_wrapper
-
- def do_help(self, arg):
- """
- Performs the '/help' operation, giving usage information for the given
- argument or a general summary if there wasn't one.
- """
-
- return stem.interpretor.help.response(self._controller, arg)
-
- def do_events(self, arg):
- """
- Performs the '/events' operation, dumping the events that we've received
- belonging to the given types. If no types are specified then this provides
- all buffered events.
- """
-
- events = self._received_events
- event_types = arg.upper().split()
-
- if event_types:
- events = filter(lambda event: event.type in event_types, events)
-
- return '\n'.join([format(str(event), *STANDARD_OUTPUT) for event in events])
-
- def do_info(self, arg):
- """
- Performs the '/info' operation, looking up a relay by fingerprint, IP
- address, or nickname and printing its descriptor and consensus entries in a
- pretty fashion.
- """
-
- try:
- fingerprint = _get_fingerprint(arg, self._controller)
- except ValueError as exc:
- return format(str(exc), *ERROR_OUTPUT)
-
- micro_desc = self._controller.get_microdescriptor(fingerprint, None)
- server_desc = self._controller.get_server_descriptor(fingerprint, None)
- ns_desc = self._controller.get_network_status(fingerprint, None)
-
- # We'll mostly rely on the router status entry. Either the server
- # descriptor or microdescriptor will be missing, so we'll treat them as
- # being optional.
-
- if not ns_desc:
- return format("Unable to find consensus information for %s" % fingerprint, *ERROR_OUTPUT)
-
- locale = self._controller.get_info('ip-to-country/%s' % ns_desc.address, None)
- locale_label = ' (%s)' % locale if locale else ''
-
- if server_desc:
- exit_policy_label = server_desc.exit_policy.summary()
- elif micro_desc:
- exit_policy_label = micro_desc.exit_policy.summary()
- else:
- exit_policy_label = 'Unknown'
-
- lines = [
- '%s (%s)' % (ns_desc.nickname, fingerprint),
- format('address: ', *BOLD_OUTPUT) + '%s:%s%s' % (ns_desc.address, ns_desc.or_port, locale_label),
- format('published: ', *BOLD_OUTPUT) + ns_desc.published.strftime('%H:%M:%S %d/%m/%Y'),
- ]
-
- if server_desc:
- lines.append(format('os: ', *BOLD_OUTPUT) + server_desc.platform.decode('utf-8', 'replace'))
- lines.append(format('version: ', *BOLD_OUTPUT) + str(server_desc.tor_version))
-
- lines.append(format('flags: ', *BOLD_OUTPUT) + ', '.join(ns_desc.flags))
- lines.append(format('exit policy: ', *BOLD_OUTPUT) + exit_policy_label)
-
- if server_desc:
- contact = server_desc.contact
-
- # clears up some highly common obscuring
-
- for alias in (' at ', ' AT '):
- contact = contact.replace(alias, '@')
-
- for alias in (' dot ', ' DOT '):
- contact = contact.replace(alias, '.')
-
- lines.append(format('contact: ', *BOLD_OUTPUT) + contact)
-
- return '\n'.join(lines)
-
- def do_python(self, arg):
- """
- Performs the '/python' operation, toggling if we accept python commands or
- not.
- """
-
- if not arg:
- status = 'enabled' if self._run_python_commands else 'disabled'
- return format('Python support is presently %s.' % status, *STANDARD_OUTPUT)
- elif arg.lower() == 'enable':
- self._run_python_commands = True
- elif arg.lower() == 'disable':
- self._run_python_commands = False
- else:
- return format("'%s' is not recognized. Please run either '/python enable' or '/python disable'." % arg, *ERROR_OUTPUT)
-
- if self._run_python_commands:
- response = "Python support enabled, we'll now run non-interpretor commands as python."
- else:
- response = "Python support disabled, we'll now pass along all commands to tor."
-
- return format(response, *STANDARD_OUTPUT)
-
- @uses_settings
- def run_command(self, command, config):
- """
- Runs the given command. Requests starting with a '/' are special commands
- to the interpretor, and anything else is sent to the control port.
-
- :param stem.control.Controller controller: tor control connection
- :param str command: command to be processed
-
- :returns: **list** out output lines, each line being a list of
- (msg, format) tuples
-
- :raises: **stem.SocketClosed** if the control connection has been severed
- """
-
- if not self._controller.is_alive():
- raise stem.SocketClosed()
-
- # Commands fall into three categories:
- #
- # * Interpretor commands. These start with a '/'.
- #
- # * Controller commands stem knows how to handle. We use our Controller's
- # methods for these to take advantage of caching and present nicer
- # output.
- #
- # * Other tor commands. We pass these directly on to the control port.
-
- cmd, arg = command.strip(), ''
-
- if ' ' in cmd:
- cmd, arg = cmd.split(' ', 1)
-
- output = ''
-
- if cmd.startswith('/'):
- cmd = cmd.lower()
-
- if cmd == '/quit':
- raise stem.SocketClosed()
- elif cmd == '/events':
- output = self.do_events(arg)
- elif cmd == '/info':
- output = self.do_info(arg)
- elif cmd == '/python':
- output = self.do_python(arg)
- elif cmd == '/help':
- output = self.do_help(arg)
- else:
- output = format("'%s' isn't a recognized command" % command, *ERROR_OUTPUT)
- else:
- cmd = cmd.upper() # makes commands uppercase to match the spec
-
- if cmd.replace('+', '') in ('LOADCONF', 'POSTDESCRIPTOR'):
- # provides a notice that multi-line controller input isn't yet implemented
- output = format(msg('msg.multiline_unimplemented_notice'), *ERROR_OUTPUT)
- elif cmd == 'QUIT':
- self._controller.msg(command)
- raise stem.SocketClosed()
- else:
- is_tor_command = cmd in config.get('help.usage', {}) and cmd.lower() != 'events'
-
- if self._run_python_commands and not is_tor_command:
- self.is_multiline_context = code.InteractiveConsole.push(self, command)
- return
- else:
- try:
- output = format(self._controller.msg(command).raw_content().strip(), *STANDARD_OUTPUT)
- except stem.ControllerError as exc:
- if isinstance(exc, stem.SocketClosed):
- raise exc
- else:
- output = format(str(exc), *ERROR_OUTPUT)
-
- output += '\n' # give ourselves an extra line before the next prompt
-
- return output
diff --git a/stem/interpretor/help.py b/stem/interpretor/help.py
deleted file mode 100644
index b7909a9..0000000
--- a/stem/interpretor/help.py
+++ /dev/null
@@ -1,142 +0,0 @@
-"""
-Provides our /help responses.
-"""
-
-from stem.interpretor import (
- STANDARD_OUTPUT,
- BOLD_OUTPUT,
- ERROR_OUTPUT,
- msg,
- uses_settings,
-)
-
-from stem.util.term import format
-
-try:
- # added in python 3.2
- from functools import lru_cache
-except ImportError:
- from stem.util.lru_cache import lru_cache
-
-
-def response(controller, arg):
- """
- Provides our /help response.
-
- :param stem.control.Controller controller: tor control connection
- :param str arg: controller or interpretor command to provide help output for
-
- :returns: **str** with our help response
- """
-
- # Normalizing inputs first so we can better cache responses.
-
- return _response(controller, _normalize(arg))
-
-
-def _normalize(arg):
- arg = arg.upper()
-
- # If there's multiple arguments then just take the first. This is
- # particularly likely if they're trying to query a full command (for
- # instance "/help GETINFO version")
-
- arg = arg.split(' ')[0]
-
- # strip slash if someone enters an interpretor command (ex. "/help /help")
-
- if arg.startswith('/'):
- arg = arg[1:]
-
- return arg
-
-
-@lru_cache()
-@uses_settings
-def _response(controller, arg, config):
- if not arg:
- return _general_help()
-
- usage_info = config.get('help.usage', {})
-
- if not arg in usage_info:
- return format("No help information available for '%s'..." % arg, *ERROR_OUTPUT)
-
- output = format(usage_info[arg] + '\n', *BOLD_OUTPUT)
-
- description = config.get('help.description.%s' % arg.lower(), '')
-
- for line in description.splitlines():
- output += format(' ' + line, *STANDARD_OUTPUT) + '\n'
-
- output += '\n'
-
- if arg == 'GETINFO':
- results = controller.get_info('info/names', None)
-
- if results:
- for line in results.splitlines():
- if ' -- ' in line:
- opt, summary = line.split(' -- ', 1)
-
- output += format('%-33s' % opt, *BOLD_OUTPUT)
- output += format(' - %s' % summary, *STANDARD_OUTPUT) + '\n'
- elif arg == 'GETCONF':
- results = controller.get_info('config/names', None)
-
- if results:
- options = [opt.split(' ', 1)[0] for opt in results.splitlines()]
-
- for i in range(0, len(options), 2):
- line = ''
-
- for entry in options[i:i + 2]:
- line += '%-42s' % entry
-
- output += format(line.rstrip(), *STANDARD_OUTPUT) + '\n'
- elif arg == 'SIGNAL':
- signal_options = config.get('help.signal.options', {})
-
- for signal, summary in signal_options.items():
- output += format('%-15s' % signal, *BOLD_OUTPUT)
- output += format(' - %s' % summary, *STANDARD_OUTPUT) + '\n'
- elif arg == 'SETEVENTS':
- results = controller.get_info('events/names', None)
-
- if results:
- entries = results.split()
-
- # displays four columns of 20 characters
-
- for i in range(0, len(entries), 4):
- line = ''
-
- for entry in entries[i:i + 4]:
- line += '%-20s' % entry
-
- output += format(line.rstrip(), *STANDARD_OUTPUT) + '\n'
- elif arg == 'USEFEATURE':
- results = controller.get_info('features/names', None)
-
- if results:
- output += format(results, *STANDARD_OUTPUT) + '\n'
- elif arg in ('LOADCONF', 'POSTDESCRIPTOR'):
- # gives a warning that this option isn't yet implemented
- output += format(msg('msg.multiline_unimplemented_notice'), *ERROR_OUTPUT) + '\n'
-
- return output.rstrip()
-
-
-def _general_help():
- lines = []
-
- for line in msg('help.general').splitlines():
- div = line.find(' - ')
-
- if div != -1:
- cmd, description = line[:div], line[div:]
- lines.append(format(cmd, *BOLD_OUTPUT) + format(description, *STANDARD_OUTPUT))
- else:
- lines.append(format(line, *BOLD_OUTPUT))
-
- return '\n'.join(lines)
diff --git a/stem/interpretor/settings.cfg b/stem/interpretor/settings.cfg
deleted file mode 100644
index 028188f..0000000
--- a/stem/interpretor/settings.cfg
+++ /dev/null
@@ -1,295 +0,0 @@
-################################################################################
-#
-# Configuration data used by Stem's interpretor prompt.
-#
-################################################################################
-
- ##################
-# GENERAL MESSAGES #
- ##################
-
-msg.multiline_unimplemented_notice Multi-line control options like this are not yet implemented.
-
-msg.help
-|Interactive interpretor for Tor. This provides you with direct access
-|to Tor's control interface via either python or direct requests.
-|
-| -i, --interface [ADDRESS:]PORT change control interface from {address}:{port}
-| -s, --socket SOCKET_PATH attach using unix domain socket if present,
-| SOCKET_PATH defaults to: {socket}
-| --no-color disables colorized output
-| -h, --help presents this help
-|
-
-msg.startup_banner
-|Welcome to Stem's interpretor prompt. This provides you with direct access to
-|Tor's control interface.
-|
-|This acts like a standard python interpretor with a Tor connection available
-|via your 'controller' variable...
-|
-| >>> controller.get_info('version')
-| '0.2.5.1-alpha-dev (git-245ecfff36c0cecc)'
-|
-|You can also issue requests directly to Tor...
-|
-| >>> GETINFO version
-| 250-version=0.2.5.1-alpha-dev (git-245ecfff36c0cecc)
-| 250 OK
-|
-|For more information run '/help'.
-|
-
-msg.tor_unavailable Tor isn't running and the command presently isn't in your PATH.
-
-msg.starting_tor
-|Tor isn't running. Starting a temporary Tor instance for our interpretor to
-|interact with. This will have a minimal non-relaying configuration, and be
-|shut down when you're done.
-|
-|--------------------------------------------------------------------------------
-|
-
- #################
-# OUTPUT OF /HELP #
- #################
-
-# Response for the '/help' command without any arguments.
-
-help.general
-|Interpretor commands include:
-| /help - provides information for interpretor and tor commands
-| /events - prints events that we've received
-| /info - general information for a relay
-| /python - enable or disable support for running python commands
-| /quit - shuts down the interpretor
-|
-|Tor commands include:
-| GETINFO - queries information from tor
-| GETCONF, SETCONF, RESETCONF - show or edit a configuration option
-| SIGNAL - issues control signal to the process (for resetting, stopping, etc)
-| SETEVENTS - configures the events tor will notify us of
-|
-| USEFEATURE - enables custom behavior for the controller
-| SAVECONF - writes tor's current configuration to our torrc
-| LOADCONF - loads the given input like it was part of our torrc
-| MAPADDRESS - replaces requests for one address with another
-| POSTDESCRIPTOR - adds a relay descriptor to our cache
-| EXTENDCIRCUIT - create or extend a tor circuit
-| SETCIRCUITPURPOSE - configures the purpose associated with a circuit
-| CLOSECIRCUIT - closes the given circuit
-| ATTACHSTREAM - associates an application's stream with a tor circuit
-| REDIRECTSTREAM - sets a stream's destination
-| CLOSESTREAM - closes the given stream
-| RESOLVE - issues an asynchronous dns or rdns request over tor
-| TAKEOWNERSHIP - instructs tor to quit when this control connection is closed
-| PROTOCOLINFO - queries version and controller authentication information
-| QUIT - disconnect the control connection
-|
-|For more information use '/help [OPTION]'.
-
-# Usage of tor and interpretor commands.
-
-help.usage HELP => /help [OPTION]
-help.usage EVENTS => /events [types]
-help.usage INFO => /info [relay fingerprint, nickname, or IP address]
-help.usage PYTHON => /python [enable,disable]
-help.usage QUIT => /quit
-help.usage GETINFO => GETINFO OPTION
-help.usage GETCONF => GETCONF OPTION
-help.usage SETCONF => SETCONF PARAM[=VALUE]
-help.usage RESETCONF => RESETCONF PARAM[=VALUE]
-help.usage SIGNAL => SIGNAL SIG
-help.usage SETEVENTS => SETEVENTS [EXTENDED] [EVENTS]
-help.usage USEFEATURE => USEFEATURE OPTION
-help.usage SAVECONF => SAVECONF
-help.usage LOADCONF => LOADCONF...
-help.usage MAPADDRESS => MAPADDRESS SOURCE_ADDR=DESTINATION_ADDR
-help.usage POSTDESCRIPTOR => POSTDESCRIPTOR [purpose=general/controller/bridge] [cache=yes/no]...
-help.usage EXTENDCIRCUIT => EXTENDCIRCUIT CircuitID [PATH] [purpose=general/controller]
-help.usage SETCIRCUITPURPOSE => SETCIRCUITPURPOSE CircuitID purpose=general/controller
-help.usage CLOSECIRCUIT => CLOSECIRCUIT CircuitID [IfUnused]
-help.usage ATTACHSTREAM => ATTACHSTREAM StreamID CircuitID [HOP=HopNum]
-help.usage REDIRECTSTREAM => REDIRECTSTREAM StreamID Address [Port]
-help.usage CLOSESTREAM => CLOSESTREAM StreamID Reason [Flag]
-help.usage RESOLVE => RESOLVE [mode=reverse] address
-help.usage TAKEOWNERSHIP => TAKEOWNERSHIP
-help.usage PROTOCOLINFO => PROTOCOLINFO [ProtocolVersion]
-
-# Longer description of what tor and interpretor commands do.
-
-help.description.help
-|Provides usage information for the given interpretor, tor command, or tor
-|configuration option.
-|
-|Example:
-| /help info # provides a description of the '/info' option
-| /help GETINFO # usage information for tor's GETINFO controller option
-
-help.description.events
-|Provides events that we've received belonging to the given event types. If
-|no types are specified then this provides all the messages that we've
-|received.
-
-help.description.info
-|Provides general information for a relay that's currently in the consensus.
-|If no relay is specified then this provides information on ourselves.
-
-help.description.python
-|Enables or disables support for running python commands. This determines how
-|we treat commands this interpretor doesn't recognize...
-|
-|* If enabled then unrecognized commands are executed as python.
-|* If disabled then unrecognized commands are passed along to tor.
-
-help.description.quit
-|Terminates the interpretor.
-
-help.description.getinfo
-|Queries the tor process for information. Options are...
-|
-
-help.description.getconf
-|Provides the current value for a given configuration value. Options include...
-|
-
-help.description.setconf
-|Sets the given configuration parameters. Values can be quoted or non-quoted
-|strings, and reverts the option to 0 or NULL if not provided.
-|
-|Examples:
-| * Sets a contact address and resets our family to NULL
-| SETCONF MyFamily ContactInfo=foo(a)bar.com
-|
-| * Sets an exit policy that only includes port 80/443
-| SETCONF ExitPolicy=\"accept *:80, accept *:443, reject *:*\"\
-
-help.description.resetconf
-|Reverts the given configuration options to their default values. If a value
-|is provided then this behaves in the same way as SETCONF.
-|
-|Examples:
-| * Returns both of our accounting parameters to their defaults
-| RESETCONF AccountingMax AccountingStart
-|
-| * Uses the default exit policy and sets our nickname to be 'Goomba'
-| RESETCONF ExitPolicy Nickname=Goomba
-
-help.description.signal
-|Issues a signal that tells the tor process to reload its torrc, dump its
-|stats, halt, etc.
-
-help.description.setevents
-|Sets the events that we will receive. This turns off any events that aren't
-|listed so sending 'SETEVENTS' without any values will turn off all event reporting.
-|
-|For Tor versions between 0.1.1.9 and 0.2.2.1 adding 'EXTENDED' causes some
-|events to give us additional information. After version 0.2.2.1 this is
-|always on.
-|
-|Events include...
-|
-
-help.description.usefeature
-|Customizes the behavior of the control port. Options include...
-|
-
-help.description.saveconf
-|Writes Tor's current configuration to its torrc.
-
-help.description.loadconf
-|Reads the given text like it belonged to our torrc.
-|
-|Example:
-| +LOADCONF
-| # sets our exit policy to just accept ports 80 and 443
-| ExitPolicy accept *:80
-| ExitPolicy accept *:443
-| ExitPolicy reject *:*
-| .
-
-help.description.mapaddress
-|Replaces future requests for one address with another.
-|
-|Example:
-| MAPADDRESS 0.0.0.0=torproject.org 1.2.3.4=tor.freehaven.net
-
-help.description.postdescriptor
-|Simulates getting a new relay descriptor.
-
-help.description.extendcircuit
-|Extends the given circuit or create a new one if the CircuitID is zero. The
-|PATH is a comma separated list of fingerprints. If it isn't set then this
-|uses Tor's normal path selection.
-
-help.description.setcircuitpurpose
-|Sets the purpose attribute for a circuit.
-
-help.description.closecircuit
-|Closes the given circuit. If "IfUnused" is included then this only closes
-|the circuit if it isn't currently being used.
-
-help.description.attachstream
-|Attaches a stream with the given built circuit (tor picks one on its own if
-|CircuitID is zero). If HopNum is given then this hop is used to exit the
-|circuit, otherwise the last relay is used.
-
-help.description.redirectstream
-|Sets the destination for a given stream. This can only be done after a
-|stream is created but before it's attached to a circuit.
-
-help.description.closestream
-|Closes the given stream, the reason being an integer matching a reason as
-|per section 6.3 of the tor-spec.
-
-help.description.resolve
-|Performs IPv4 DNS resolution over tor, doing a reverse lookup instead if
-|"mode=reverse" is included. This request is processed in the background and
-|results in a ADDRMAP event with the response.
-
-help.description.takeownership
-|Instructs Tor to gracefully shut down when this control connection is closed.
-
-help.description.protocolinfo
-|Provides bootstrapping information that a controller might need when first
-|starting, like Tor's version and controller authentication. This can be done
-|before authenticating to the control port.
-
-help.signal.options RELOAD / HUP => reload our torrc
-help.signal.options SHUTDOWN / INT => gracefully shut down, waiting 30 seconds if we're a relay
-help.signal.options DUMP / USR1 => logs information about open connections and circuits
-help.signal.options DEBUG / USR2 => makes us log at the DEBUG runlevel
-help.signal.options HALT / TERM => immediately shut down
-help.signal.options CLEARDNSCACHE => clears any cached DNS results
-help.signal.options NEWNYM => clears the DNS cache and uses new circuits for future connections
-
- ################
-# TAB COMPLETION #
- ################
-
-# Commands we'll autocomplete when the user hits tab. This is just the start of
-# our autocompletion list - more are determined dynamically by checking what
-# tor supports.
-
-autocomplete /help
-autocomplete /events
-autocomplete /info
-autocomplete /quit
-autocomplete SAVECONF
-autocomplete MAPADDRESS
-autocomplete EXTENDCIRCUIT
-autocomplete SETCIRCUITPURPOSE
-autocomplete SETROUTERPURPOSE
-autocomplete ATTACHSTREAM
-#autocomplete +POSTDESCRIPTOR # TODO: needs multi-line support
-autocomplete REDIRECTSTREAM
-autocomplete CLOSESTREAM
-autocomplete CLOSECIRCUIT
-autocomplete QUIT
-autocomplete RESOLVE
-autocomplete PROTOCOLINFO
-#autocomplete +LOADCONF # TODO: needs multi-line support
-autocomplete TAKEOWNERSHIP
-autocomplete AUTHCHALLENGE
-autocomplete DROPGUARDS
-
diff --git a/stem/util/system.py b/stem/util/system.py
index 7827eb6..c940578 100644
--- a/stem/util/system.py
+++ b/stem/util/system.py
@@ -910,7 +910,7 @@ def get_process_name():
#
# ' '.join(['python'] + sys.argv)
#
- # ... doesn't do the trick since this will miss interpretor arguments.
+ # ... doesn't do the trick since this will miss interpreter arguments.
#
# python -W ignore::DeprecationWarning my_script.py
diff --git a/test/settings.cfg b/test/settings.cfg
index 79852b0..2ef3736 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -183,10 +183,10 @@ test.unit_tests
|test.unit.connection.authentication.TestAuthenticate
|test.unit.connection.connect.TestConnect
|test.unit.control.controller.TestControl
-|test.unit.interpretor.arguments.TestArgumentParsing
-|test.unit.interpretor.autocomplete.TestAutocompletion
-|test.unit.interpretor.help.TestHelpResponses
-|test.unit.interpretor.commands.TestInterpretorCommands
+|test.unit.interpreter.arguments.TestArgumentParsing
+|test.unit.interpreter.autocomplete.TestAutocompletion
+|test.unit.interpreter.help.TestHelpResponses
+|test.unit.interpreter.commands.TestInterpretorCommands
|test.unit.doctest.TestDocumentation
test.integ_tests
diff --git a/test/unit/interpreter/__init__.py b/test/unit/interpreter/__init__.py
new file mode 100644
index 0000000..7c10768
--- /dev/null
+++ b/test/unit/interpreter/__init__.py
@@ -0,0 +1,39 @@
+"""
+Unit tests for the stem's interpreter prompt.
+"""
+
+__all__ = [
+ 'arguments',
+ 'autocomplete',
+ 'commands',
+ 'help',
+]
+
+try:
+ # added in python 3.3
+ from unittest.mock import Mock
+except ImportError:
+ from mock import Mock
+
+GETINFO_NAMES = """
+info/names -- List of GETINFO options, types, and documentation.
+ip-to-country/* -- Perform a GEOIP lookup
+md/id/* -- Microdescriptors by ID
+""".strip()
+
+GETCONF_NAMES = """
+ExitNodes RouterList
+ExitPolicy LineList
+ExitPolicyRejectPrivate Boolean
+""".strip()
+
+
+CONTROLLER = Mock()
+
+CONTROLLER.get_info.side_effect = lambda arg, _: {
+ 'info/names': GETINFO_NAMES,
+ 'config/names': GETCONF_NAMES,
+ 'events/names': 'BW DEBUG INFO NOTICE',
+ 'features/names': 'VERBOSE_NAMES EXTENDED_EVENTS',
+ 'signal/names': 'RELOAD HUP SHUTDOWN',
+}[arg]
diff --git a/test/unit/interpreter/arguments.py b/test/unit/interpreter/arguments.py
new file mode 100644
index 0000000..60fda3d
--- /dev/null
+++ b/test/unit/interpreter/arguments.py
@@ -0,0 +1,57 @@
+import unittest
+
+from stem.interpreter.arguments import DEFAULT_ARGS, parse, get_help
+
+
+class TestArgumentParsing(unittest.TestCase):
+ def test_that_we_get_default_values(self):
+ args = parse([])
+
+ for attr in DEFAULT_ARGS:
+ self.assertEqual(DEFAULT_ARGS[attr], getattr(args, attr))
+
+ def test_that_we_load_arguments(self):
+ args = parse(['--interface', '10.0.0.25:80'])
+ self.assertEqual('10.0.0.25', args.control_address)
+ self.assertEqual(80, args.control_port)
+
+ args = parse(['--interface', '80'])
+ self.assertEqual(DEFAULT_ARGS['control_address'], args.control_address)
+ self.assertEqual(80, args.control_port)
+
+ args = parse(['--socket', '/tmp/my_socket'])
+ self.assertEqual('/tmp/my_socket', args.control_socket)
+
+ args = parse(['--help'])
+ self.assertEqual(True, args.print_help)
+
+ def test_examples(self):
+ args = parse(['-i', '1643'])
+ self.assertEqual(1643, args.control_port)
+
+ args = parse(['-s', '~/.tor/socket'])
+ self.assertEqual('~/.tor/socket', args.control_socket)
+
+ def test_that_we_reject_unrecognized_arguments(self):
+ self.assertRaises(ValueError, parse, ['--blarg', 'stuff'])
+
+ def test_that_we_reject_invalid_interfaces(self):
+ invalid_inputs = (
+ '',
+ ' ',
+ 'blarg',
+ '127.0.0.1',
+ '127.0.0.1:',
+ ':80',
+ '400.0.0.1:80',
+ '127.0.0.1:-5',
+ '127.0.0.1:500000',
+ )
+
+ for invalid_input in invalid_inputs:
+ self.assertRaises(ValueError, parse, ['--interface', invalid_input])
+
+ def test_get_help(self):
+ help_text = get_help()
+ self.assertTrue('Interactive interpreter for Tor.' in help_text)
+ self.assertTrue('change control interface from 127.0.0.1:9051' in help_text)
diff --git a/test/unit/interpreter/autocomplete.py b/test/unit/interpreter/autocomplete.py
new file mode 100644
index 0000000..40bcab4
--- /dev/null
+++ b/test/unit/interpreter/autocomplete.py
@@ -0,0 +1,112 @@
+import unittest
+
+from stem.interpreter.autocomplete import _get_commands, Autocompleter
+
+from test.unit.interpreter import CONTROLLER
+
+try:
+ # added in python 3.3
+ from unittest.mock import Mock
+except ImportError:
+ from mock import Mock
+
+
+class TestAutocompletion(unittest.TestCase):
+ def test_autocomplete_results_from_config(self):
+ """
+ Check that we load autocompletion results from our configuration.
+ """
+
+ commands = _get_commands(None)
+ self.assertTrue('PROTOCOLINFO' in commands)
+ self.assertTrue('/quit' in commands)
+
+ def test_autocomplete_results_from_tor(self):
+ """
+ Check our ability to determine autocompletion results based on our tor
+ instance's capabilities.
+ """
+
+ # Check that when GETINFO requests fail we have base commands, but nothing
+ # with arguments.
+
+ controller = Mock()
+ controller.get_info.return_value = None
+ commands = _get_commands(controller)
+
+ self.assertTrue('GETINFO ' in commands)
+ self.assertTrue('GETCONF ' in commands)
+ self.assertTrue('SIGNAL ' in commands)
+
+ self.assertFalse('GETINFO info/names' in commands)
+ self.assertFalse('GETCONF ExitPolicy' in commands)
+ self.assertFalse('SIGNAL SHUTDOWN' in commands)
+
+ # Now check where we should be able to determine tor's capabilities.
+
+ commands = _get_commands(CONTROLLER)
+
+ expected = (
+ 'GETINFO info/names',
+ 'GETINFO ip-to-country/',
+ 'GETINFO md/id/',
+
+ 'GETCONF ExitNodes',
+ 'GETCONF ExitPolicy',
+ 'SETCONF ExitPolicy',
+ 'RESETCONF ExitPolicy',
+
+ 'SETEVENTS BW',
+ 'SETEVENTS INFO',
+ 'USEFEATURE VERBOSE_NAMES',
+ 'USEFEATURE EXTENDED_EVENTS',
+ 'SIGNAL RELOAD',
+ 'SIGNAL SHUTDOWN',
+ )
+
+ for result in expected:
+ self.assertTrue(result in commands)
+
+ # We shouldn't include the base commands since we have results with
+ # their arguments.
+
+ self.assertFalse('GETINFO ' in commands)
+ self.assertFalse('GETCONF ' in commands)
+ self.assertFalse('SIGNAL ' in commands)
+
+ def test_autocompleter_match(self):
+ """
+ Exercise our Autocompleter's match method.
+ """
+
+ autocompleter = Autocompleter(None)
+
+ self.assertEqual(['/help'], autocompleter.matches('/help'))
+ self.assertEqual(['/help'], autocompleter.matches('/hel'))
+ self.assertEqual(['/help'], autocompleter.matches('/he'))
+ self.assertEqual(['/help'], autocompleter.matches('/h'))
+ self.assertEqual(['/help', '/events', '/info', '/quit'], autocompleter.matches('/'))
+
+ # check case sensitivity
+
+ self.assertEqual(['/help'], autocompleter.matches('/HELP'))
+ self.assertEqual(['/help'], autocompleter.matches('/HeLp'))
+
+ # check when we shouldn't have any matches
+
+ self.assertEqual([], autocompleter.matches('blarg'))
+
+ def test_autocompleter_complete(self):
+ """
+ Exercise our Autocompleter's complete method.
+ """
+
+ autocompleter = Autocompleter(None)
+
+ self.assertEqual('/help', autocompleter.complete('/', 0))
+ self.assertEqual('/events', autocompleter.complete('/', 1))
+ self.assertEqual('/info', autocompleter.complete('/', 2))
+ self.assertEqual('/quit', autocompleter.complete('/', 3))
+ self.assertEqual(None, autocompleter.complete('/', 4))
+
+ self.assertEqual(None, autocompleter.complete('blarg', 0))
diff --git a/test/unit/interpreter/commands.py b/test/unit/interpreter/commands.py
new file mode 100644
index 0000000..9afab59
--- /dev/null
+++ b/test/unit/interpreter/commands.py
@@ -0,0 +1,198 @@
+import datetime
+import unittest
+
+import stem
+import stem.response
+import stem.version
+
+from stem.interpreter.commands import ControlInterpretor, _get_fingerprint
+
+from test import mocking
+from test.unit.interpreter import CONTROLLER
+
+try:
+ # added in python 3.3
+ from unittest.mock import Mock
+except ImportError:
+ from mock import Mock
+
+EXPECTED_EVENTS_RESPONSE = """\
+\x1b[34mBW 15 25\x1b[0m
+\x1b[34mBW 758 570\x1b[0m
+\x1b[34mDEBUG connection_edge_process_relay_cell(): Got an extended cell! Yay.\x1b[0m
+"""
+
+EXPECTED_INFO_RESPONSE = """\
+moria1 (9695DFC35FFEB861329B9F1AB04C46397020CE31)
+\x1b[34;1maddress: \x1b[0m128.31.0.34:9101 (us)
+\x1b[34;1mpublished: \x1b[0m05:52:05 05/05/2014
+\x1b[34;1mos: \x1b[0mLinux
+\x1b[34;1mversion: \x1b[0m0.2.5.3-alpha-dev
+\x1b[34;1mflags: \x1b[0mAuthority, Fast, Guard, HSDir, Named, Running, Stable, V2Dir, Valid
+\x1b[34;1mexit policy: \x1b[0mreject 1-65535
+\x1b[34;1mcontact: \x1b[0m1024D/28988BF5 arma mit edu
+"""
+
+EXPECTED_GETCONF_RESPONSE = """\
+\x1b[34;1mlog\x1b[0m\x1b[34m => notice stdout\x1b[0m
+\x1b[34;1maddress\x1b[0m\x1b[34m => \x1b[0m
+
+"""
+
+FINGERPRINT = '9695DFC35FFEB861329B9F1AB04C46397020CE31'
+
+
+class TestInterpretorCommands(unittest.TestCase):
+ def test_get_fingerprint_for_ourselves(self):
+ controller = Mock()
+
+ controller.get_info.side_effect = lambda arg: {
+ 'fingerprint': FINGERPRINT,
+ }[arg]
+
+ self.assertEqual(FINGERPRINT, _get_fingerprint('', controller))
+
+ controller.get_info.side_effect = stem.ControllerError
+ self.assertRaises(ValueError, _get_fingerprint, '', controller)
+
+ def test_get_fingerprint_for_fingerprint(self):
+ self.assertEqual(FINGERPRINT, _get_fingerprint(FINGERPRINT, Mock()))
+
+ def test_get_fingerprint_for_nickname(self):
+ controller, descriptor = Mock(), Mock()
+ descriptor.fingerprint = FINGERPRINT
+
+ controller.get_network_status.side_effect = lambda arg: {
+ 'moria1': descriptor,
+ }[arg]
+
+ self.assertEqual(FINGERPRINT, _get_fingerprint('moria1', controller))
+
+ controller.get_network_status.side_effect = stem.ControllerError
+ self.assertRaises(ValueError, _get_fingerprint, 'moria1', controller)
+
+ def test_get_fingerprint_for_address(self):
+ controller = Mock()
+
+ self.assertRaises(ValueError, _get_fingerprint, '127.0.0.1:-1', controller)
+ self.assertRaises(ValueError, _get_fingerprint, '127.0.0.901:80', controller)
+
+ descriptor = Mock()
+ descriptor.address = '127.0.0.1'
+ descriptor.or_port = 80
+ descriptor.fingerprint = FINGERPRINT
+
+ controller.get_network_statuses.return_value = [descriptor]
+
+ self.assertEqual(FINGERPRINT, _get_fingerprint('127.0.0.1', controller))
+ self.assertEqual(FINGERPRINT, _get_fingerprint('127.0.0.1:80', controller))
+ self.assertRaises(ValueError, _get_fingerprint, '127.0.0.1:81', controller)
+ self.assertRaises(ValueError, _get_fingerprint, '127.0.0.2', controller)
+
+ def test_get_fingerprint_for_unrecognized_inputs(self):
+ self.assertRaises(ValueError, _get_fingerprint, 'blarg!', Mock())
+
+ def test_when_disconnected(self):
+ controller = Mock()
+ controller.is_alive.return_value = False
+
+ interpreter = ControlInterpretor(controller)
+ self.assertRaises(stem.SocketClosed, interpreter.run_command, '/help')
+
+ def test_quit(self):
+ interpreter = ControlInterpretor(CONTROLLER)
+ self.assertRaises(stem.SocketClosed, interpreter.run_command, '/quit')
+ self.assertRaises(stem.SocketClosed, interpreter.run_command, 'QUIT')
+
+ def test_help(self):
+ interpreter = ControlInterpretor(CONTROLLER)
+
+ self.assertTrue('Interpretor commands include:' in interpreter.run_command('/help'))
+ self.assertTrue('Queries the tor process for information.' in interpreter.run_command('/help GETINFO'))
+ self.assertTrue('Queries the tor process for information.' in interpreter.run_command('/help GETINFO version'))
+
+ def test_events(self):
+ interpreter = ControlInterpretor(CONTROLLER)
+
+ # no received events
+
+ self.assertEqual('\n', interpreter.run_command('/events'))
+
+ # with enqueued events
+
+ event_contents = (
+ '650 BW 15 25',
+ '650 BW 758 570',
+ '650 DEBUG connection_edge_process_relay_cell(): Got an extended cell! Yay.',
+ )
+
+ for content in event_contents:
+ event = mocking.get_message(content)
+ stem.response.convert('EVENT', event)
+ interpreter._received_events.append(event)
+
+ self.assertEqual(EXPECTED_EVENTS_RESPONSE, interpreter.run_command('/events'))
+
+ def test_info(self):
+ controller, server_desc, ns_desc = Mock(), Mock(), Mock()
+
+ controller.get_microdescriptor.return_value = None
+ controller.get_server_descriptor.return_value = server_desc
+ controller.get_network_status.return_value = ns_desc
+
+ controller.get_info.side_effect = lambda arg, _: {
+ 'ip-to-country/128.31.0.34': 'us',
+ }[arg]
+
+ ns_desc.address = '128.31.0.34'
+ ns_desc.or_port = 9101
+ ns_desc.published = datetime.datetime(2014, 5, 5, 5, 52, 5)
+ ns_desc.nickname = 'moria1'
+ ns_desc.flags = ['Authority', 'Fast', 'Guard', 'HSDir', 'Named', 'Running', 'Stable', 'V2Dir', 'Valid']
+
+ server_desc.exit_policy.summary.return_value = 'reject 1-65535'
+ server_desc.platform = 'Linux'
+ server_desc.tor_version = stem.version.Version('0.2.5.3-alpha-dev')
+ server_desc.contact = '1024D/28988BF5 arma mit edu'
+
+ interpreter = ControlInterpretor(controller)
+ self.assertEqual(EXPECTED_INFO_RESPONSE, interpreter.run_command('/info ' + FINGERPRINT))
+
+ def test_unrecognized_interpreter_command(self):
+ interpreter = ControlInterpretor(CONTROLLER)
+
+ expected = "\x1b[1;31m'/unrecognized' isn't a recognized command\x1b[0m\n"
+ self.assertEqual(expected, interpreter.run_command('/unrecognized'))
+
+ def test_getinfo(self):
+ response = '250-version=0.2.5.1-alpha-dev (git-245ecfff36c0cecc)\r\n250 OK'
+
+ controller = Mock()
+ controller.msg.return_value = mocking.get_message(response)
+
+ interpreter = ControlInterpretor(controller)
+
+ self.assertEqual('\x1b[34m%s\x1b[0m\n' % response, interpreter.run_command('GETINFO version'))
+ controller.msg.assert_called_with('GETINFO version')
+
+ controller.msg.side_effect = stem.ControllerError('kaboom!')
+ self.assertEqual('\x1b[1;31mkaboom!\x1b[0m\n', interpreter.run_command('getinfo process/user'))
+
+ def test_getconf(self):
+ response = '250-Log=notice stdout\r\n250 Address'
+
+ controller = Mock()
+ controller.msg.return_value = mocking.get_message(response)
+
+ interpreter = ControlInterpretor(controller)
+
+ self.assertEqual('\x1b[34m%s\x1b[0m\n' % response, interpreter.run_command('GETCONF log address'))
+ controller.msg.assert_called_with('GETCONF log address')
+
+ def test_setevents(self):
+ controller = Mock()
+ controller.msg.return_value = mocking.get_message('250 OK')
+
+ interpreter = ControlInterpretor(controller)
+
+ self.assertEqual('\x1b[34m250 OK\x1b[0m\n', interpreter.run_command('SETEVENTS BW'))
diff --git a/test/unit/interpreter/help.py b/test/unit/interpreter/help.py
new file mode 100644
index 0000000..49df746
--- /dev/null
+++ b/test/unit/interpreter/help.py
@@ -0,0 +1,54 @@
+import unittest
+
+from stem.interpreter.help import response, _normalize
+
+from test.unit.interpreter import CONTROLLER
+
+
+class TestHelpResponses(unittest.TestCase):
+ def test_normalization(self):
+ self.assertEqual('', _normalize(''))
+ self.assertEqual('', _normalize(' '))
+
+ self.assertEqual('GETINFO', _normalize('GETINFO'))
+ self.assertEqual('GETINFO', _normalize('GetInfo'))
+ self.assertEqual('GETINFO', _normalize('getinfo'))
+ self.assertEqual('GETINFO', _normalize('GETINFO version'))
+ self.assertEqual('GETINFO', _normalize('GETINFO '))
+
+ self.assertEqual('INFO', _normalize('/info'))
+ self.assertEqual('INFO', _normalize('/info caerSidi'))
+
+ def test_unrecognized_option(self):
+ result = response(CONTROLLER, 'FOOBAR')
+ self.assertEqual("\x1b[1;31mNo help information available for 'FOOBAR'...\x1b[0m", result)
+
+ def test_general_help(self):
+ result = response(CONTROLLER, '')
+ self.assertTrue('Interpretor commands include:' in result)
+ self.assertTrue('\x1b[34;1m GETINFO\x1b[0m\x1b[34m - queries information from tor\x1b[0m\n' in result)
+
+ def test_getinfo_help(self):
+ result = response(CONTROLLER, 'GETINFO')
+ self.assertTrue('Queries the tor process for information. Options are...' in result)
+ self.assertTrue('\x1b[34;1minfo/names \x1b[0m\x1b[34m - List of GETINFO options, types, and documentation.' in result)
+
+ def test_getconf_help(self):
+ result = response(CONTROLLER, 'GETCONF')
+ self.assertTrue('Provides the current value for a given configuration value. Options include...' in result)
+ self.assertTrue('\x1b[34mExitNodes ExitPolicy' in result)
+
+ def test_signal_help(self):
+ result = response(CONTROLLER, 'SIGNAL')
+ self.assertTrue('Issues a signal that tells the tor process to' in result)
+ self.assertTrue('\x1b[34;1mRELOAD / HUP \x1b[0m\x1b[34m - reload our torrc' in result)
+
+ def test_setevents_help(self):
+ result = response(CONTROLLER, 'SETEVENTS')
+ self.assertTrue('Sets the events that we will receive.' in result)
+ self.assertTrue('\x1b[34mBW DEBUG INFO NOTICE\x1b[0m' in result)
+
+ def test_usefeature_help(self):
+ result = response(CONTROLLER, 'USEFEATURE')
+ self.assertTrue('Customizes the behavior of the control port.' in result)
+ self.assertTrue('\x1b[34mVERBOSE_NAMES EXTENDED_EVENTS\x1b[0m' in result)
diff --git a/test/unit/interpretor/__init__.py b/test/unit/interpretor/__init__.py
deleted file mode 100644
index 705734d..0000000
--- a/test/unit/interpretor/__init__.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""
-Unit tests for the stem's interpretor prompt.
-"""
-
-__all__ = [
- 'arguments',
- 'autocomplete',
- 'commands',
- 'help',
-]
-
-try:
- # added in python 3.3
- from unittest.mock import Mock
-except ImportError:
- from mock import Mock
-
-GETINFO_NAMES = """
-info/names -- List of GETINFO options, types, and documentation.
-ip-to-country/* -- Perform a GEOIP lookup
-md/id/* -- Microdescriptors by ID
-""".strip()
-
-GETCONF_NAMES = """
-ExitNodes RouterList
-ExitPolicy LineList
-ExitPolicyRejectPrivate Boolean
-""".strip()
-
-
-CONTROLLER = Mock()
-
-CONTROLLER.get_info.side_effect = lambda arg, _: {
- 'info/names': GETINFO_NAMES,
- 'config/names': GETCONF_NAMES,
- 'events/names': 'BW DEBUG INFO NOTICE',
- 'features/names': 'VERBOSE_NAMES EXTENDED_EVENTS',
- 'signal/names': 'RELOAD HUP SHUTDOWN',
-}[arg]
diff --git a/test/unit/interpretor/arguments.py b/test/unit/interpretor/arguments.py
deleted file mode 100644
index ab835c4..0000000
--- a/test/unit/interpretor/arguments.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import unittest
-
-from stem.interpretor.arguments import DEFAULT_ARGS, parse, get_help
-
-
-class TestArgumentParsing(unittest.TestCase):
- def test_that_we_get_default_values(self):
- args = parse([])
-
- for attr in DEFAULT_ARGS:
- self.assertEqual(DEFAULT_ARGS[attr], getattr(args, attr))
-
- def test_that_we_load_arguments(self):
- args = parse(['--interface', '10.0.0.25:80'])
- self.assertEqual('10.0.0.25', args.control_address)
- self.assertEqual(80, args.control_port)
-
- args = parse(['--interface', '80'])
- self.assertEqual(DEFAULT_ARGS['control_address'], args.control_address)
- self.assertEqual(80, args.control_port)
-
- args = parse(['--socket', '/tmp/my_socket'])
- self.assertEqual('/tmp/my_socket', args.control_socket)
-
- args = parse(['--help'])
- self.assertEqual(True, args.print_help)
-
- def test_examples(self):
- args = parse(['-i', '1643'])
- self.assertEqual(1643, args.control_port)
-
- args = parse(['-s', '~/.tor/socket'])
- self.assertEqual('~/.tor/socket', args.control_socket)
-
- def test_that_we_reject_unrecognized_arguments(self):
- self.assertRaises(ValueError, parse, ['--blarg', 'stuff'])
-
- def test_that_we_reject_invalid_interfaces(self):
- invalid_inputs = (
- '',
- ' ',
- 'blarg',
- '127.0.0.1',
- '127.0.0.1:',
- ':80',
- '400.0.0.1:80',
- '127.0.0.1:-5',
- '127.0.0.1:500000',
- )
-
- for invalid_input in invalid_inputs:
- self.assertRaises(ValueError, parse, ['--interface', invalid_input])
-
- def test_get_help(self):
- help_text = get_help()
- self.assertTrue('Interactive interpretor for Tor.' in help_text)
- self.assertTrue('change control interface from 127.0.0.1:9051' in help_text)
diff --git a/test/unit/interpretor/autocomplete.py b/test/unit/interpretor/autocomplete.py
deleted file mode 100644
index 6541da3..0000000
--- a/test/unit/interpretor/autocomplete.py
+++ /dev/null
@@ -1,112 +0,0 @@
-import unittest
-
-from stem.interpretor.autocomplete import _get_commands, Autocompleter
-
-from test.unit.interpretor import CONTROLLER
-
-try:
- # added in python 3.3
- from unittest.mock import Mock
-except ImportError:
- from mock import Mock
-
-
-class TestAutocompletion(unittest.TestCase):
- def test_autocomplete_results_from_config(self):
- """
- Check that we load autocompletion results from our configuration.
- """
-
- commands = _get_commands(None)
- self.assertTrue('PROTOCOLINFO' in commands)
- self.assertTrue('/quit' in commands)
-
- def test_autocomplete_results_from_tor(self):
- """
- Check our ability to determine autocompletion results based on our tor
- instance's capabilities.
- """
-
- # Check that when GETINFO requests fail we have base commands, but nothing
- # with arguments.
-
- controller = Mock()
- controller.get_info.return_value = None
- commands = _get_commands(controller)
-
- self.assertTrue('GETINFO ' in commands)
- self.assertTrue('GETCONF ' in commands)
- self.assertTrue('SIGNAL ' in commands)
-
- self.assertFalse('GETINFO info/names' in commands)
- self.assertFalse('GETCONF ExitPolicy' in commands)
- self.assertFalse('SIGNAL SHUTDOWN' in commands)
-
- # Now check where we should be able to determine tor's capabilities.
-
- commands = _get_commands(CONTROLLER)
-
- expected = (
- 'GETINFO info/names',
- 'GETINFO ip-to-country/',
- 'GETINFO md/id/',
-
- 'GETCONF ExitNodes',
- 'GETCONF ExitPolicy',
- 'SETCONF ExitPolicy',
- 'RESETCONF ExitPolicy',
-
- 'SETEVENTS BW',
- 'SETEVENTS INFO',
- 'USEFEATURE VERBOSE_NAMES',
- 'USEFEATURE EXTENDED_EVENTS',
- 'SIGNAL RELOAD',
- 'SIGNAL SHUTDOWN',
- )
-
- for result in expected:
- self.assertTrue(result in commands)
-
- # We shouldn't include the base commands since we have results with
- # their arguments.
-
- self.assertFalse('GETINFO ' in commands)
- self.assertFalse('GETCONF ' in commands)
- self.assertFalse('SIGNAL ' in commands)
-
- def test_autocompleter_match(self):
- """
- Exercise our Autocompleter's match method.
- """
-
- autocompleter = Autocompleter(None)
-
- self.assertEqual(['/help'], autocompleter.matches('/help'))
- self.assertEqual(['/help'], autocompleter.matches('/hel'))
- self.assertEqual(['/help'], autocompleter.matches('/he'))
- self.assertEqual(['/help'], autocompleter.matches('/h'))
- self.assertEqual(['/help', '/events', '/info', '/quit'], autocompleter.matches('/'))
-
- # check case sensitivity
-
- self.assertEqual(['/help'], autocompleter.matches('/HELP'))
- self.assertEqual(['/help'], autocompleter.matches('/HeLp'))
-
- # check when we shouldn't have any matches
-
- self.assertEqual([], autocompleter.matches('blarg'))
-
- def test_autocompleter_complete(self):
- """
- Exercise our Autocompleter's complete method.
- """
-
- autocompleter = Autocompleter(None)
-
- self.assertEqual('/help', autocompleter.complete('/', 0))
- self.assertEqual('/events', autocompleter.complete('/', 1))
- self.assertEqual('/info', autocompleter.complete('/', 2))
- self.assertEqual('/quit', autocompleter.complete('/', 3))
- self.assertEqual(None, autocompleter.complete('/', 4))
-
- self.assertEqual(None, autocompleter.complete('blarg', 0))
diff --git a/test/unit/interpretor/commands.py b/test/unit/interpretor/commands.py
deleted file mode 100644
index dfdbddb..0000000
--- a/test/unit/interpretor/commands.py
+++ /dev/null
@@ -1,198 +0,0 @@
-import datetime
-import unittest
-
-import stem
-import stem.response
-import stem.version
-
-from stem.interpretor.commands import ControlInterpretor, _get_fingerprint
-
-from test import mocking
-from test.unit.interpretor import CONTROLLER
-
-try:
- # added in python 3.3
- from unittest.mock import Mock
-except ImportError:
- from mock import Mock
-
-EXPECTED_EVENTS_RESPONSE = """\
-\x1b[34mBW 15 25\x1b[0m
-\x1b[34mBW 758 570\x1b[0m
-\x1b[34mDEBUG connection_edge_process_relay_cell(): Got an extended cell! Yay.\x1b[0m
-"""
-
-EXPECTED_INFO_RESPONSE = """\
-moria1 (9695DFC35FFEB861329B9F1AB04C46397020CE31)
-\x1b[34;1maddress: \x1b[0m128.31.0.34:9101 (us)
-\x1b[34;1mpublished: \x1b[0m05:52:05 05/05/2014
-\x1b[34;1mos: \x1b[0mLinux
-\x1b[34;1mversion: \x1b[0m0.2.5.3-alpha-dev
-\x1b[34;1mflags: \x1b[0mAuthority, Fast, Guard, HSDir, Named, Running, Stable, V2Dir, Valid
-\x1b[34;1mexit policy: \x1b[0mreject 1-65535
-\x1b[34;1mcontact: \x1b[0m1024D/28988BF5 arma mit edu
-"""
-
-EXPECTED_GETCONF_RESPONSE = """\
-\x1b[34;1mlog\x1b[0m\x1b[34m => notice stdout\x1b[0m
-\x1b[34;1maddress\x1b[0m\x1b[34m => \x1b[0m
-
-"""
-
-FINGERPRINT = '9695DFC35FFEB861329B9F1AB04C46397020CE31'
-
-
-class TestInterpretorCommands(unittest.TestCase):
- def test_get_fingerprint_for_ourselves(self):
- controller = Mock()
-
- controller.get_info.side_effect = lambda arg: {
- 'fingerprint': FINGERPRINT,
- }[arg]
-
- self.assertEqual(FINGERPRINT, _get_fingerprint('', controller))
-
- controller.get_info.side_effect = stem.ControllerError
- self.assertRaises(ValueError, _get_fingerprint, '', controller)
-
- def test_get_fingerprint_for_fingerprint(self):
- self.assertEqual(FINGERPRINT, _get_fingerprint(FINGERPRINT, Mock()))
-
- def test_get_fingerprint_for_nickname(self):
- controller, descriptor = Mock(), Mock()
- descriptor.fingerprint = FINGERPRINT
-
- controller.get_network_status.side_effect = lambda arg: {
- 'moria1': descriptor,
- }[arg]
-
- self.assertEqual(FINGERPRINT, _get_fingerprint('moria1', controller))
-
- controller.get_network_status.side_effect = stem.ControllerError
- self.assertRaises(ValueError, _get_fingerprint, 'moria1', controller)
-
- def test_get_fingerprint_for_address(self):
- controller = Mock()
-
- self.assertRaises(ValueError, _get_fingerprint, '127.0.0.1:-1', controller)
- self.assertRaises(ValueError, _get_fingerprint, '127.0.0.901:80', controller)
-
- descriptor = Mock()
- descriptor.address = '127.0.0.1'
- descriptor.or_port = 80
- descriptor.fingerprint = FINGERPRINT
-
- controller.get_network_statuses.return_value = [descriptor]
-
- self.assertEqual(FINGERPRINT, _get_fingerprint('127.0.0.1', controller))
- self.assertEqual(FINGERPRINT, _get_fingerprint('127.0.0.1:80', controller))
- self.assertRaises(ValueError, _get_fingerprint, '127.0.0.1:81', controller)
- self.assertRaises(ValueError, _get_fingerprint, '127.0.0.2', controller)
-
- def test_get_fingerprint_for_unrecognized_inputs(self):
- self.assertRaises(ValueError, _get_fingerprint, 'blarg!', Mock())
-
- def test_when_disconnected(self):
- controller = Mock()
- controller.is_alive.return_value = False
-
- interpretor = ControlInterpretor(controller)
- self.assertRaises(stem.SocketClosed, interpretor.run_command, '/help')
-
- def test_quit(self):
- interpretor = ControlInterpretor(CONTROLLER)
- self.assertRaises(stem.SocketClosed, interpretor.run_command, '/quit')
- self.assertRaises(stem.SocketClosed, interpretor.run_command, 'QUIT')
-
- def test_help(self):
- interpretor = ControlInterpretor(CONTROLLER)
-
- self.assertTrue('Interpretor commands include:' in interpretor.run_command('/help'))
- self.assertTrue('Queries the tor process for information.' in interpretor.run_command('/help GETINFO'))
- self.assertTrue('Queries the tor process for information.' in interpretor.run_command('/help GETINFO version'))
-
- def test_events(self):
- interpretor = ControlInterpretor(CONTROLLER)
-
- # no received events
-
- self.assertEqual('\n', interpretor.run_command('/events'))
-
- # with enqueued events
-
- event_contents = (
- '650 BW 15 25',
- '650 BW 758 570',
- '650 DEBUG connection_edge_process_relay_cell(): Got an extended cell! Yay.',
- )
-
- for content in event_contents:
- event = mocking.get_message(content)
- stem.response.convert('EVENT', event)
- interpretor._received_events.append(event)
-
- self.assertEqual(EXPECTED_EVENTS_RESPONSE, interpretor.run_command('/events'))
-
- def test_info(self):
- controller, server_desc, ns_desc = Mock(), Mock(), Mock()
-
- controller.get_microdescriptor.return_value = None
- controller.get_server_descriptor.return_value = server_desc
- controller.get_network_status.return_value = ns_desc
-
- controller.get_info.side_effect = lambda arg, _: {
- 'ip-to-country/128.31.0.34': 'us',
- }[arg]
-
- ns_desc.address = '128.31.0.34'
- ns_desc.or_port = 9101
- ns_desc.published = datetime.datetime(2014, 5, 5, 5, 52, 5)
- ns_desc.nickname = 'moria1'
- ns_desc.flags = ['Authority', 'Fast', 'Guard', 'HSDir', 'Named', 'Running', 'Stable', 'V2Dir', 'Valid']
-
- server_desc.exit_policy.summary.return_value = 'reject 1-65535'
- server_desc.platform = 'Linux'
- server_desc.tor_version = stem.version.Version('0.2.5.3-alpha-dev')
- server_desc.contact = '1024D/28988BF5 arma mit edu'
-
- interpretor = ControlInterpretor(controller)
- self.assertEqual(EXPECTED_INFO_RESPONSE, interpretor.run_command('/info ' + FINGERPRINT))
-
- def test_unrecognized_interpretor_command(self):
- interpretor = ControlInterpretor(CONTROLLER)
-
- expected = "\x1b[1;31m'/unrecognized' isn't a recognized command\x1b[0m\n"
- self.assertEqual(expected, interpretor.run_command('/unrecognized'))
-
- def test_getinfo(self):
- response = '250-version=0.2.5.1-alpha-dev (git-245ecfff36c0cecc)\r\n250 OK'
-
- controller = Mock()
- controller.msg.return_value = mocking.get_message(response)
-
- interpretor = ControlInterpretor(controller)
-
- self.assertEqual('\x1b[34m%s\x1b[0m\n' % response, interpretor.run_command('GETINFO version'))
- controller.msg.assert_called_with('GETINFO version')
-
- controller.msg.side_effect = stem.ControllerError('kaboom!')
- self.assertEqual('\x1b[1;31mkaboom!\x1b[0m\n', interpretor.run_command('getinfo process/user'))
-
- def test_getconf(self):
- response = '250-Log=notice stdout\r\n250 Address'
-
- controller = Mock()
- controller.msg.return_value = mocking.get_message(response)
-
- interpretor = ControlInterpretor(controller)
-
- self.assertEqual('\x1b[34m%s\x1b[0m\n' % response, interpretor.run_command('GETCONF log address'))
- controller.msg.assert_called_with('GETCONF log address')
-
- def test_setevents(self):
- controller = Mock()
- controller.msg.return_value = mocking.get_message('250 OK')
-
- interpretor = ControlInterpretor(controller)
-
- self.assertEqual('\x1b[34m250 OK\x1b[0m\n', interpretor.run_command('SETEVENTS BW'))
diff --git a/test/unit/interpretor/help.py b/test/unit/interpretor/help.py
deleted file mode 100644
index e656060..0000000
--- a/test/unit/interpretor/help.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import unittest
-
-from stem.interpretor.help import response, _normalize
-
-from test.unit.interpretor import CONTROLLER
-
-
-class TestHelpResponses(unittest.TestCase):
- def test_normalization(self):
- self.assertEqual('', _normalize(''))
- self.assertEqual('', _normalize(' '))
-
- self.assertEqual('GETINFO', _normalize('GETINFO'))
- self.assertEqual('GETINFO', _normalize('GetInfo'))
- self.assertEqual('GETINFO', _normalize('getinfo'))
- self.assertEqual('GETINFO', _normalize('GETINFO version'))
- self.assertEqual('GETINFO', _normalize('GETINFO '))
-
- self.assertEqual('INFO', _normalize('/info'))
- self.assertEqual('INFO', _normalize('/info caerSidi'))
-
- def test_unrecognized_option(self):
- result = response(CONTROLLER, 'FOOBAR')
- self.assertEqual("\x1b[1;31mNo help information available for 'FOOBAR'...\x1b[0m", result)
-
- def test_general_help(self):
- result = response(CONTROLLER, '')
- self.assertTrue('Interpretor commands include:' in result)
- self.assertTrue('\x1b[34;1m GETINFO\x1b[0m\x1b[34m - queries information from tor\x1b[0m\n' in result)
-
- def test_getinfo_help(self):
- result = response(CONTROLLER, 'GETINFO')
- self.assertTrue('Queries the tor process for information. Options are...' in result)
- self.assertTrue('\x1b[34;1minfo/names \x1b[0m\x1b[34m - List of GETINFO options, types, and documentation.' in result)
-
- def test_getconf_help(self):
- result = response(CONTROLLER, 'GETCONF')
- self.assertTrue('Provides the current value for a given configuration value. Options include...' in result)
- self.assertTrue('\x1b[34mExitNodes ExitPolicy' in result)
-
- def test_signal_help(self):
- result = response(CONTROLLER, 'SIGNAL')
- self.assertTrue('Issues a signal that tells the tor process to' in result)
- self.assertTrue('\x1b[34;1mRELOAD / HUP \x1b[0m\x1b[34m - reload our torrc' in result)
-
- def test_setevents_help(self):
- result = response(CONTROLLER, 'SETEVENTS')
- self.assertTrue('Sets the events that we will receive.' in result)
- self.assertTrue('\x1b[34mBW DEBUG INFO NOTICE\x1b[0m' in result)
-
- def test_usefeature_help(self):
- result = response(CONTROLLER, 'USEFEATURE')
- self.assertTrue('Customizes the behavior of the control port.' in result)
- self.assertTrue('\x1b[34mVERBOSE_NAMES EXTENDED_EVENTS\x1b[0m' in result)
diff --git a/tor-prompt b/tor-prompt
index fc115a3..0f3755b 100755
--- a/tor-prompt
+++ b/tor-prompt
@@ -2,7 +2,7 @@
# Copyright 2014, Damian Johnson and The Tor Project
# See LICENSE for licensing information
-import stem.interpretor
+import stem.interpreter
if __name__ == '__main__':
- stem.interpretor.main()
+ stem.interpreter.main()
1
0
commit ffc1f706920fac1dec61c1aedf19028bac0057f5
Author: Damian Johnson <atagar(a)torproject.org>
Date: Thu May 22 09:04:58 2014 -0700
Renaming 'prompt' to 'tor-prompt'
Renaming our prompt to be less generic so we can add it to bin on installation.
I'm not in love with this name, but at least it's pretty self-explanatory.
---
prompt | 8 --------
setup.py | 26 +++++++++++++++-----------
tor-prompt | 8 ++++++++
3 files changed, 23 insertions(+), 19 deletions(-)
diff --git a/prompt b/prompt
deleted file mode 100755
index fc115a3..0000000
--- a/prompt
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env python
-# Copyright 2014, Damian Johnson and The Tor Project
-# See LICENSE for licensing information
-
-import stem.interpretor
-
-if __name__ == '__main__':
- stem.interpretor.main()
diff --git a/setup.py b/setup.py
index 0aa8b00..47eb478 100644
--- a/setup.py
+++ b/setup.py
@@ -41,16 +41,20 @@ try:
except ImportError:
from distutils.command.build_py import build_py
-setup(name = 'stem',
- version = module_info['version'],
- description = DESCRIPTION,
- license = module_info['license'],
- author = module_info['author'],
- author_email = module_info['contact'],
- url = module_info['url'],
- packages = ['stem', 'stem.descriptor', 'stem.response', 'stem.util'],
- provides = ['stem'],
- cmdclass = {'build_py': build_py},
- keywords = "tor onion controller",
+setup(
+ name = 'stem',
+ version = module_info['version'],
+ description = DESCRIPTION,
+ license = module_info['license'],
+ author = module_info['author'],
+ author_email = module_info['contact'],
+ url = module_info['url'],
+ packages = ['stem', 'stem.descriptor', 'stem.interpretor', 'stem.response', 'stem.util'],
+ provides = ['stem'],
+ cmdclass = {'build_py': build_py},
+ keywords = 'tor onion controller',
+ data_files = [
+ ('/usr/bin', ['tor-prompt'])
+ ],
)
diff --git a/tor-prompt b/tor-prompt
new file mode 100755
index 0000000..fc115a3
--- /dev/null
+++ b/tor-prompt
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+# Copyright 2014, Damian Johnson and The Tor Project
+# See LICENSE for licensing information
+
+import stem.interpretor
+
+if __name__ == '__main__':
+ stem.interpretor.main()
1
0