diff --git a/README.md b/README.md index a3ae145..d022a67 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ For the full story, see [Reverse Engineering the TP-Link HS110](https://www.soft ## tplink_smartplug.py ## A python client for the proprietary TP-Link Smart Home protocol to control TP-Link HS100 and HS110 WiFi Smart Plugs. -The SmartHome protocol runs on TCP port 9999 and uses a trivial XOR autokey encryption that provides no security. +The SmartHome protocol runs on TCP/UDP port 9999 and uses a trivial XOR autokey encryption that provides no security. +Other TP-Link products use the same protocol (such as LB-XXX lightbulbs), but may not be compatible with command templates like `on` or `off`. There is no authentication mechanism and commands are accepted independent of device state (configured/unconfigured). @@ -23,9 +24,14 @@ A full list of commands is provided in [tplink-smarthome-commands.txt](tplink-sm #### Usage #### - `./tplink_smartplug.py -t [-c || -j ]` + `./tplink_smartplug.py [-h] (-t | -b) + [-c | -j ] [-u] + [-s
] [-T ]` -Provide the target IP using `-t` and a command to send using either `-c` or `-j`. Commands for the `-c` flag: + +Provide the target IP using `-t` and a command to send using either `-c` or `-j`. Use `-u` to use UDP instead of TCP, and use `-b` to send to the 255.255.255.255 broadcast address (also UDP). To send from a specific source address, specify the address with `-s`. To specify a timeout to wait for all devices to reply to a broadcast, use `-T` (also applies to UDP, default 0.5 seconds). When sending a broadcast, the script will dump all responses received and wait until timeout. + +Predefined commands for the `-c` flag: | Command | Description | |-----------|--------------------------------------| @@ -42,6 +48,9 @@ Provide the target IP using `-t` and a command to send using either `-c` or `-j` | reset | Reset the device to factory settings | | energy | Return realtime voltage/current/power| +(when no command and no JSON string specified, `info` is used by default) + + More advanced commands such as creating or editing rules can be issued using the `-j` flag by providing the full JSON string for the command. Please consult [tplink-smarthome-commands.txt](tplink-smarthome-commands.txt) for a comprehensive list of commands. ## Wireshark Dissector ## diff --git a/tplink_smartplug.py b/tplink_smartplug.py index 4012a55..141f78b 100755 --- a/tplink_smartplug.py +++ b/tplink_smartplug.py @@ -4,7 +4,8 @@ # For use with TP-Link HS-100 or HS-110 # # by Lubomir Stroetmann -# Copyright 2016 softScheck GmbH +# Copyright 2016-2018 softScheck GmbH +# Copyrifht 2018 Wojciech Owczarek # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,17 +20,18 @@ # limitations under the License. # -import socket -import argparse +from socket import * from struct import pack +import argparse +import sys -version = 0.2 +version = 0.3 # Check if hostname is valid def validHostname(hostname): try: - socket.gethostbyname(hostname) - except socket.error: + gethostbyname(hostname) + except error: parser.error("Invalid hostname.") return hostname @@ -53,7 +55,10 @@ commands = {'info' : '{"system":{"get_sysinfo":{}}}', # XOR Autokey Cipher with starting key = 171 def encrypt(string): key = 171 - result = pack('>I', len(string)) + if args.broadcast or args.udp: + result = "" + else: + result = pack('>I', len(string)) for i in string: a = key ^ ord(i) key = a @@ -71,33 +76,82 @@ def decrypt(string): # Parse commandline arguments parser = argparse.ArgumentParser(description="TP-Link Wi-Fi Smart Plug Client v" + str(version)) -parser.add_argument("-t", "--target", metavar="", required=True, help="Target hostname or IP address", type=validHostname) + group = parser.add_mutually_exclusive_group(required=True) +group.add_argument("-t", "--target", metavar="", help="Target hostname or IP address", type=validHostname) +group.add_argument("-b", "--broadcast", help="Send UDP broadcast (255.255.255.255)", default=False, action='store_true') + +group = parser.add_mutually_exclusive_group() group.add_argument("-c", "--command", metavar="", help="Preset command to send. Choices are: "+", ".join(commands), choices=commands) group.add_argument("-j", "--json", metavar="", help="Full JSON string of command to send") + +parser.add_argument("-u", "--udp", help="Send command via UDP instead of TCP (broadcast always UDP)", default=False, action='store_true') +parser.add_argument("-s", "--source", metavar="
", help="Source IP address to use (default is any source)", default="0.0.0.0", type=validHostname) +parser.add_argument("-T", "--timeout", metavar="", help="Maximum time to wait for broadcast reply", default="0.5", type=float, choices=range(0, 3600)) + args = parser.parse_args() # Set target IP, port and command to send +bufsize = 2048 ip = args.target +listenport = 9999 port = 9999 +headerlen = 4 + if args.command is None: cmd = args.json + if cmd is None: + cmd = commands['info'] else: cmd = commands[args.command] - +if args.broadcast: + ip = "255.255.255.255" + headerlen = 0 +if args.udp: + headerlen = 0 # Send command and receive reply try: - sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock_tcp.connect((ip, port)) - sock_tcp.send(encrypt(cmd)) - data = sock_tcp.recv(2048) - sock_tcp.close() - print "Sent: ", cmd - print "Received: ", decrypt(data[4:]) -except socket.error: + gotdata = False + + if args.broadcast or args.udp: + sock = socket(AF_INET, SOCK_DGRAM) + sock.bind((args.source, listenport)) + sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + if args.broadcast: + sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + sock.sendto(encrypt(cmd), (ip, port)) + if args.broadcast: + sys.stderr.write("Broadcasted: " + cmd + '\n') + else: + sys.stderr.write("Sent via UDP: " + cmd + '\n') + sock.settimeout(args.timeout) + (data, src) = sock.recvfrom(bufsize) + while data is not None: + gotdata = True + sys.stderr.write("Received from "+src[0]+":"+str(src[1])+": ") + print decrypt(data[headerlen:]) + (data, src) = sock.recvfrom(bufsize) + else: + sock = socket(AF_INET, SOCK_STREAM) + sock.bind((args.source, listenport)) + sock.connect((ip, port)) + sock.send(encrypt(cmd)) + sys.stderr.write("Sent via TCP: " + cmd + '\n') + data = sock.recv(bufsize) + sys.stderr.write("Received: ") + print decrypt(data[headerlen:]) + + sock.close() + +except timeout: + if gotdata: + quit("Timeout (no more data)") + else: + quit("Timeout and no data received while sending to " + ip + ":" + str(port)) +except error: quit("Cound not connect to host " + ip + ":" + str(port))