Projet

Général

Profil

Paste
Télécharger au format
Statistiques
| Branche: | Révision:

root / plugins / router / arris-tg3442 @ 7063330e

Historique | Voir | Annoter | Télécharger (8,07 ko)

1
#!/usr/bin/env python3
2

    
3
"""
4
=head1 NAME
5

    
6
arris - MUNIN Plugin to monitor status of Arris TG3442 / TG2492LG-85
7
        and compatible cable modems
8

    
9
=head1 DESCRIPTION
10
Connect to the web-frontend and get current DOCSIS status of upstream and
11
downstream channels. (Signal Power, SNR, Lock Status)
12

    
13

    
14
=head1 REQUIREMENTS
15
- BeautifulSoup
16
- pycryptodome
17

    
18

    
19
=head1 CONFIGURATION
20

    
21
=head2 Example
22
[arris]
23
env.url http://192.168.100.1
24
env.username admin
25
env.password yourpassword
26

    
27

    
28
=head2 Parameters
29
url      - URL to web-frontend
30
username - defaults to "admin"
31
password - valid password
32

    
33

    
34
=head1 REFERENCES
35
https://www.arris.com/products/touchstone-tg3442-cable-voice-gateway/
36

    
37

    
38
=head1 AUTHOR
39

    
40
 Copyright (c) 2019 Daniel Hiepler <d-munin@coderdu.de>
41
 Copyright (c) 2004-2009 Nicolas Stransky <Nico@stransky.cx>
42
 Copyright (c) 2018 Lars Kruse <devel@sumpfralle.de>
43

    
44

    
45
=head1 LICENSE
46
 Permission to use, copy, and modify this software with or without fee
47
 is hereby granted, provided that this entire notice is included in
48
 all source code copies of any software which is or includes a copy or
49
 modification of this software.
50

    
51
 THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR
52
 IMPLIED WARRANTY. IN PARTICULAR, NONE OF THE AUTHORS MAKES ANY
53
 REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE
54
 MERCHANTABILITY OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR
55
 PURPOSE.
56

    
57

    
58
=head1 MAGIC MARKERS
59

    
60
 #%# family=contrib
61

    
62
=cut
63
"""
64

    
65
import binascii
66
from bs4 import BeautifulSoup
67
from Crypto.Cipher import AES
68
import hashlib
69
import json
70
import re
71
import requests
72
import sys
73
import os
74

    
75

    
76
"""
77
The CREDENTIAL_COOKIE below equals the following:
78
base64.encodebytes(b'{ "unique":"280oaPSLiF", "family":"852", "modelname":"TG2492LG-85", '
79
                   '"name":"technician", "tech":true, "moca":0, "wifi":5, "conType":"WAN", '
80
                   '"gwWan":"f", "DefPasswdChanged":"YES" }').decode()
81
"""
82
CREDENTIAL_COOKIE = "eyAidW5pcXVlIjoiMjgwb2FQU0xpRiIsICJmYW1pbHkiOiI4NTIiLCAibW9kZWxuYW1lIjoiVEcy"\
83
    "NDkyTEctODUiLCAibmFtZSI6InRlY2huaWNpYW4iLCAidGVjaCI6dHJ1ZSwgIm1vY2EiOjAsICJ3"\
84
    "aWZpIjo1LCAiY29uVHlwZSI6IldBTiIsICJnd1dhbiI6ImYiLCAiRGVmUGFzc3dkQ2hhbmdlZCI6"\
85
    "IllFUyIgfQ=="
86

    
87

    
88
def login(session, url, username, password):
89
    """login to """
90
    # get login page
91
    r = session.get(f"{url}")
92
    # parse HTML
93
    h = BeautifulSoup(r.text, "lxml")
94
    # get session id from javascript in head
95
    current_session_id = re.search(r".*var currentSessionId = '(.+)';.*", h.head.text)[1]
96

    
97
    # encrypt password
98
    salt = os.urandom(8)
99
    iv = os.urandom(8)
100
    key = hashlib.pbkdf2_hmac(
101
        'sha256',
102
        bytes(password.encode("ascii")),
103
        salt,
104
        iterations=1000,
105
        dklen=128 / 8
106
    )
107
    secret = {"Password": password, "Nonce": current_session_id}
108
    plaintext = bytes(json.dumps(secret).encode("ascii"))
109
    associated_data = "loginPassword"
110
    # initialize cipher
111
    cipher = AES.new(key, AES.MODE_CCM, iv)
112
    # set associated data
113
    cipher.update(bytes(associated_data.encode("ascii")))
114
    # encrypt plaintext
115
    encrypt_data = cipher.encrypt(plaintext)
116
    # append digest
117
    encrypt_data += cipher.digest()
118
    # return
119
    login_data = {
120
        'EncryptData': binascii.hexlify(encrypt_data).decode("ascii"),
121
        'Name': username,
122
        'Salt': binascii.hexlify(salt).decode("ascii"),
123
        'Iv': binascii.hexlify(iv).decode("ascii"),
124
        'AuthData': associated_data
125
    }
126

    
127
    # login
128
    r = session.put(
129
        f"{url}/php/ajaxSet_Password.php",
130
        headers={
131
            "Content-Type": "application/json",
132
            "csrfNonce": "undefined"
133
        },
134
        data=json.dumps(login_data)
135
    )
136

    
137
    # parse result
138
    result = json.loads(r.text)
139
    # success?
140
    if result['p_status'] == "Fail":
141
        print("login failure", file=sys.stderr)
142
        exit(-1)
143
    # remember CSRF nonce
144
    csrf_nonce = result['nonce']
145

    
146
    # prepare headers
147
    session.headers.update({
148
        "X-Requested-With": "XMLHttpRequest",
149
        "csrfNonce": csrf_nonce,
150
        "Origin": f"{url}/",
151
        "Referer": f"{url}/"
152
    })
153
    # set credentials cookie
154
    session.cookies.set("credential", CREDENTIAL_COOKIE)
155

    
156
    # set session
157
    r = session.post(f"{url}/php/ajaxSet_Session.php")
158

    
159

    
160
def docsis_status(session):
161
    """get current DOCSIS status page, parse and return channel data"""
162
    r = session.get(f"{url}/php/status_docsis_data.php")
163
    # extract json from javascript
164
    json_downstream_data = re.search(r".*json_dsData = (.+);.*", r.text)[1]
165
    json_upstream_data = re.search(r".*json_usData = (.+);.*", r.text)[1]
166
    # parse json
167
    downstream_data = json.loads(json_downstream_data)
168
    upstream_data = json.loads(json_upstream_data)
169
    # convert lock status to numeric values
170
    for d in [upstream_data, downstream_data]:
171
        for c in d:
172
            if c['LockStatus'] == "ACTIVE" or c['LockStatus'] == "Locked":
173
                c['LockStatus'] = 1
174
            else:
175
                c['LockStatus'] = 0
176
    return downstream_data, upstream_data
177

    
178

    
179
# -----------------------------------------------------------------------------
180
if __name__ == "__main__":
181
    # get config
182
    url = os.getenv("url")
183
    username = os.getenv("username")
184
    password = os.getenv("password")
185
    # validate config
186
    if not url or not username or not password:
187
        print("Set url, username and password first.", file=sys.stderr)
188
        exit(1)
189
    # create session
190
    session = requests.Session()
191
    # login with username and password
192
    login(session, url, username, password)
193
    # get DOCSIS status
194
    downstream, upstream = docsis_status(session)
195
    # prepare munin graph info
196
    graph_descriptions = [
197
        {
198
            "name": "up_signal",
199
            "title": "DOCSIS Upstream signal strength",
200
            "vlabel": "dBmV",
201
            "info": "DOCSIS upstream signal strength by channel",
202
            "data": upstream,
203
            "key": "PowerLevel"
204
        },
205
        {
206
            "name": "up_lock",
207
            "title": "DOCSIS Upstream lock",
208
            "vlabel": "locked",
209
            "info": "DOCSIS upstream channel lock status",
210
            "data": upstream,
211
            "key": "LockStatus"
212
        },
213
        {
214
            "name": "down_signal",
215
            "title": "DOCSIS Downstream signal strength",
216
            "vlabel": "dBmV",
217
            "info": "DOCSIS downstream signal strength by channel",
218
            "data": downstream,
219
            "key": "PowerLevel"
220
        },
221
        {
222
            "name": "down_lock",
223
            "title": "DOCSIS Downstream lock",
224
            "vlabel": "locked",
225
            "info": "DOCSIS downstream channel lock status",
226
            "data": downstream,
227
            "key": "LockStatus"
228
        },
229
        {
230
            "name": "down_snr",
231
            "title": "DOCSIS Downstream signal/noise ratio",
232
            "vlabel": "dB",
233
            "info": "SNR/MER",
234
            "data": downstream,
235
            "key": "SNRLevel"
236
        }
237
    ]
238

    
239
    # configure ?
240
    if len(sys.argv) > 1 and "config" == sys.argv[1]:
241
        # process all graphs
242
        for g in graph_descriptions:
243
            # graph config
244
            print(f"multigraph docsis_{g['name']}")
245
            print(f"graph_title {g['title']}")
246
            print(f"graph_category network")
247
            print(f"graph_vlabel {g['vlabel']}")
248
            print(f"graph_info {g['info']}")
249
            print(f"graph_scale no")
250

    
251
            # channels
252
            for c in g['data']:
253
                # only use channels with PowerLevel
254
                if not c['PowerLevel']:
255
                    continue
256
                info_text = f"Channel type: {c['ChannelType']}, Modulation: {c['Modulation']}"
257
                print(f"channel_{c['ChannelID']}.label {c['ChannelID']} ({c['Frequency']} MHz)")
258
                print(f"channel_{c['ChannelID']}.info {info_text}")
259

    
260
    # output values ?
261
    else:
262
        # process all graphs
263
        for g in graph_descriptions:
264
            print(f"multigraph docsis_{g['name']}")
265
            # channels
266
            for c in g['data']:
267
                # only use channels with PowerLevel
268
                if not c['PowerLevel']:
269
                    continue
270
                print(f"channel_{c['ChannelID']}.value {c[g['key']]}")