1
|
#!/usr/bin/env python
|
2
|
#
|
3
|
# Copyright (C) 2012 Adam Sutton <[email protected]>
|
4
|
#
|
5
|
# This program is free software: you can redistribute it and/or modify
|
6
|
# it under the terms of the GNU General Public License as published by
|
7
|
# the Free Software Foundation, version 3 of the License.
|
8
|
#
|
9
|
# This program is distributed in the hope that it will be useful,
|
10
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
# GNU General Public License for more details.
|
13
|
#
|
14
|
# You should have received a copy of the GNU General Public License
|
15
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
#
|
17
|
"""
|
18
|
This is a very simple HTSP client library written in python mainly just
|
19
|
for demonstration purposes.
|
20
|
|
21
|
Much of the code is pretty rough, but might help people get started
|
22
|
with communicating with HTSP server
|
23
|
"""
|
24
|
|
25
|
# ###########################################################################
|
26
|
# Utilities
|
27
|
# ###########################################################################
|
28
|
|
29
|
def int2bin ( i ):
|
30
|
return chr(i >> 24 & 0xFF) + chr(i >> 16 & 0xFF)\
|
31
|
+ chr(i >> 16 & 0xFF) + chr(i & 0xFF)
|
32
|
|
33
|
def bin2int ( d ):
|
34
|
return (ord(d[0]) << 24) + (ord(d[1]) << 16)\
|
35
|
+ (ord(d[2]) << 8) + ord(d[3])
|
36
|
|
37
|
# ###########################################################################
|
38
|
# HTSMSG Binary handler
|
39
|
#
|
40
|
# Note: this will work with the python native types:
|
41
|
# dict => HMF_MAP
|
42
|
# list => HMF_LIST
|
43
|
# str => HMF_STR
|
44
|
# int => HMF_S64
|
45
|
#
|
46
|
# Note: BIN/STR are both equated to str in python
|
47
|
# ###########################################################################
|
48
|
|
49
|
# HTSMSG types
|
50
|
HMF_MAP = 1
|
51
|
HMF_S64 = 2
|
52
|
HMF_STR = 3
|
53
|
HMF_BIN = 4
|
54
|
HMF_LIST = 5
|
55
|
|
56
|
# Convert python to HTSMSG type
|
57
|
def hmf_type ( f ):
|
58
|
if type(f) == list:
|
59
|
return HMF_MAP
|
60
|
elif type(f) == dict:
|
61
|
return HMF_LIST
|
62
|
elif type(f) == str:
|
63
|
return HMF_STR
|
64
|
elif type(f) == int:
|
65
|
return HMF_S64
|
66
|
else:
|
67
|
raise Exception('invalid type')
|
68
|
|
69
|
# Size for field
|
70
|
def _htsmsg_binary_count ( f ):
|
71
|
ret = 0
|
72
|
if type(f) == str:
|
73
|
ret = ret + len(f)
|
74
|
elif type(f) == int:
|
75
|
while (f):
|
76
|
ret = ret + 1
|
77
|
f = f >> 8
|
78
|
elif type(f) == list or type(f) == map:
|
79
|
ret = ret + htsmsg_binary_count(f)
|
80
|
else:
|
81
|
raise Exception('invalid data type')
|
82
|
return ret
|
83
|
|
84
|
# Recursively determine size of message
|
85
|
def htsmsg_binary_count ( msg ):
|
86
|
ret = 0
|
87
|
lst = type(msg) == list
|
88
|
for f in msg:
|
89
|
ret = ret + 6
|
90
|
if not lst:
|
91
|
ret = ret + len(f)
|
92
|
f = msg[f]
|
93
|
ret = ret + _htsmsg_binary_count(f)
|
94
|
return ret
|
95
|
|
96
|
# Write out field in binary form
|
97
|
def htsmsg_binary_write ( msg ):
|
98
|
ret = ''
|
99
|
lst = type(msg) == list
|
100
|
for f in msg:
|
101
|
na = ''
|
102
|
if not lst:
|
103
|
na = f
|
104
|
f = msg[f]
|
105
|
ret = ret + chr(hmf_type(f))
|
106
|
ret = ret + chr(len(na) & 0xFF)
|
107
|
l = _htsmsg_binary_count(f)
|
108
|
ret = ret + int2bin(l)
|
109
|
ret = ret + na
|
110
|
|
111
|
if type(f) == list or type(f) == dict:
|
112
|
ret = ret + htsmsg_binary_write(f)
|
113
|
elif type(f) == str:
|
114
|
ret = ret + f
|
115
|
elif type(f) == int:
|
116
|
while f:
|
117
|
ret = ret + chr(f & 0xFF)
|
118
|
f = f >> 8
|
119
|
else:
|
120
|
raise Exception('invalid type')
|
121
|
return ret
|
122
|
|
123
|
# Serialize a htsmsg
|
124
|
def htsmsg_binary_serialize ( msg ):
|
125
|
cnt = htsmsg_binary_count(msg)
|
126
|
return int2bin(cnt) + htsmsg_binary_write(msg)
|
127
|
|
128
|
# Deserialize an htsmsg
|
129
|
def htsmsg_binary_deserialize ( data ):
|
130
|
msg = {}
|
131
|
while len(data) > 5:
|
132
|
typ = ord(data[0])
|
133
|
nlen = ord(data[1])
|
134
|
dlen = bin2int(data[2:6])
|
135
|
data = data[6:]
|
136
|
|
137
|
if len < nlen + dlen: raise Exception('not enough data')
|
138
|
|
139
|
name = data[:nlen]
|
140
|
data = data[nlen:]
|
141
|
if typ in [ HMF_STR, HMF_BIN ]:
|
142
|
item = data[:dlen]
|
143
|
elif typ == HMF_S64:
|
144
|
item = 0
|
145
|
i = dlen - 1
|
146
|
while i >= 0:
|
147
|
item = (item << 8) | ord(data[i])
|
148
|
i = i - 1
|
149
|
elif typ in [ HMF_LIST, HMF_MAP ]:
|
150
|
item = htsmsg_binary_deserialize(data[:dlen])
|
151
|
else:
|
152
|
raise Exception('invalid data type %d' % typ)
|
153
|
msg[name] = item
|
154
|
data = data[dlen:]
|
155
|
return msg
|
156
|
|
157
|
# ###########################################################################
|
158
|
# HTSP Client
|
159
|
# ###########################################################################
|
160
|
|
161
|
HTSP_PROTO_VERSION = 5
|
162
|
|
163
|
# Create passwd digest
|
164
|
def htsp_digest ( user, passwd, chal ):
|
165
|
import hashlib
|
166
|
ret = hashlib.sha1(passwd + chal).digest()
|
167
|
return ret
|
168
|
|
169
|
# Client object
|
170
|
class HTSPClient:
|
171
|
|
172
|
# Setup connection
|
173
|
def __init__ ( self, addr, name = 'HTSP PyClient' ):
|
174
|
|
175
|
# Setup
|
176
|
self._sock = socket.create_connection(addr)
|
177
|
self._sock.settimeout(1.0)
|
178
|
self._name = name
|
179
|
self._auth = None
|
180
|
|
181
|
# Handshake
|
182
|
self.hello()
|
183
|
|
184
|
# Send
|
185
|
def _send ( self, func, args ):
|
186
|
args['method'] = func
|
187
|
self._sock.send(htsmsg_binary_serialize(args))
|
188
|
|
189
|
# Receive
|
190
|
def _recv ( self ):
|
191
|
num = bin2int(self._sock.recv(4))
|
192
|
data = ''
|
193
|
while len(data) != num:
|
194
|
tmp = self._sock.recv(num - len(data))
|
195
|
if not tmp: raise Exception('htsp_recv() failed')
|
196
|
data = data + tmp
|
197
|
return htsmsg_binary_deserialize(data)
|
198
|
|
199
|
# Setup
|
200
|
def hello ( self ):
|
201
|
args = {
|
202
|
'htspversion' : HTSP_PROTO_VERSION,
|
203
|
'clientname' : self._name
|
204
|
}
|
205
|
self._send('hello', args)
|
206
|
resp = self._recv()
|
207
|
|
208
|
# Validate
|
209
|
if resp['htspversion'] != HTSP_PROTO_VERSION:
|
210
|
raise Exception('version mismatch')
|
211
|
self._auth = resp['challenge']
|
212
|
|
213
|
# Output info
|
214
|
print 'Connected to: %s (%s)' % (resp['servername'], resp['serverversion'])
|
215
|
return resp
|
216
|
|
217
|
# Authenticate
|
218
|
def authenticate ( self, user, passwd = None ):
|
219
|
args = {
|
220
|
'challenge' : self._auth,
|
221
|
'username' : user,
|
222
|
}
|
223
|
if passwd:
|
224
|
args['digest'] = htsp_digest(user, passwd, self._auth)
|
225
|
self._send('authenticate', args)
|
226
|
resp = self._recv()
|
227
|
|
228
|
# Validate
|
229
|
import pprint
|
230
|
pprint.pprint(resp)
|
231
|
|
232
|
#
|
233
|
# Test
|
234
|
#
|
235
|
|
236
|
if __name__ == '__main__':
|
237
|
import sys, socket
|
238
|
host = 'localhost'
|
239
|
if len(sys.argv) > 1: host = sys.argv[1]
|
240
|
htsp = HTSPClient((host, 9982))
|
241
|
htsp.authenticate('xbmc', 'xbmc')
|