Forum Archive

Is it possible to send a URL to pythonista from my phone and make it open on Windows?

Jhonmicky

Hello everyone,,
I tried a few apps that say they sync ios with windows, but they don't work the way I want or they are very limited.https://showbox.bio/ https://tutuapp.uno/

What I want to do is pass a URL from my iOS device to my windows PC and make it open automatically.

The steps I think it need to have are:

Get the URL

Pass it to Pythonista

Use a script to send this URL, as well as appropriate commands to my windows PC

Run the command, which will open a new tab, loading the URL

What I cannot do yet is to connect pythonista and windows. I think that I'll be able to ssh (now that windows 10 has native ssh). But I have little to no knowledge on these things and I don't even know how to start configuring a ssh server on my pc, let alone connect it using stash.

Do you think this is possible? If it is, could you help me understand how can I create an active server (maybe ssh, but it can also be by using powershell)

mikael

@Jhonmicky, see the instructions here for setting up SSH Remoting on Windows 10.

In Pythonista, you would only use stash if you want to send the URL manually every time; in a script, you can use the paramiko module.

TPO

@Jhonmicky, I have had the exact same need for an app I am currently developping.

You can use Desktop.py below as follows:
- Run the module on the PC
- On the iPhone :

from Desktop import announce_service

# Will launch Firefox on the Desktop, and open 'your_url'
announce_service(your_url, True)
...
# (Optional) Will close the Firefox window
announce_service(your_url, False)

Lots of things can be configured, please see in the source code. This module was developped for a specific application (MyDB, you will see it mentionned several times in the source), not as a general purpose module, you may have to tweak it a bit to suit your needs.

Note on implementation : what started out as a simple UDP broadcast function ended up as a full protocol with a fallback against routers that filter out UDP broadcast packets, and fences against lost or delayed packets, WiFi networks that are slow to authenticate, etc., as I tried it out in various environments (a tame Internet Box at home, enterprise LAN, hotel LAN, etc.)

This is still work in progress.

Hope this helps.

""" Desktop companion for MyDB Web UI.

There are two components to this module:
- service_daemon() runs on the desktop. It will automatically open a browser
  window when the user activates desktop mode on the iPhone, and close the
  browser window when the user exits desktop mode. This is done by listening
  for MyDB Web UI service announcements.
- announce_service() is used by Web_UI.py, to announce that MyDB's Web UI
  service is available / unavailable.

Revision history:
22-Jul-2019 TPO - Created this module
25-Jul-2019 TPO - Initial release """

import json
import os
import socket
import subprocess
import time
import threading

# When the following variable is set to True, both service_daemon() and
# announce_service() will print debug information on stdout. 
DEBUG = True

# Change the following 2 variables if using another browser than Firefox:
START_BROWSER = [(r'"C:\Program Files (x86)\Mozilla Firefox\Firefox.exe" '
                  r'-new-window {ip}/'),
                 (r'"C:\Program Files\Mozilla Firefox\Firefox.exe" '
                  r'-new-window {ip}/')]
STOP_BROWSER = r'TASKKILL /IM firefox.exe /FI "WINDOWTITLE eq MyDB*"'


ANNOUNCE_PORT = 50000
ACK_PORT = 50001
MAGIC = "XXMYDBXX"

ANNOUNCE_COUNTER = 0


def debug(message: str) -> None:
    global DEBUG
    if DEBUG:
        print(message)


def announce_service(ip: str, available: bool) -> bool:
    """ Announce that MyDB's Web UI service is available / unavailable.

    Broadcast the status of the MyDB Web UI service, so that the desktop
    daemon can open a browser window when the user activates desktop mode on
    the iPhone, and close the browser window when the user exits desktop mode.

    Arguments:
    - ip: string containing our IP address.
    - available: if True, announce that the service is now available. If False,
      announce that the service is now unavailable.

    Returns: True if the announcement has been received and ackowledged by the
    desktop daemon, False otherwise.

    Two broadcast modes are tried:
    - UDP broadcast is tried first (code is courtesy of goncalopp,
      https://stackoverflow.com/a/21090815)
    - If UDP broadcast fails, as can happen on LANs where broadcasting packets
      are filtered by the routers (think airport or hotel LAN), a brute force
      method is tried, by sending the announcement packet to 254 IP adresses,
      using values 1 - 255 for byte 4 of our own IP address (should actually
      use subnet mask, but this is a quick and dirty kludge !)

    TODO: document ACK mechanism + counter and session id """

    def do_brute_force_broadcast(s: socket.socket,
                                 ip: str,
                                 port: int,
                                 data: bytes) -> None:
        ip_bytes_1_to_3 = ip[:ip.rfind('.')] + '.'
        for i in range(1, 255):
            print(i, sep=" ")
            s.sendto(data, (ip_bytes_1_to_3 + str(i), port))
        print(".")

    global ACK_PORT, ANNOUNCE_PORT, MAGIC, ANNOUNCE_COUNTER
    SOCKET_TIMEOUT = 0.3
    data = json.dumps({'magic': MAGIC,
                       'counter': ANNOUNCE_COUNTER,
                       'service available': available,
                       'IP': ip,
                       'Session id': time.time()}).encode('utf-8')
    snd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    snd.bind(('', ANNOUNCE_PORT))
    rcv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    rcv.bind(('', ACK_PORT))
    rcv.settimeout(SOCKET_TIMEOUT)
    ack = False
    debug(f"Counter = {ANNOUNCE_COUNTER}, announcing service is "
          f"{'ON' if available else 'OFF'}")
    for brute_force, retries in ((False, 3), (True, 8)):
        debug(f"  Trying {'Brute force' if brute_force else 'UDP'} broadcast")
        snd.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, not brute_force)
        for retry in range(retries):
            debug(f"    Retry # {retry}")
            if brute_force:
                brute_force_broadcast_thread = threading.Thread(
                    target=do_brute_force_broadcast,
                    args=(snd, ip, ANNOUNCE_PORT, data))
                brute_force_broadcast_thread.start()
            else:
                snd.sendto(data, ('<broadcast>', ANNOUNCE_PORT))
            while 1:
                try:
                    data_bytes, addr = rcv.recvfrom(1024)
                except socket.timeout:
                    debug("      Socket time out, going for next retry")
                    break
                debug(f"      Received {data_bytes}")
                try:
                    data = json.loads(data_bytes.decode('utf-8'))
                except json.JSONDecodeError:
                    debug("      Invalid JSON, ignoring")
                    continue
                if (isinstance(data, dict)
                        and data.get('magic') == MAGIC
                        and data.get('counter') == ANNOUNCE_COUNTER
                        and data.get('IP') == ip):
                    debug("      ACK received")
                    ack = True
                    break
                print("      Invalid ACK, ignoring")
            if ack:
                break
        if brute_force:
            # Need to wait for broadcast_thread to be done before we proceed
            # to close the snd socket, or do_brute_force_broadcast() will fail
            # with "Errno 9: Bad file descriptor".
            brute_force_broadcast_thread.join()
        if ack:
            break
    if not ack:
        debug("  Both UDP and brute force broadcast methods failed, giving up")
    snd.close()
    rcv.close()
    ANNOUNCE_COUNTER += 1
    return ack


def service_daemon():
    """ Automatically open / close web browser on desktop.

    service_daemon() runs on the desktop. It will automatically open a browser
    window when the user activates desktop mode on the iPhone, and close the
    browser window when the user exits desktop mode. This is done by listening
    for MyDB Web UI service announcements. """

    global ACK_PORT, ANNOUNCE_PORT, MAGIC

    # Keep track of the counter value for last annoucement packet processed, in
    # order to ignore retry packets sent by announce_service(), which all have
    # the same counter value.
    last_counter = -1

    # Web_UI sessions all start with a counter value of 0, so we need to keep
    # track of Web_UI sessions and reset last_counter every time a new session
    # is started (i.e. when the user activates desktop mode on the iPhone)
    current_session_id = -1

    rcv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    rcv.bind(('', ANNOUNCE_PORT))
    snd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    snd.bind(('', ACK_PORT))
    debug(f"Listening on port {ANNOUNCE_PORT}")
    while 1:
        data_bytes, addr = rcv.recvfrom(1024)
        debug(f"Received packet from {addr}:\n    '{data_bytes}'")
        try:
            data = json.loads(data_bytes.decode('utf-8'))
        except json.JSONDecodeError:
            debug("    Invalid JSON, ignoring")
            continue
        if (isinstance(data, dict)
                and data.get('magic') == MAGIC
                and 'counter' in data
                and 'IP' in data
                and 'service available' in data
                and 'Session id' in data):
            if (data['Session id'] == current_session_id
                    and data['counter'] <= last_counter):
                debug(f"    Ignoring MyDB announcement for counter = "
                      f"{data['counter']}, already processed")
                continue
            current_session_id = data['Session id']
            last_counter = data['counter']
            debug(f"    MyDB announcement: IP = {data['IP']}, "
                  f"service {'ON' if data['service available'] else 'OFF'}, "
                  f"counter = {data['counter']}")
            ack = json.dumps({'magic': MAGIC,
                              'counter': data['counter'],
                              'IP': data['IP']}).encode('utf-8')
            snd.sendto(ack, (data['IP'], ACK_PORT))
            debug(f"    ACK sent back to {data['IP']}:{ACK_PORT}")
            if data['service available']:
                debug("    Launching browser")
                for start_browser in START_BROWSER:
                    try:
                        subprocess.Popen(start_browser.format(ip=data['IP']))
                    except FileNotFoundError:
                        continue
                    break
            else:
                debug("    Closing browser")
                os.system(STOP_BROWSER)
        else:
            debug("    Not a MyDB announcement, ignoring")


if __name__ == '__main__':
    service_daemon()
bennr01

A few month ago someone asked a similiar question on reddit.
Assuming you can run python on both sides, you can use this example. For more details, please see the discussion in the reddit discussion linked above. This example does not use SSH, but the socket & webbrowser modules instead.

TPO

@Jhonmicky, can you be more precise on your use case ?

More specifically, does your Windows 10 PC have a fixed IP address ? If so, the method proposed by @bennr01 should work (and would be simpler). If your PC does not have a fixed IP address, then you need to establish a protocol between your iOS device and your windows PC, so that they can exchange IP addresses (and this is where the module I posted earlier would come in handy).

cvp

To dialog with my Mac, I use SSH (paramiko) and I connect to the computer name of the Mac which does not have a fixed ip (dhcp)