Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Startup timer #572

Closed
wants to merge 4 commits into from
Closed

Startup timer #572

wants to merge 4 commits into from

Conversation

Labels
None yet
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

2 participants
@mig5
Copy link
Collaborator

@mig5 mig5 commented Jan 26, 2018

I've implemented a startup timer (fixes #571) in a similar fashion to the shutdown timer.

This one delays the start of a share (at the point that we try to negotiate ADD_ONION in the controller) for the length of time specified.

I have modified the UI so that both timer options are hidden under a 'Schedule this share?' checkbox. This keeps the UI from getting too cluttered I think.

onionshare_startup_timer_grouping_option

You can combine the startup and shutdown timers if you wish, or use them independently.

Since the user might change their mind about the scheduled start-up, I allow them to click the server button to 'cancel' the startup before it occurs.

onionshare_startup_timer_working

After the start-up occurs, the share proceeds as normal

onionshare_startup_timer_ready

The CLI also works (--startup-timer 120 for example) - note that the time does get slightly skewed because the 'sleep' starts after the Tor connection itself is made, and the share starts after the ADD_ONION is negotiated, but it's not a big deal IMO.

@mig5
Copy link
Collaborator Author

@mig5 mig5 commented Jan 26, 2018

I've simplified the UI even more by removing the extraneous 'Start/stop the share at:' labels - instead the checkbox becomes the label, and the date/time setting becomes 'editable' if the checkbox is checked.

I think this is a big improvement in preventing these options from cluttering the UI.

(Remember that in the screenshot below, Start at/Stop at is only shown if the 'Schedule this share?' is checked, otherwise they are hidden entirely)

onionshare_grouping_simplified

Note also that the 'start time' gets set to a few minutes into the future, and can't be set older than the current time. If both the start and stop timers are set, then the default/proposed 'shutdown time' is set to be further in time than the start time, and can't be set earlier than the start time.

Damn you Travis for failing for no reason to do with this branch!

@maqp
Copy link

@maqp maqp commented Jan 26, 2018

The clutter-free UI looks good. However the URL is apparently only visible to user after it's published. Regarding the dead man's switch, what I would like to have is the following:

OnionShare pre-generates the ephemeral RSA key pair and derives the onion URL and slug in the background at start, or it fetches the persistent ones if setting for that is enabled.

If I set timer for start, the URL/slug and "Copy URL" button would appear below the Scheduled to start... click to cancel button immediately after I press Start Sharing (should that button change to Start Sharing with Timer or Start Timer when the start timer checkbox is checked?). This way I have the URL for hidden service even before it's running. To clear any confusion about why the URL is not yet online and displaying something like "nothing here yet", above the URL it could have a countdown timer that says something like

The following URL will be public in 04 days, 05 hours, 10 minutes, 02 seconds

There's a problem however. If static URL is not checked, it's not possible to pre-fetch onion-URL using controller.create_ephemeral_hidden_service with await_publication=False since Stem will generate different key and URL every time. However, it's possible to pre-generate the RSA key pair with cryptography library just before launching the timer. Here's a standalone PoC I made based on OnionShare's code

#!/usr/bin/env python3.5
# -*- coding: utf-8 -*-

# sudo apt install python3-pip tor
# pip install cryptography stem

import base64
import hashlib
import os
import random
import shlex
import socket
import subprocess
import tempfile
import time
import typing

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa

from stem.control import Controller

if typing.TYPE_CHECKING:
    from tempfile import TemporaryDirectory


def get_available_port(min_port: int, max_port: int) -> str:
    with socket.socket() as tmpsock:
        while True:
            try:
                tmpsock.bind(('127.0.0.1', random.randint(min_port, max_port)))
                break
            except OSError:
                pass
        _, port = tmpsock.getsockname()
    return port


class Tor(object):

    def __init__(self) -> None:
        self.tor_process        = None  # type: subprocess.Popen
        self.controller         = None  # type: Controller
        self.tor_data_directory = None  # type: TemporaryDirectory

    def connect(self, port: str) -> None:
        self.tor_data_directory = tempfile.TemporaryDirectory()

        torrc_data = """\
        DataDirectory {{data_directory}}
        SocksPort {{socks_port}}
        ControlSocket {{control_socket}}
        CookieAuthentication 1
        CookieAuthFile {{cookie_auth_file}}
        AvoidDiskWrites 1
        Log notice stdout
        GeoIPFile /usr/share/tor/geoip
        GeoIPv6File /usr/share/tor/geoip6
        """

        tor_control_socket   = os.path.join(self.tor_data_directory.name, 'control_socket')
        tor_cookie_auth_file = os.path.join(self.tor_data_directory.name, 'cookie')
        tor_torrc            = os.path.join(self.tor_data_directory.name, 'torrc')

        torrc_data = torrc_data.replace('{{data_directory}}',   self.tor_data_directory.name)
        torrc_data = torrc_data.replace('{{socks_port}}',       str(port))
        torrc_data = torrc_data.replace('{{control_socket}}',   str(tor_control_socket))
        torrc_data = torrc_data.replace('{{cookie_auth_file}}', tor_cookie_auth_file)

        with open(tor_torrc, 'w') as f:
            f.write(torrc_data)

        start_ts = time.monotonic()
        self.tor_process = subprocess.Popen(['/usr/bin/tor', '-f', tor_torrc],
                                             stdout=subprocess.PIPE,
                                             stderr=subprocess.PIPE)
        time.sleep(2)
        self.controller = Controller.from_socket_file(path=tor_control_socket)
        self.controller.authenticate()

        while True:
            time.sleep(0.1)
            response = self.controller.get_info("status/bootstrap-phase")
            res_parts = shlex.split(response)
            progress = res_parts[2].split('=')[1]
            summary = res_parts[4].split('=')[1]
            print("{}: {}% - {}{}".format('connecting_to_tor', progress, summary, "\033[K"), end="\r")

            if summary == 'Done':
                print()
                break
            if time.monotonic() - start_ts > 45:
                start_ts = time.monotonic()
                self.controller = Controller.from_socket_file(path=tor_control_socket)
                self.controller.authenticate()


    def stop(self) -> None:
        if self.tor_process:
            self.tor_process.terminate()
            time.sleep(0.2)
            if not self.tor_process.poll():
                self.tor_process.kill()


def main() -> None:

    # Generate Onion Service key pair
    private_key = rsa.generate_private_key(public_exponent=65537,
                                           key_size=1024,
                                           backend=default_backend())
    hs_public_key = private_key.public_key()

    # Pre-generate Onion URL
    der_format = hs_public_key.public_bytes(encoding=serialization.Encoding.DER,
                                            format=serialization.PublicFormat.PKCS1)

    onion_url = base64.b32encode(hashlib.sha1(der_format).digest()[:-10]).lower().decode()


    # Generate Stem-compatible key content
    pem_format = private_key.private_bytes(encoding=serialization.Encoding.PEM,
                                           format=serialization.PrivateFormat.TraditionalOpenSSL,
                                           encryption_algorithm=serialization.NoEncryption())
    serialized_key = ''.join(pem_format.decode().split('\n')[1:-2])

    # Generate Stem-compatible HidServAuth cookie for stealth services
    auth_cookie = base64.b64encode(os.urandom(16)).decode().strip('=')

    # Start bundled Tor
    tor_port = get_available_port(1000, 65535)
    tor = Tor()
    tor.connect(tor_port)

    slug = "smelting-smoky" # mock as irrelevant to PoC

    timer = 3
    print('\x1b[2J\x1b[H')  # Clear screen
    for i in range(timer):
        print(6*'\x1b[1A\x1b[2K')
        print("The following URL will be public in {} seconds".format(timer-i))
        print("http://{}.onion/{}".format(onion_url, slug))
        print('')
        print('Cookie for accessing URL will be')
        print("HidServAuth {}.onion {}".format(onion_url, auth_cookie))
        time.sleep(1)

    # Start Onion Service
    service = tor.controller.create_ephemeral_hidden_service(ports={80: 5000},
                                                             key_type='RSA1024',
                                                             key_content=serialized_key,
                                                             await_publication=False,
                                                             basic_auth={'onionshare1': auth_cookie, # Shows the pre-generated cookie is valid
                                                                         'onionshare2': None}        # Makes Stem generate another for comparison
                                                             )
    assert onion_url == service.service_id
    assert len(auth_cookie) == len(service.client_auth['onionshare2'])

    print('\n---------------------------\n')
    print('Stem: Onion URL is')
    print("http://{}.onion/{}".format(service.service_id, slug))
    print('')
    print('Stem: Cookies for accessing the URL are')
    print("HidServAuth {}.onion {}".format(service.service_id, auth_cookie))  # Mock for comparison as it isn't part of service.client_auth dict
    print("HidServAuth {}.onion {}".format(service.service_id, service.client_auth['onionshare2']))
    print('\n')

    tor.stop()


if __name__ == '__main__':
    main()

An example output of this will be

The following URL will be public in 1 seconds
http://dpv3rvacyyzarbak.onion/smelting-smoky

Cookie for accessing URL will be
HidServAuth dpv3rvacyyzarbak.onion x348UBTeZHMjecKB84oXuQ

---------------------------

Stem: Onion URL is
http://dpv3rvacyyzarbak.onion/smelting-smoky

Stem: Cookies for accessing the URL are
HidServAuth dpv3rvacyyzarbak.onion x348UBTeZHMjecKB84oXuQ
HidServAuth dpv3rvacyyzarbak.onion F7R7LxIFhwikq1GgKiCIJw

This of course requires more work now that v3 Onion Services are on way but I hope this helps at least a bit.

@mig5
Copy link
Collaborator Author

@mig5 mig5 commented Jan 27, 2018

Hmm, I'm not sure how much refactoring of the backend would be necessary to achieve this. Currently the much easier method is to simply briefly start the share to obtain the URL, note it down somewhere, then stop the share and then configure the auto-start timer.

Implementation of the above is probably beyond my skill set, I'll leave to @micahflee to think about, or feel free to fork my branch and implement this.

@mig5 mig5 closed this Jan 27, 2018
@maqp
Copy link

@maqp maqp commented Jan 27, 2018

The code above might look complicated but I want to point out that what's new is just the seven SLoC at the start of main(). The rest of the main is just explanation where to use the values, and proof that it works. The Tor class is just a compact representation of what OnionShare does. The backend would probably not require major changes, but small matter of programming usually isn't. I'll look into this when I find the time.

Currently the much easier method...

This is true but only in the case persistent URL is checked. If it's not, pressing Stop Sharing in between will generate different URL. I think in the long run it would be better for users if instead of

1. Click settings icon
2. Check `Use a persistent URL`
3. Click `Save`
4. Choose files to share
5. Press share, wait for URL to appear
6. Click `Copy URL`
8. Press `Stop Sharing`
9. Check `Schedule this share`
10. Check `Start at` and adjust time 
11. Press `Start Sharing`
12. Publish URL

it could be half the number of steps:

1. Choose files to share
2. Check `Schedule this share`
3. Check `Start at` and adjust time
4. Press `Start Sharing`
5. Click `Copy URL`
6. Publish URL

@mig5 mig5 mentioned this pull request Jan 27, 2018
@mig5
Copy link
Collaborator Author

@mig5 mig5 commented Jan 27, 2018

Yep, to be clear, I agree with all those points, I just lack the skill to implement - especially when it will come to the IPC between GUI and backend and showing the URL/slug before start_onion_service() (which is called after the startup timer runs out) - just as one example.

I mainly just fix small bugs and help triage support tickets in this project, I got myself too deep here. I'll leave my branch here in case it helps anyone else should they attempt to implement properly, cheers for your research and suggestions.

@mig5 mig5 deleted the 571_startup_timer branch Mar 4, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment