UDP + broadcast support:
- Support for broadcast (discovering all devices and mass commands) - Support for UDP communication - Configurable UDP timeout and source address (for both TCP and UDP) - Only json output sent to stdout, rest to stderr, to get clean json output
This commit is contained in:
parent
395c352100
commit
2c1347b565
15
README.md
15
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 <ip> [-c <cmd> || -j <json>]`
|
||||
`./tplink_smartplug.py [-h] (-t <hostname> | -b)
|
||||
[-c <command> | -j <JSON string>] [-u]
|
||||
[-s <address>] [-T <seconds>]`
|
||||
|
||||
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 ##
|
||||
|
@ -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="<hostname>", required=True, help="Target hostname or IP address", type=validHostname)
|
||||
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument("-t", "--target", metavar="<hostname>", 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="<command>", help="Preset command to send. Choices are: "+", ".join(commands), choices=commands)
|
||||
group.add_argument("-j", "--json", metavar="<JSON string>", 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="<address>", help="Source IP address to use (default is any source)", default="0.0.0.0", type=validHostname)
|
||||
parser.add_argument("-T", "--timeout", metavar="<seconds>", 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))
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user