├── LICENSE ├── README.md ├── onionexpose ├── config.py ├── display.py ├── displayutil.py ├── entrypoint.py ├── fileserver.py ├── infopage_file.html ├── infopage_status.html ├── main.py ├── qrutil.py ├── statusserver.py └── torutil.py ├── requirements.txt └── screenshots ├── screenshot1.png └── screenshot2.png /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Ethan White 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introducing `onion-expose` 2 | 3 | `onion-expose` is a utility that allows one to easily create and control temporary Tor onion services. 4 | 5 | `onion-expose` can be used for any sort of TCP traffic, from simple HTTP to Internet radio to Minecraft to SSH servers. 6 | It can also be used to expose individual files and allow you to request them from another computer. 7 | 8 | # Why not just use `ngrok`? 9 | 10 | `ngrok` is nice. But it requires everything to go through a central authority (a potential security issue), and imposes 11 | artificial restrictions, such as a limit of one TCP tunnel per user. It also doesn't allow you to expose files easily 12 | (you have to set it up yourself). 13 | 14 | # Screenshots 15 | 16 | ![A screenshot of onion-expose exposing the file /usr/share/dict/words.](screenshots/screenshot1.png) 17 | ![A screenshot of onion-expose exposing port 8080.](screenshots/screenshot2.png) 18 | 19 | # Getting started 20 | 21 | For now, there's no setup.py script (working on it...) 22 | 23 | ``` 24 | $ pip install -r requirements.txt 25 | $ cd onionexpose 26 | $ python main.py 8080 27 | ``` 28 | 29 | This will create a Tor onion service tunneling port 80 of the onion service to port 8080 locally. 30 | 31 | ``` 32 | $ python main.py /usr/share/dict/words 33 | ``` 34 | 35 | This will create a Tor onion service tunneling port 80 of the onion service to a simple HTTP server responding with 36 | the content of `/usr/share/dict/words` on a request to the root. 37 | 38 | ``` 39 | $ python main.py 8080 --remote-port 8080 40 | ``` 41 | 42 | This tunnels port 8080 of the onion service to port 8080 locally. Note that this also works for the file server. 43 | 44 | If you want to be able to connect to an onion service, but your application doesn't support SOCKS5 proxying, you can 45 | use netcat to tunnel a port on localhost to that onion service: 46 | 47 | ``` 48 | $ ncat -c "ncat --proxy-type socks5 --proxy 127.0.0.1:9050 .onion " -l 1234 49 | ``` 50 | 51 | # Security properties 52 | 53 | In theory, `onion-expose` tunnels _should_ have the same security properties as regular Tor onion services. Note that 54 | this explicitly _doesn't include confidentiality of files that are exposed via the file server_. If you need confidentiality 55 | for the files you're exposing, use `openssl enc` to encrypt it before you expose it. 56 | 57 | However, do note that `onion-expose` comes with absolutely no warranty. 58 | 59 | # Compatibility 60 | 61 | `onion-expose` supports _only Python 3_. It will not run on Python 2. It has been tested on Debian 8 "Jessie" with 62 | Python 3.4 and Tor 0.2.7.6. It _should_ run on Windows, OS X, or (Open|Free)BSD, but I haven't tested it. 63 | -------------------------------------------------------------------------------- /onionexpose/config.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Ethan White 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | import yaml 27 | import getpass 28 | import os.path 29 | import socket 30 | 31 | defaults = { 32 | "expose_port": -1, 33 | "status_port": -1, 34 | "port_start": 17320, 35 | "tor_proxy_port": 9050, 36 | "tor_controller_password": None, 37 | "debug": False 38 | } 39 | 40 | class Config: 41 | def __init__(self, path=os.path.expanduser("~/.onion-expose.yml")): 42 | self.path = path 43 | self.pairs = None 44 | 45 | def _convert_name(self, yamlname): 46 | return yamlname.replace("-", "_").lower() 47 | 48 | def load(self): 49 | cfg = None 50 | if os.path.isfile(self.path): 51 | with open(self.path) as f: 52 | # TODO: What if we've got malformed YAML? 53 | try: 54 | cfg = yaml.load(f.read()) # NOTE: Arbitrary code execution. You've been warned. 55 | except: 56 | pass 57 | 58 | if cfg is None: 59 | cfg = {} 60 | 61 | for key in cfg: 62 | if self._convert_name(key) == key: 63 | continue 64 | cfg[self._convert_name(key)] = cfg[key] 65 | del cfg[key] 66 | 67 | self.pairs = {} 68 | for key in defaults: 69 | self.pairs[key] = cfg.get(key, defaults[key]) 70 | 71 | if self.tor_controller_password == None: 72 | self._prompt_tor_password() 73 | 74 | assert type(self.port_start) is int 75 | assert type(self.tor_proxy_port) is int 76 | assert type(self.tor_controller_password) is str 77 | 78 | self._set_ports() 79 | 80 | assert type(self.expose_port) is int 81 | assert type(self.status_port) is int 82 | 83 | def _prompt_tor_password(self): 84 | print("I didn't find an entry for 'Tor-Controller-Password' your YAML config file at\n(~/.onion-expose.yml).") 85 | self.pairs["tor_controller_password"] = getpass.getpass("Enter your Tor controller password (or Ctrl-C and create a config file): ") 86 | 87 | def _try_port(self, port): 88 | try: 89 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 90 | sock.settimeout(0.3) 91 | sock.connect(("localhost", port)) 92 | sock.close() 93 | return True 94 | except: 95 | return False 96 | 97 | def _set_ports(self): 98 | print("Finding free ports, hold on...") 99 | # TODO: We could do a binary search or something here, but I really don't think it's necessary. 100 | current_port = self.port_start 101 | while self._try_port(current_port): 102 | current_port += 2 103 | 104 | self.pairs["start_port"] = current_port 105 | self.pairs["status_port"] = current_port 106 | self.pairs["expose_port"] = current_port + 1 107 | 108 | def __getattr__(self, key): 109 | if self.pairs is None: 110 | raise ValueError("Using config before initialization") 111 | if key in self.pairs: 112 | return self.pairs[key] 113 | else: 114 | raise AttributeError(key) 115 | 116 | config = Config() 117 | -------------------------------------------------------------------------------- /onionexpose/display.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Ethan White 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from __future__ import division 27 | import curses 28 | import time 29 | import math 30 | import signal 31 | import qrutil 32 | import displayutil as util 33 | from collections import OrderedDict 34 | 35 | def draw_title(addr, localport, remoteport, window): 36 | center_text = util.center_pad_text("Tunneling \xb2%s\xb2 to \xb2%s\xb2" % ("%s:%s" % (addr, remoteport), "localhost:" + str(localport)), window.width) 37 | window.draw_fancy_text(center_text, 0, 0, curses.color_pair(1)) 38 | 39 | def draw_kv(kv, window): 40 | max_len = max([len(k) for k in kv]) 41 | new_kv = OrderedDict({}) 42 | for key in kv: 43 | new_key = "%s%s" % (key, " " * (max_len - len(key))) 44 | new_kv[new_key] = kv[key] 45 | i = 0 46 | for key in new_kv: 47 | window.draw_fancy_text("%s \xb2%s\xb2" % (key, new_kv[key]), i + 2, 1) 48 | i += 1 49 | 50 | 51 | def draw_qr(data, window): 52 | try: 53 | # TODO: How bad is this for performance? 54 | lines = qrutil.QRWrapper(data).compact_repr().split("\n") 55 | # TODO: Having `i` floating here randomly doesn't look good. 56 | i = 0 57 | for line in lines: 58 | y = window.height - len(lines) + i 59 | x = window.width - len(line) 60 | window.draw_text(line, x, y) 61 | i += 1 62 | except curses.error: 63 | # TODO: Whyyyyyyyyyyyyyyy 64 | pass 65 | 66 | def draw(tunnel, window): 67 | window.update_size() 68 | window.window.clear() 69 | draw_title(tunnel.addr, tunnel.localport, tunnel.remoteport, window) 70 | kv = OrderedDict() 71 | if tunnel.is_file_tunnel: 72 | kv["Exposed file"] = tunnel.exposed_file 73 | else: 74 | kv["Local port"] = str(tunnel.localport) 75 | kv["Remote host"] = "%s:%s" % (tunnel.addr, tunnel.remoteport) 76 | kv["Remote HTTP URL"] = "http://%s:%s" % (tunnel.addr, tunnel.remoteport) 77 | kv["Status"] = { 78 | "startup": "Startup (unavailable)", 79 | "running": "Available", 80 | "down": "Error (unavailable)" 81 | }[tunnel.status] 82 | kv["Uptime"] = "%s second(s)" % math.floor(tunnel.uptime) if tunnel.uptime < 60 else \ 83 | "%s minute(s)" % math.floor(tunnel.uptime / 60) if tunnel.uptime < 60*60 else \ 84 | "%s hour(s)" % math.floor(tunnel.uptime / (60*60)) if tunnel.uptime < 60*60*24 else \ 85 | "%s day(s)" % math.floor(tunnel.uptime / (60*60*24)) 86 | draw_kv(kv, window) 87 | draw_qr("%s:%s" % (tunnel.addr, tunnel.remoteport), window) 88 | window.refresh() 89 | 90 | def display_tunnel(tunnel): 91 | window = util.WindowWrapper() 92 | window.init() 93 | def redraw(arg1=None, arg2=None): 94 | draw(tunnel, window) 95 | redraw() 96 | try: 97 | while True: 98 | time.sleep(1) 99 | redraw() 100 | except KeyboardInterrupt: 101 | window.teardown() 102 | print("Thanks for using onion-expose!") 103 | 104 | if __name__ == "__main__": 105 | starttime = time.time() 106 | class DummyTunnel: 107 | def __init__(self): 108 | self.addr = "facebookcorewwwi.onion" 109 | self.localport = 8004 110 | self.remoteport = 80 111 | self.first_active = 0 112 | def get_uptime(self): 113 | return time.time() - starttime 114 | uptime = property(get_uptime) 115 | def __getattr__(self, key): 116 | if key == "active": 117 | return time.time() - starttime > 5 118 | raise AttributeError 119 | window = util.WindowWrapper() 120 | tunnel = DummyTunnel() 121 | window.init() 122 | def redraw(arg1=None, arg2=None): 123 | draw(tunnel, window) 124 | redraw() 125 | try: 126 | while True: 127 | # This is actually only one of two update loops. 128 | # The other redraws information about the hidden service; 129 | # it's much more periodic. 130 | time.sleep(1) 131 | redraw() 132 | except KeyboardInterrupt: 133 | window.teardown() 134 | print("Thanks for using onion-expose!") 135 | -------------------------------------------------------------------------------- /onionexpose/displayutil.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Ethan White 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | import curses 27 | import math 28 | 29 | def center_pad_text(text, desired_len): 30 | # See display_fancy_text(...) for explanation pertaining to "\xb2". 31 | len_needed = desired_len - len(text.replace("\xb2", "")) 32 | if len_needed <= 0: 33 | return text 34 | leftpad = math.floor(len_needed / 2) 35 | rightpad = math.ceil(len_needed / 2) 36 | return " " * leftpad + text + " " * rightpad 37 | 38 | class WindowWrapper(): 39 | def __init__(self): 40 | pass 41 | 42 | def init(self): 43 | self.window = curses.initscr() 44 | curses.start_color() 45 | curses.noecho() 46 | curses.cbreak() 47 | curses.curs_set(0) 48 | curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE) 49 | self.window.keypad(1) 50 | self.window.erase() 51 | 52 | def teardown(self): 53 | self.window.erase() 54 | curses.nocbreak() 55 | curses.echo() 56 | curses.endwin() 57 | curses.curs_set(1) 58 | 59 | def update_size(self): 60 | self.height, self.width = self.window.getmaxyx() 61 | 62 | def draw_text(self, text, x, y, attr=0): 63 | self.window.addstr(y, x, text, attr) 64 | 65 | def draw_fancy_text(self, text, y, x, attr=0): 66 | """ 67 | Displays text. A unicode `Superscript Two` (U+00B2) toggles bold 68 | formatting; a unicode `Superscript Three` (U+00B3) toggles 69 | color inversion. The bold and inversion formatting flags are 70 | ORed with the `attr` parameter. 71 | """ 72 | # TODO: Allow formatting other than bold (possibly using other exotic Unicode characters). 73 | pos = 0 74 | for i in range(len(text)): 75 | if text[i] == "\xb2": 76 | attr ^= curses.A_BOLD 77 | elif text[i] == "\xb3": 78 | attr ^= curses.A_REVERSE 79 | else: 80 | self.window.addch(y, x + pos, text[i], attr) 81 | pos += 1 82 | 83 | def refresh(self): 84 | self.window.refresh() 85 | -------------------------------------------------------------------------------- /onionexpose/entrypoint.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Ethan White 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | 27 | # This file is simply to provide a main function for setuptools. 28 | 29 | def main(): 30 | import main 31 | -------------------------------------------------------------------------------- /onionexpose/fileserver.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Ethan White 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from flask import Flask, send_file, request, abort 27 | import threading 28 | import requests 29 | from config import config 30 | import os 31 | import binascii 32 | 33 | class FileServerRunThread(threading.Thread): 34 | def __init__(self, app, port): 35 | threading.Thread.__init__(self) 36 | self.app = app 37 | self.port = port 38 | 39 | def run(self): 40 | self.app.run(port=self.port) 41 | 42 | class FileServer: 43 | def __init__(self, path, port=config.expose_port): 44 | self.path = path 45 | self.port = port 46 | self.app = Flask(__name__) 47 | self.nonce = binascii.hexlify(os.urandom(32)).decode("utf8") 48 | 49 | @self.app.route("/") 50 | def serve_file_wrapper(): 51 | return self.serve_file() 52 | 53 | @self.app.route("/shutdown") 54 | def serve_shutdown_wrapper(): 55 | return self.serve_shutdown() 56 | 57 | @self.app.route("/") 58 | def serve_info_wrapper(path): 59 | return self.serve_info(path) 60 | 61 | def run(self): 62 | self.run_thread = FileServerRunThread(self.app, self.port) 63 | self.run_thread.start() 64 | 65 | def serve_file(self): 66 | return send_file(self.path) 67 | 68 | def serve_info(self, path): 69 | return send_file("infopage_file.html") 70 | 71 | def serve_shutdown(self): 72 | if request.args.get("nonce") != self.nonce: 73 | abort(403, "Need the correct nonce.") 74 | request.environ.get("werkzeug.server.shutdown")() 75 | return "Doing shutdown." 76 | 77 | def teardown(self): 78 | if config.debug: 79 | print("Doing teardown request") 80 | requests.get("http://localhost:%s/shutdown" % self.port, params=dict(nonce=self.nonce)) 81 | if config.debug: 82 | print("Done teardown request") 83 | -------------------------------------------------------------------------------- /onionexpose/infopage_file.html: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | 30 | 31 | 46 | 47 | 48 |

This is an onion-expose tunnel.

49 | onion-expose is a program that allows one to easily create .onion services. It exposes 50 | files and ports. 51 |

File server

52 | This port of this .onion service is the file server. Request the root to get the file. Any other 53 | path will return this page. 54 |

About onion-expose

55 | Onion-expose is written by Ethan White, and licensed under 3-clause BSD. 56 | 57 | 58 | -------------------------------------------------------------------------------- /onionexpose/infopage_status.html: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | 30 | 31 | 46 | 47 | 48 |

This is an onion-expose tunnel.

49 | onion-expose is a program that allows one to easily create .onion services. It exposes 50 | files and ports. 51 |

Status server

52 | This server exposes a simple status page in order to allow the GUI to display a service status by pinging this 53 | port of the service periodically. 54 |

About onion-expose

55 | onion-expose is written by Ethan White, and licensed under 3-clause BSD. 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /onionexpose/main.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Ethan White 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from config import config 27 | config.load() 28 | import display 29 | import time 30 | import torutil 31 | import argparse 32 | import sys 33 | import os.path 34 | import fileserver 35 | import logging 36 | import io 37 | 38 | 39 | # Disable logging 40 | logging.basicConfig(level=logging.CRITICAL + 500, stream=io.StringIO()) 41 | 42 | parser = argparse.ArgumentParser(description="Easily expose ports and files to the public through Tor onion services.") 43 | parser.add_argument("localitem", help="Either (a) a local port number to expose, or (b) a file to expose") 44 | parser.add_argument("--remote-port", metavar="remote-port", help="The remote port that the service will expose", type=int, default=80) 45 | 46 | args = parser.parse_args(sys.argv[1:]) 47 | 48 | localport = -1 49 | remoteport = args.remote_port 50 | expose_file = False 51 | exposed_file = None 52 | file_server = None 53 | 54 | if os.path.exists(os.path.realpath(args.localitem)): 55 | if not os.path.isfile(os.path.realpath(args.localitem)): 56 | print("onion-expose: error: first argument does exist on the filesystem, but isn't a file. Directories aren't supported yet.") 57 | sys.exit(-1) 58 | expose_file = True 59 | exposed_file = os.path.realpath(args.localitem) 60 | localport = config.expose_port 61 | else: 62 | try: 63 | localport = int(args.localitem) 64 | except ValueError: 65 | print("onion-expose: error: first argument doesn't exist on the filesystem, and isn't a number.") 66 | sys.exit(-1) 67 | if not 0 < localport < 65536: 68 | print("onion-expose: error: first argument was a number, but wasn't a valid port") 69 | sys.exit(-1) 70 | 71 | torutil.setup() 72 | tunnel = None 73 | 74 | if expose_file: 75 | file_server = fileserver.FileServer(exposed_file) 76 | file_server.run() 77 | tunnel = torutil.Tunnel(localport, remoteport, exposed_file=exposed_file) 78 | else: 79 | tunnel = torutil.Tunnel(localport, remoteport) 80 | 81 | tunnel.create() 82 | if not config.debug: 83 | display.display_tunnel(tunnel) 84 | else: 85 | while True: 86 | try: 87 | time.sleep(1) 88 | except KeyboardInterrupt: 89 | print("Ctrl-C detected, exiting the loop...") 90 | break 91 | print(tunnel.addr, tunnel.remoteport, tunnel.localport, tunnel.status, tunnel.uptime) 92 | 93 | if file_server is not None: 94 | file_server.teardown() 95 | 96 | if config.debug: 97 | print("Doing tunnel teardown") 98 | tunnel.teardown() 99 | if config.debug: 100 | print("Tunnel torn down") 101 | print("Doing torutil teardown") 102 | torutil.teardown() 103 | if config.debug: 104 | print("Torn down torutil") 105 | -------------------------------------------------------------------------------- /onionexpose/qrutil.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Ethan White 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | import pyqrcode 27 | import math 28 | 29 | """ 30 | This file aims to turn a QR code produced by pyqrcode's `text()` method 31 | into something similar to the output of `qrencode -t utf8`, thus 32 | allowing it to take up half the space in each direction and fit on an 33 | 80x24 terminal. 34 | """ 35 | 36 | class QRMatrix: 37 | def __init__(self, lines): 38 | self.lines = lines 39 | 40 | def __getitem__(self, param): 41 | if type(param) is not tuple: 42 | raise ValueError("Expected tuple") 43 | x, y = param 44 | try: 45 | return self.lines[x][y] == "1" 46 | except IndexError: 47 | return False 48 | 49 | def get_width(self): 50 | return len(self.lines[0]) 51 | 52 | def get_height(self): 53 | return len(self.lines) 54 | 55 | def get_size(self): 56 | return get_width(), get_height() 57 | 58 | width = property(get_width) 59 | height = property(get_height) 60 | size = property(get_size) 61 | 62 | class QRWrapper: 63 | def __init__(self, data): 64 | self.matrix = QRMatrix(pyqrcode.create(data, error="L").text().split("\n")) 65 | 66 | def _get_display_char(self, top, bottom): 67 | if top and bottom: 68 | return " " 69 | elif not top and bottom: 70 | return "\u2580" 71 | elif not bottom and top: 72 | return "\u2584" 73 | elif not bottom and not top: 74 | return "\u2588" 75 | 76 | def compact_repr(self): 77 | lines = [] 78 | for i in range(math.floor(self.matrix.height / 2)): 79 | line = "" 80 | for j in range(self.matrix.width): 81 | line += self._get_display_char(self.matrix[j, i * 2], self.matrix[j, i * 2 + 1]) 82 | lines += [line] 83 | return "\n".join(lines) 84 | 85 | 86 | if __name__ == "__main__": 87 | print(QRWrapper("Just for debugging!").compact_repr()) 88 | -------------------------------------------------------------------------------- /onionexpose/statusserver.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Ethan White 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | import threading 27 | import requests 28 | import os 29 | import binascii 30 | from flask import Flask, send_file, request, abort 31 | from config import config 32 | 33 | # TODO: We've got a lot of code duplication in here from fileserver.py. Perhaps merge the two, or build some fancy OO something. 34 | # Note to self: ncat -c "ncat --proxy-type socks5 --proxy 127.0.0.1:9050 sx57pqdgpz3x7frb.onion 80" -l 1234 35 | 36 | class StatusServerRunThread(threading.Thread): 37 | def __init__(self, app, port): 38 | threading.Thread.__init__(self) 39 | self.app = app 40 | self.port = port 41 | 42 | def run(self): 43 | self.app.run(port=self.port) 44 | 45 | class StatusServer: 46 | def __init__(self, port=config.status_port): 47 | self.port = port 48 | self.app = Flask(__name__) 49 | self.nonce = binascii.hexlify(os.urandom(32)).decode("utf8") 50 | if config.debug: 51 | print("Status server", self.port, self.nonce) 52 | 53 | @self.app.route("/shutdown") 54 | def serve_shutdown_wrapper(): 55 | return self.serve_shutdown() 56 | 57 | @self.app.route("/") 58 | @self.app.route("/") 59 | def serve_info_wrapper(path=""): 60 | return self.serve_info(path) 61 | 62 | def run(self): 63 | self.run_thread = StatusServerRunThread(self.app, self.port) 64 | self.run_thread.start() 65 | 66 | def serve_info(self, path): 67 | return send_file("infopage_status.html") 68 | 69 | def serve_shutdown(self): 70 | if config.debug: 71 | print("Got a shutdown request for statusserver", request.args.get("nonce")) 72 | if request.args.get("nonce") != self.nonce: 73 | if config.debug: 74 | print("Invalid nonce") 75 | abort(403, "Need the correct nonce.") 76 | request.environ.get("werkzeug.server.shutdown")() 77 | return "Doing shutdown." 78 | 79 | def teardown(self): 80 | if config.debug: 81 | print("Sending a shutdown request to statusserver") 82 | requests.get("http://localhost:%s/shutdown" % self.port, params=dict(nonce=self.nonce)) 83 | -------------------------------------------------------------------------------- /onionexpose/torutil.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016, Ethan White 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | import stem 27 | import display 28 | import time 29 | import statusserver 30 | import pycurl 31 | import io 32 | import threading 33 | import stem.control 34 | from config import config 35 | 36 | controller = None 37 | 38 | def setup(): 39 | global controller 40 | controller = stem.control.Controller.from_port() 41 | controller.authenticate(password=config.tor_controller_password) 42 | 43 | class TunnelStatusPollingThread(threading.Thread): 44 | def __init__(self, tunnel): 45 | threading.Thread.__init__(self) 46 | self.stopped = False 47 | self.in_contact = False 48 | self.tunnel = tunnel 49 | 50 | def run(self): 51 | while not self.stopped: 52 | if self.in_contact: 53 | for i in range(20): 54 | time.sleep(1) 55 | if self.stopped: 56 | return 57 | else: 58 | time.sleep(2) 59 | if self.stopped: 60 | return 61 | self.in_contact = self.tunnel.update_status() 62 | 63 | class Tunnel: 64 | def __init__(self, localport, remoteport, exposed_file=None): 65 | self.localport = localport 66 | self.remoteport = remoteport 67 | self.killed = False 68 | self.exposed_file = exposed_file 69 | self.is_file_tunnel = self.exposed_file is not None 70 | self.status_server = statusserver.StatusServer(config.status_port) 71 | self.polling_thread = TunnelStatusPollingThread(self) 72 | 73 | def create(self): 74 | self.service = controller.create_ephemeral_hidden_service({ 75 | self.remoteport: self.localport, 76 | self.status_server.port: self.status_server.port 77 | }) 78 | self.addr = "%s.onion" % self.service.service_id 79 | self.start_time = time.time() 80 | self.status_server.run() 81 | self.polling_thread.start() 82 | self.status = "startup" 83 | 84 | def teardown(self): 85 | controller.remove_ephemeral_hidden_service(self.service.service_id) 86 | self.status_server.teardown() 87 | self.polling_thread.stopped = True 88 | self.killed = True 89 | 90 | def _check_status(self): 91 | """ 92 | For internal use only. Only members of :class:`Tunnel` 93 | """ 94 | try: 95 | output = io.BytesIO() 96 | 97 | query = pycurl.Curl() 98 | query.setopt(pycurl.URL, "%s:%s" % (self.addr, self.status_server.port)) 99 | query.setopt(pycurl.PROXY, "127.0.0.1") 100 | query.setopt(pycurl.PROXYPORT, config.tor_proxy_port) 101 | query.setopt(pycurl.PROXYTYPE, pycurl.PROXYTYPE_SOCKS5_HOSTNAME) 102 | query.setopt(pycurl.WRITEFUNCTION, output.write) 103 | query.perform() 104 | 105 | return "ONIONGROK_CLIENT_STATUS_SUCCESS" in output.getvalue().decode("utf8") 106 | except Exception as e: 107 | return False 108 | 109 | def update_status(self): 110 | is_alive = self._check_status() 111 | 112 | if self.status in ["startup", "down"] and is_alive: 113 | self.status = "running" 114 | 115 | if self.status == "running" and not is_alive: 116 | self.status = "down" 117 | 118 | return is_alive 119 | 120 | def get_uptime(self): 121 | return time.time() - self.start_time 122 | 123 | uptime = property(get_uptime) 124 | 125 | def teardown(): 126 | controller.close() 127 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.11.1 2 | itsdangerous==0.24 3 | Jinja2==2.8 4 | MarkupSafe==0.23 5 | pycurl==7.43.0 6 | PyQRCode==1.2.1 7 | PyYAML==3.11 8 | requests==2.10.0 9 | stem==1.4.0 10 | Werkzeug==0.11.10 11 | -------------------------------------------------------------------------------- /screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethan2-0/onion-expose/6cc20ce0215b632badd7f392d83700d2326a5c39/screenshots/screenshot1.png -------------------------------------------------------------------------------- /screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethan2-0/onion-expose/6cc20ce0215b632badd7f392d83700d2326a5c39/screenshots/screenshot2.png --------------------------------------------------------------------------------