├── remly ├── __init__.py ├── network │ ├── __init__.py │ ├── utils.py │ └── arp.py ├── __main__.py ├── main.py └── remly.py ├── .gitignore ├── fast_install.sh ├── setup.py ├── LICENSE └── README.md /remly/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /remly/network/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /remly/__main__.py: -------------------------------------------------------------------------------- 1 | from remly.main import cli 2 | 3 | cli() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | build/* 3 | remly.egg-info/* 4 | __pycache__/* 5 | *.pyc -------------------------------------------------------------------------------- /fast_install.sh: -------------------------------------------------------------------------------- 1 | yes | pip3 uninstall remly 2 | python3 setup.py sdist bdist_wheel 3 | pip3 install dist/remly-2.0-py3-none-any.whl -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from setuptools import setup, find_packages 3 | 4 | HERE = pathlib.Path(__file__).parent 5 | 6 | setup(name='remly', 7 | version='2.0', 8 | description='Small python library and CLI script which allows running computers remotely on LAN.', 9 | long_description=(HERE / "README.md").read_text(), 10 | long_description_content_type="text/markdown", 11 | author='Piotr Markiewicz', 12 | keywords=['remly'], 13 | license='MIT License', 14 | author_email='sectasy0@gmail.com', 15 | url='https://github.com/sectasy0/remly', 16 | packages=find_packages(), 17 | entry_points={ 18 | 'console_scripts': ['remly=remly.main:cli'] 19 | }, 20 | ) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Christian Bühlmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /remly/network/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from struct import unpack 3 | 4 | def is_valid_eth_address(eth_addr: str) -> bool: 5 | ''' 6 | verification of the mac address independents of the operating system 7 | ''' 8 | return True if len(eth_addr.split(':')) == 6 or len(eth_addr.split('-')) == 6 else False 9 | 10 | 11 | 12 | def is_valid_ipv4_address(ipv4_addr: str) -> bool: 13 | ''' 14 | verification of the ipv4 address 15 | ''' 16 | if ipv4_addr is None: return False 17 | 18 | try: 19 | socket.inet_pton(socket.AF_INET, ipv4_addr) 20 | except AttributeError: 21 | try: 22 | socket.inet_aton(ipv4_addr) 23 | except socket.error: 24 | return False 25 | return ipv4_addr.count('.') == 3 26 | except socket.error: 27 | return False 28 | 29 | return True 30 | 31 | 32 | def crc16(packet: bytes) -> bytes: 33 | ''' 34 | crc16 checksum algorith 35 | ''' 36 | total: int = 0 37 | num_words: int = len(packet) // 2 38 | for chunk in unpack("!%sH" % num_words, packet[0:num_words*2]): 39 | total += chunk 40 | 41 | if len(packet) % 2: 42 | total += ord(packet[-1]) << 8 43 | 44 | total = (total >> 16) + (total & 0xffff) 45 | total += total >> 16 46 | return (~total + 0x10000 & 0xffff) 47 | -------------------------------------------------------------------------------- /remly/network/arp.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen, PIPE 2 | from re import IGNORECASE, search 3 | from platform import system 4 | from os.path import exists 5 | 6 | 7 | def read_arptable(eth_addr: str = None, ipv4_addr: str = None) -> str: 8 | ''' 9 | universal way to get ip address and mac from the system ARP table 10 | select one of the parameters, eth_addr has priority if you fill up both 11 | ''' 12 | searchstr: str = None 13 | addr_type: int = 0 # 0 - mac address 1 - ipv4 address 14 | 15 | if eth_addr and isinstance(eth_addr, str): 16 | searchstr = eth_addr.replace( 17 | ':', '.?') if eth_addr[2] == ':' else eth_addr.replace('-', '.?') 18 | elif ipv4_addr and isinstance(ipv4_addr, str): 19 | addr_type = 1 20 | searchstr = ipv4_addr.replace('.', '.?') 21 | else: raise ValueError('ipv4 addres and mac addres must be a string') 22 | 23 | if system() == "Windows": 24 | output = Popen(['arp', '-a'], stdout=PIPE, 25 | stderr=PIPE).communicate()[0].decode('utf-8') 26 | for line in output.strip().split('\n'): 27 | if search(searchstr, line, IGNORECASE): 28 | return line.split()[addr_type] if addr_type == 0 else line.split()[addr_type + 2] 29 | else: 30 | arp_cache: str = '/proc/net/arp' 31 | if exists(arp_cache): 32 | with open(arp_cache, 'r') as file: 33 | for line in file.readlines()[1:]: 34 | line = line.strip() 35 | if search(searchstr, line): 36 | # for posix systems in this case addr_type for ipv4 must be addr_type + 2 37 | # see /proc/net/arp 38 | return line.split()[addr_type] if addr_type == 0 else line.split()[addr_type + 2] 39 | else: 40 | raise NameError('Record not found in /proc/net/arp') 41 | else: 42 | return None 43 | -------------------------------------------------------------------------------- /remly/main.py: -------------------------------------------------------------------------------- 1 | from remly.remly import wake_up, status 2 | from argparse import ArgumentParser 3 | 4 | 5 | import argparse 6 | 7 | def cli() -> None: 8 | parser: ArgumentParser = ArgumentParser( 9 | description="Cli script allow turn on your computer remotely and check status") 10 | subparsers = parser.add_subparsers(help='commands', dest='command') 11 | 12 | parser_status = subparsers.add_parser('s', help='check device status (accept ipv4 and mac)') 13 | parser_status.add_argument('--mac', '-m', dest='eth_addr', type=str, help='device physical address', required=False, default=argparse.SUPPRESS) 14 | parser_status.add_argument('--ipv4', '-v4', dest='ip_address', type=str, help='device ipv4 address', required=False, default=argparse.SUPPRESS) 15 | parser_status.add_argument('--port', '-p', dest='port', type=int, help='port for ICMP protocol (default: 7)', required=False, default=argparse.SUPPRESS) 16 | parser_status.add_argument('--timeout', '-t', dest='timeout', type=int, help='', required=False, default=argparse.SUPPRESS) 17 | 18 | parser_wake = subparsers.add_parser('w', help="wake up computer") 19 | parser_wake.add_argument('--mac', '-m', dest='eth_addr', type=str, help='device physical address', required=False, default=argparse.SUPPRESS) 20 | parser_wake.add_argument('--port', '-p', dest='port', type=int, help='port for WoL protocol (default: 9)', required=False, default=argparse.SUPPRESS) 21 | parser_wake.add_argument('--bcasts','-b', dest='bcasts', type=str, help='broadcast address (default: 192.168.0.255)', required=False, default=argparse.SUPPRESS, nargs='+') 22 | 23 | args = vars(parser.parse_args()) 24 | if not any(args.values()): 25 | parser.print_usage() 26 | return 27 | 28 | if args['command'] == 's': 29 | del args['command'] 30 | if not args: 31 | parser_status.print_usage() 32 | return 33 | 34 | if status(**args): 35 | print("Online") 36 | else: print("Offline") 37 | 38 | elif args['command'] == 'w': 39 | del args['command'] 40 | if not args: 41 | parser_wake.print_usage() 42 | return 43 | 44 | wake_up(**args) -------------------------------------------------------------------------------- /remly/remly.py: -------------------------------------------------------------------------------- 1 | from struct import pack, unpack 2 | from random import random 3 | import socket 4 | 5 | from typing import List 6 | 7 | from remly.network.utils import (is_valid_eth_address, 8 | is_valid_ipv4_address, crc16) 9 | from remly.network.arp import read_arptable 10 | 11 | 12 | def wake_up(eth_addr: str, bcasts: List[str] = ['192.168.0.255'], port: int = 9) -> None: 13 | ''' 14 | sends the magic packet to broadcast address with device mac address which we want to power on remotely. 15 | ''' 16 | if not is_valid_eth_address(eth_addr): 17 | raise ValueError("Incorrect entry, use 6-bytes physical address") 18 | 19 | address_oct: List[str] = eth_addr.split( 20 | ':') if eth_addr[2] == ':' else eth_addr.split('-') 21 | 22 | magic_packet: bytes = pack('!BBBBBB', 23 | int(address_oct[0], 16), 24 | int(address_oct[1], 16), 25 | int(address_oct[2], 16), 26 | int(address_oct[3], 16), 27 | int(address_oct[4], 16), 28 | int(address_oct[5], 16), 29 | ) 30 | 31 | magic_packet = b'\xff' * 6 + magic_packet * 16 32 | 33 | with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as __sock: 34 | __sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 35 | 36 | for bcast in bcasts: 37 | __sock.sendto(magic_packet, (bcast, port)) 38 | __sock.close() 39 | 40 | 41 | 42 | def status(ip_address: str = None, eth_addr: str = None, port: int = 1, timeout: int = 1) -> bool: 43 | ''' 44 | check the status of the device based on ip or mac address 45 | ''' 46 | if eth_addr: 47 | ip_address = read_arptable(eth_addr) 48 | 49 | if is_valid_ipv4_address(ip_address): 50 | ICMP_ECHO_REQUEST: int = 8 51 | ICMP_CODE: int = socket.getprotobyname('icmp') 52 | 53 | ident: int = int((id(1) * random()) % 65535) 54 | icmp_header: bytes = pack('!BBHHH', ICMP_ECHO_REQUEST, 0, 0, int( 55 | ident), 1) 56 | 57 | payload: bytes = bytes((16 * 'Q').encode()) 58 | 59 | packet_checksum: int = int(crc16(icmp_header + payload)) 60 | icmp_header = pack('!BBHHH', ICMP_ECHO_REQUEST, 0, packet_checksum, ident, 1) 61 | 62 | with socket.socket(socket.AF_INET, socket.SOCK_RAW, ICMP_CODE) as __sock: 63 | try: 64 | __sock.settimeout(timeout) 65 | __sock.sendto(icmp_header+payload, (ip_address, port)) 66 | raw_data: bytes = __sock.recv(1024) 67 | icmp_header: tuple = unpack('!BBHHH', raw_data[20:28]) 68 | if icmp_header[0] == 0: 69 | return True 70 | except socket.timeout: 71 | return False 72 | 73 | __sock.close() 74 | else: 75 | raise ValueError('Incorrect entry, please use IPv4 CIDR or mac format') 76 | 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remly (Wake On Lan) 2 | > Small python library and CLI script which allows running computers remotely on LAN using WoL protocol. 3 | 4 | ![Python version][python-image] 5 | 6 | ## Installation 7 | 8 | ```sh 9 | pip install remly 10 | ``` 11 | 12 | ## Usage example 13 | 14 | A few motivating and useful examples of how remly can be used. 15 | 16 | #### CLI program 17 | 18 | ```sh 19 | usage: remly [-h] {s,w} ... 20 | 21 | Cli script allow turn on your computer remotely and check status 22 | 23 | positional arguments: 24 | {s,w} commands 25 | s check device status (accept ipv4 and mac) 26 | w wake up computer 27 | 28 | optional arguments: 29 | -h, --help show this help message and exit 30 | ``` 31 | 32 | Wake up device 33 | ```sh 34 | remly w -m AA:AA:AA:AA:AA:AA 35 | ``` 36 | 37 | ```sh 38 | usage: remly w [-h] [--mac ETH_ADDR] [--port PORT] [--bcasts BCASTS [BCASTS ...]] 39 | 40 | optional arguments: 41 | -h, --help show this help message and exit 42 | --mac ETH_ADDR, -m ETH_ADDR 43 | device physical address 44 | --port PORT, -p PORT port for WoL protocol (default: 9) 45 | --bcasts BCASTS [BCASTS ...], -b BCASTS [BCASTS ...] 46 | broadcast address (default: 192.168.0.255) 47 | ``` 48 | 49 | Check device status 50 | ``` 51 | remly s -m AA:AA:AA:AA:AA:AA 52 | ``` 53 | 54 | ```sh 55 | usage: remly s [-h] [--mac ETH_ADDR] [--ipv4 IP_ADDRESS] [--port PORT] [--timeout TIMEOUT] 56 | 57 | optional arguments: 58 | -h, --help show this help message and exit 59 | --mac ETH_ADDR, -m ETH_ADDR 60 | device physical address 61 | --ipv4 IP_ADDRESS, -v4 IP_ADDRESS 62 | device ipv4 address 63 | --port PORT, -p PORT port for ICMP protocol (default: 7) 64 | --timeout TIMEOUT, -t TIMEOUT 65 | ``` 66 | 67 | #### library 68 | ```python 69 | from remly import wake_up, status 70 | 71 | # wake up device 72 | wake_up(eth_addr='AA:AA:AA:AA:AA:AA', port=555, broadcast=['192.168.16.255']) 73 | 74 | # check device status 75 | # takes either an ip or a mac address 76 | status(ip_address='192.168.16.5') 77 | 78 | # based on physical address 79 | status(eth_addr='2b:56:ff:d3:3f:31', timeout=5, port=1) 80 | 81 | ``` 82 | ```python 83 | from remly import wake_up 84 | 85 | known_computers = { 86 | 'dev1': '2b:56:ff:d3:3f:31', 87 | 'dev2': '60:f4:4c:53:9a:7f' 88 | } 89 | 90 | for __, dev in known_computers.items(): 91 | wake_up(eth_addr=dev, bcasts=['192.168.16.255'], port=9) 92 | 93 | ``` 94 | 95 | ## Release History 96 | 97 | * 2.0 98 | * code documentation. 99 | * upgrade mac verification function to support more physical addresses formats. 100 | * added future allows getting ip addres from mac (read_arptable). 101 | * added checking device status function. 102 | * 1.0 103 | * release working program. 104 | 105 | ## Meta 106 | 107 | Piotr Markiewicz – [@LinkedIn](https://www.linkedin.com/in/piotr-markiewicz-a44b491b1/) – sectasy0@gmail.coom 108 | 109 | Distributed under the MIT license. See ``LICENSE`` for more information. 110 | 111 | [https://github.com/sectasy0](https://github.com/sectasy0) 112 | 113 | ## Contributing 114 | 115 | 1. Fork it () 116 | 2. Create your feature branch (`git checkout -b feature/fooBar`) 117 | 3. Commit your changes (`git commit -am 'Add some fooBar'`) 118 | 4. Push to the branch (`git push origin feature/fooBar`) 119 | 5. Create a new Pull Request 120 | 121 | 122 | [python-image]: https://img.shields.io/badge/python-3.8-blue 123 | [pypi-image]: https://img.shields.io/badge/pypi-remly-blue 124 | [pypi-url]: pypi.org/project/remly/ 125 | --------------------------------------------------------------------------------