xref: /freebsd/contrib/wpa/wpa_supplicant/examples/dpp-nfc.py (revision 2e3f49888ec8851bafb22011533217487764fdb0)
1#!/usr/bin/python3
2#
3# Example nfcpy to wpa_supplicant wrapper for DPP NFC operations
4# Copyright (c) 2012-2013, Jouni Malinen <j@w1.fi>
5# Copyright (c) 2019-2020, The Linux Foundation
6#
7# This software may be distributed under the terms of the BSD license.
8# See README for more details.
9
10import binascii
11import errno
12import os
13import struct
14import sys
15import time
16import threading
17import argparse
18
19import nfc
20import ndef
21
22import logging
23
24scriptsdir = os.path.dirname(os.path.realpath(sys.modules[__name__].__file__))
25sys.path.append(os.path.join(scriptsdir, '..', '..', 'wpaspy'))
26import wpaspy
27
28wpas_ctrl = '/var/run/wpa_supplicant'
29ifname = None
30init_on_touch = False
31in_raw_mode = False
32prev_tcgetattr = 0
33no_input = False
34continue_loop = True
35terminate_now = False
36summary_file = None
37success_file = None
38netrole = None
39operation_success = False
40mutex = threading.Lock()
41
42C_NORMAL = '\033[0m'
43C_RED = '\033[91m'
44C_GREEN = '\033[92m'
45C_YELLOW = '\033[93m'
46C_BLUE = '\033[94m'
47C_MAGENTA = '\033[95m'
48C_CYAN = '\033[96m'
49
50def summary(txt, color=None):
51    with mutex:
52        if color:
53            print(color + txt + C_NORMAL)
54        else:
55            print(txt)
56        if summary_file:
57            with open(summary_file, 'a') as f:
58                f.write(txt + "\n")
59
60def success_report(txt):
61    summary(txt)
62    if success_file:
63        with open(success_file, 'a') as f:
64            f.write(txt + "\n")
65
66def wpas_connect():
67    ifaces = []
68    if os.path.isdir(wpas_ctrl):
69        try:
70            ifaces = [os.path.join(wpas_ctrl, i) for i in os.listdir(wpas_ctrl)]
71        except OSError as error:
72            summary("Could not find wpa_supplicant: %s", str(error))
73            return None
74
75    if len(ifaces) < 1:
76        summary("No wpa_supplicant control interface found")
77        return None
78
79    for ctrl in ifaces:
80        if ifname and ifname not in ctrl:
81            continue
82        if os.path.basename(ctrl).startswith("p2p-dev-"):
83            # skip P2P management interface
84            continue
85        try:
86            summary("Trying to use control interface " + ctrl)
87            wpas = wpaspy.Ctrl(ctrl)
88            return wpas
89        except Exception as e:
90            pass
91    summary("Could not connect to wpa_supplicant")
92    return None
93
94def dpp_nfc_uri_process(uri):
95    wpas = wpas_connect()
96    if wpas is None:
97        return False
98    peer_id = wpas.request("DPP_NFC_URI " + uri)
99    if "FAIL" in peer_id:
100        summary("Could not parse DPP URI from NFC URI record", color=C_RED)
101        return False
102    peer_id = int(peer_id)
103    summary("peer_id=%d for URI from NFC Tag: %s" % (peer_id, uri))
104    cmd = "DPP_AUTH_INIT peer=%d" % peer_id
105    global enrollee_only, configurator_only, config_params
106    if enrollee_only:
107        cmd += " role=enrollee"
108    elif configurator_only:
109        cmd += " role=configurator"
110    if config_params:
111        cmd += " " + config_params
112    summary("Initiate DPP authentication: " + cmd)
113    res = wpas.request(cmd)
114    if "OK" not in res:
115        summary("Failed to initiate DPP Authentication", color=C_RED)
116        return False
117    summary("DPP Authentication initiated")
118    return True
119
120def dpp_hs_tag_read(record):
121    wpas = wpas_connect()
122    if wpas is None:
123        return False
124    summary(record)
125    if len(record.data) < 5:
126        summary("Too short DPP HS", color=C_RED)
127        return False
128    if record.data[0] != 0:
129        summary("Unexpected URI Identifier Code", color=C_RED)
130        return False
131    uribuf = record.data[1:]
132    try:
133        uri = uribuf.decode()
134    except:
135        summary("Invalid URI payload", color=C_RED)
136        return False
137    summary("URI: " + uri)
138    if not uri.startswith("DPP:"):
139        summary("Not a DPP URI", color=C_RED)
140        return False
141    return dpp_nfc_uri_process(uri)
142
143def get_status(wpas, extra=None):
144    if extra:
145        extra = "-" + extra
146    else:
147        extra = ""
148    res = wpas.request("STATUS" + extra)
149    lines = res.splitlines()
150    vals = dict()
151    for l in lines:
152        try:
153            [name, value] = l.split('=', 1)
154        except ValueError:
155            summary("Ignore unexpected status line: %s" % l)
156            continue
157        vals[name] = value
158    return vals
159
160def get_status_field(wpas, field, extra=None):
161    vals = get_status(wpas, extra)
162    if field in vals:
163        return vals[field]
164    return None
165
166def own_addr(wpas):
167    addr = get_status_field(wpas, "address")
168    if addr is None:
169        addr = get_status_field(wpas, "bssid[0]")
170    return addr
171
172def dpp_bootstrap_gen(wpas, type="qrcode", chan=None, mac=None, info=None,
173                      curve=None, key=None):
174    cmd = "DPP_BOOTSTRAP_GEN type=" + type
175    if chan:
176        cmd += " chan=" + chan
177    if mac:
178        if mac is True:
179            mac = own_addr(wpas)
180        if mac is None:
181            summary("Could not determine local MAC address for bootstrap info")
182        else:
183            cmd += " mac=" + mac.replace(':', '')
184    if info:
185        cmd += " info=" + info
186    if curve:
187        cmd += " curve=" + curve
188    if key:
189        cmd += " key=" + key
190    res = wpas.request(cmd)
191    if "FAIL" in res:
192        raise Exception("Failed to generate bootstrapping info")
193    return int(res)
194
195def dpp_start_listen(wpas, freq):
196    if get_status_field(wpas, "bssid[0]"):
197        summary("Own AP freq: %s MHz" % str(get_status_field(wpas, "freq")))
198        if get_status_field(wpas, "beacon_set", extra="DRIVER") is None:
199            summary("Enable beaconing to have radio ready for RX")
200            wpas.request("DISABLE")
201            wpas.request("SET start_disabled 0")
202            wpas.request("ENABLE")
203    cmd = "DPP_LISTEN %d" % freq
204    global enrollee_only
205    global configurator_only
206    if enrollee_only:
207        cmd += " role=enrollee"
208    elif configurator_only:
209        cmd += " role=configurator"
210    global netrole
211    if netrole:
212        cmd += " netrole=" + netrole
213    summary(cmd)
214    res = wpas.request(cmd)
215    if "OK" not in res:
216        summary("Failed to start DPP listen", color=C_RED)
217        return False
218    return True
219
220def wpas_get_nfc_uri(start_listen=True, pick_channel=False, chan_override=None):
221    listen_freq = 2412
222    wpas = wpas_connect()
223    if wpas is None:
224        return None
225    global own_id, chanlist
226    if chan_override:
227        chan = chan_override
228    else:
229        chan = chanlist
230    if chan and chan.startswith("81/"):
231        listen_freq = int(chan[3:].split(',')[0]) * 5 + 2407
232    if chan is None and get_status_field(wpas, "bssid[0]"):
233        freq = get_status_field(wpas, "freq")
234        if freq:
235            freq = int(freq)
236            if freq >= 2412 and freq <= 2462:
237                chan = "81/%d" % ((freq - 2407) / 5)
238                summary("Use current AP operating channel (%d MHz) as the URI channel list (%s)" % (freq, chan))
239                listen_freq = freq
240    if chan is None and pick_channel:
241        chan = "81/6"
242        summary("Use channel 2437 MHz since no other preference provided")
243        listen_freq = 2437
244    own_id = dpp_bootstrap_gen(wpas, type="nfc-uri", chan=chan, mac=True)
245    res = wpas.request("DPP_BOOTSTRAP_GET_URI %d" % own_id).rstrip()
246    if "FAIL" in res:
247        return None
248    if start_listen:
249        if not dpp_start_listen(wpas, listen_freq):
250            raise Exception("Failed to start listen operation on %d MHz" % listen_freq)
251    return res
252
253def wpas_report_handover_req(uri):
254    wpas = wpas_connect()
255    if wpas is None:
256        return None
257    global own_id
258    cmd = "DPP_NFC_HANDOVER_REQ own=%d uri=%s" % (own_id, uri)
259    return wpas.request(cmd)
260
261def wpas_report_handover_sel(uri):
262    wpas = wpas_connect()
263    if wpas is None:
264        return None
265    global own_id
266    cmd = "DPP_NFC_HANDOVER_SEL own=%d uri=%s" % (own_id, uri)
267    return wpas.request(cmd)
268
269def dpp_handover_client(handover, alt=False):
270    summary("About to start run_dpp_handover_client (alt=%s)" % str(alt))
271    if alt:
272        handover.i_m_selector = False
273    run_dpp_handover_client(handover, alt)
274    summary("Done run_dpp_handover_client (alt=%s)" % str(alt))
275
276def run_client_alt(handover, alt):
277    if handover.start_client_alt and not alt:
278        handover.start_client_alt = False
279        summary("Try to send alternative handover request")
280        dpp_handover_client(handover, alt=True)
281
282class HandoverClient(nfc.handover.HandoverClient):
283    def __init__(self, handover, llc):
284        super(HandoverClient, self).__init__(llc)
285        self.handover = handover
286
287    def recv_records(self, timeout=None):
288        msg = self.recv_octets(timeout)
289        if msg is None:
290            return None
291        records = list(ndef.message_decoder(msg, 'relax'))
292        if records and records[0].type == 'urn:nfc:wkt:Hs':
293            summary("Handover client received message '{0}'".format(records[0].type))
294            return list(ndef.message_decoder(msg, 'relax'))
295        summary("Handover client received invalid message: %s" + binascii.hexlify(msg))
296        return None
297
298    def recv_octets(self, timeout=None):
299        start = time.time()
300        msg = bytearray()
301        while True:
302            poll_timeout = 0.1 if timeout is None or timeout > 0.1 else timeout
303            if not self.socket.poll('recv', poll_timeout):
304                if timeout:
305                    timeout -= time.time() - start
306                    if timeout <= 0:
307                        return None
308                    start = time.time()
309                continue
310            try:
311                r = self.socket.recv()
312                if r is None:
313                    return None
314                msg += r
315            except TypeError:
316                return b''
317            try:
318                list(ndef.message_decoder(msg, 'strict', {}))
319                return bytes(msg)
320            except ndef.DecodeError:
321                if timeout:
322                    timeout -= time.time() - start
323                    if timeout <= 0:
324                        return None
325                    start = time.time()
326                continue
327        return None
328
329def run_dpp_handover_client(handover, alt=False):
330    chan_override = None
331    if alt:
332        chan_override = handover.altchanlist
333        handover.alt_proposal_used = True
334    global test_uri, test_alt_uri
335    if test_uri:
336        summary("TEST MODE: Using specified URI (alt=%s)" % str(alt))
337        uri = test_alt_uri if alt else test_uri
338    else:
339        uri = wpas_get_nfc_uri(start_listen=False, chan_override=chan_override)
340    if uri is None:
341        summary("Cannot start handover client - no bootstrap URI available",
342                color=C_RED)
343        return
344    handover.my_uri = uri
345    uri = ndef.UriRecord(uri)
346    summary("NFC URI record for DPP: " + str(uri))
347    carrier = ndef.Record('application/vnd.wfa.dpp', 'A', uri.data)
348    global test_crn
349    if test_crn:
350        prev, = struct.unpack('>H', test_crn)
351        summary("TEST MODE: Use specified crn %d" % prev)
352        crn = test_crn
353        test_crn = struct.pack('>H', prev + 0x10)
354    else:
355        crn = os.urandom(2)
356    hr = ndef.HandoverRequestRecord(version="1.4", crn=crn)
357    hr.add_alternative_carrier('active', carrier.name)
358    message = [hr, carrier]
359    summary("NFC Handover Request message for DPP: " + str(message))
360
361    if handover.peer_crn is not None and not alt:
362        summary("NFC handover request from peer was already received - do not send own")
363        return
364    if handover.client:
365        summary("Use already started handover client")
366        client = handover.client
367    else:
368        summary("Start handover client")
369        client = HandoverClient(handover, handover.llc)
370        try:
371            summary("Trying to initiate NFC connection handover")
372            client.connect()
373            summary("Connected for handover")
374        except nfc.llcp.ConnectRefused:
375            summary("Handover connection refused")
376            client.close()
377            return
378        except Exception as e:
379            summary("Other exception: " + str(e))
380            client.close()
381            return
382        handover.client = client
383
384    if handover.peer_crn is not None and not alt:
385        summary("NFC handover request from peer was already received - do not send own")
386        return
387
388    summary("Sending handover request")
389
390    handover.my_crn_ready = True
391
392    if not client.send_records(message):
393        handover.my_crn_ready = False
394        summary("Failed to send handover request", color=C_RED)
395        run_client_alt(handover, alt)
396        return
397
398    handover.my_crn, = struct.unpack('>H', crn)
399
400    summary("Receiving handover response")
401    try:
402        start = time.time()
403        message = client.recv_records(timeout=3.0)
404        end = time.time()
405        summary("Received {} record(s) in {} seconds".format(len(message) if message is not None else -1, end - start))
406    except Exception as e:
407        # This is fine if we are the handover selector
408        if handover.hs_sent:
409            summary("Client receive failed as expected since I'm the handover server: %s" % str(e))
410        elif handover.alt_proposal_used and not alt:
411            summary("Client received failed for initial proposal as expected since alternative proposal was also used: %s" % str(e))
412        else:
413            summary("Client receive failed: %s" % str(e), color=C_RED)
414        message = None
415    if message is None:
416        if handover.hs_sent:
417            summary("No response received as expected since I'm the handover server")
418        elif handover.alt_proposal_used and not alt:
419            summary("No response received for initial proposal as expected since alternative proposal was also used")
420        elif handover.try_own and not alt:
421            summary("No response received for initial proposal as expected since alternative proposal will also be sent")
422        else:
423            summary("No response received", color=C_RED)
424        run_client_alt(handover, alt)
425        return
426    summary("Received message: " + str(message))
427    if len(message) < 1 or \
428       not isinstance(message[0], ndef.HandoverSelectRecord):
429        summary("Response was not Hs - received: " + message.type)
430        return
431
432    summary("Received handover select message")
433    summary("alternative carriers: " + str(message[0].alternative_carriers))
434    if handover.i_m_selector:
435        summary("Ignore the received select since I'm the handover selector")
436        run_client_alt(handover, alt)
437        return
438
439    if handover.alt_proposal_used and not alt:
440        summary("Ignore received handover select for the initial proposal since alternative proposal was sent")
441        client.close()
442        return
443
444    dpp_found = False
445    for carrier in message:
446        if isinstance(carrier, ndef.HandoverSelectRecord):
447            continue
448        summary("Remote carrier type: " + carrier.type)
449        if carrier.type == "application/vnd.wfa.dpp":
450            if len(carrier.data) == 0 or carrier.data[0] != 0:
451                summary("URI Identifier Code 'None' not seen", color=C_RED)
452                continue
453            summary("DPP carrier type match - send to wpa_supplicant")
454            dpp_found = True
455            uri = carrier.data[1:].decode("utf-8")
456            summary("DPP URI: " + uri)
457            handover.peer_uri = uri
458            if test_uri:
459                summary("TEST MODE: Fake processing")
460                break
461            res = wpas_report_handover_sel(uri)
462            if res is None or "FAIL" in res:
463                summary("DPP handover report rejected", color=C_RED)
464                break
465
466            success_report("DPP handover reported successfully (initiator)")
467            summary("peer_id=" + res)
468            peer_id = int(res)
469            wpas = wpas_connect()
470            if wpas is None:
471                break
472
473            global enrollee_only
474            global config_params
475            if enrollee_only:
476                extra = " role=enrollee"
477            elif config_params:
478                extra = " role=configurator " + config_params
479            else:
480                # TODO: Single Configurator instance
481                res = wpas.request("DPP_CONFIGURATOR_ADD")
482                if "FAIL" in res:
483                    summary("Failed to initiate Configurator", color=C_RED)
484                    break
485                conf_id = int(res)
486                extra = " conf=sta-dpp configurator=%d" % conf_id
487            global own_id
488            summary("Initiate DPP authentication")
489            cmd = "DPP_AUTH_INIT peer=%d own=%d" % (peer_id, own_id)
490            cmd += extra
491            res = wpas.request(cmd)
492            if "FAIL" in res:
493                summary("Failed to initiate DPP authentication", color=C_RED)
494            break
495
496    if not dpp_found and handover.no_alt_proposal:
497        summary("DPP carrier not seen in response - do not allow alternative proposal anymore")
498    elif not dpp_found:
499        summary("DPP carrier not seen in response - allow peer to initiate a new handover with different parameters")
500        handover.alt_proposal = True
501        handover.my_crn_ready = False
502        handover.my_crn = None
503        handover.peer_crn = None
504        handover.hs_sent = False
505        summary("Returning from dpp_handover_client")
506        return
507
508    summary("Remove peer")
509    handover.close()
510    summary("Done with handover")
511    global only_one
512    if only_one:
513        print("only_one -> stop loop")
514        global continue_loop
515        continue_loop = False
516
517    global no_wait
518    if no_wait or only_one:
519        summary("Trying to exit..")
520        global terminate_now
521        terminate_now = True
522
523    summary("Returning from dpp_handover_client")
524
525class HandoverServer(nfc.handover.HandoverServer):
526    def __init__(self, handover, llc):
527        super(HandoverServer, self).__init__(llc)
528        self.sent_carrier = None
529        self.ho_server_processing = False
530        self.success = False
531        self.llc = llc
532        self.handover = handover
533
534    def serve(self, socket):
535        peer_sap = socket.getpeername()
536        summary("Serving handover client on remote sap {0}".format(peer_sap))
537        send_miu = socket.getsockopt(nfc.llcp.SO_SNDMIU)
538        try:
539            while socket.poll("recv"):
540                req = bytearray()
541                while socket.poll("recv"):
542                    r = socket.recv()
543                    if r is None:
544                        return None
545                    summary("Received %d octets" % len(r))
546                    req += r
547                    if len(req) == 0:
548                        continue
549                    try:
550                        list(ndef.message_decoder(req, 'strict', {}))
551                    except ndef.DecodeError:
552                        continue
553                    summary("Full message received")
554                    resp = self._process_request_data(req)
555                    if resp is None or len(resp) == 0:
556                        summary("No handover select to send out - wait for a possible alternative handover request")
557                        handover.alt_proposal = True
558                        req = bytearray()
559                        continue
560
561                    for offset in range(0, len(resp), send_miu):
562                        if not socket.send(resp[offset:offset + send_miu]):
563                            summary("Failed to send handover select - connection closed")
564                            return
565                    summary("Sent out full handover select")
566                    if handover.terminate_on_hs_send_completion:
567                        handover.delayed_exit()
568
569        except nfc.llcp.Error as e:
570            global terminate_now
571            summary("HandoverServer exception: %s" % e,
572                    color=None if e.errno == errno.EPIPE or terminate_now else C_RED)
573        finally:
574            socket.close()
575            summary("Handover serve thread exiting")
576
577    def process_handover_request_message(self, records):
578        handover = self.handover
579        self.ho_server_processing = True
580        global in_raw_mode
581        was_in_raw_mode = in_raw_mode
582        clear_raw_mode()
583        if was_in_raw_mode:
584            print("\n")
585        summary("HandoverServer - request received: " + str(records))
586
587        for carrier in records:
588            if not isinstance(carrier, ndef.HandoverRequestRecord):
589                continue
590            if carrier.collision_resolution_number:
591                handover.peer_crn = carrier.collision_resolution_number
592                summary("peer_crn: %d" % handover.peer_crn)
593
594        if handover.my_crn is None and handover.my_crn_ready:
595            summary("Still trying to send own handover request - wait a moment to see if that succeeds before checking crn values")
596            for i in range(10):
597                if handover.my_crn is not None:
598                    break
599                time.sleep(0.01)
600        if handover.my_crn is not None:
601            summary("my_crn: %d" % handover.my_crn)
602
603        if handover.my_crn is not None and handover.peer_crn is not None:
604            if handover.my_crn == handover.peer_crn:
605                summary("Same crn used - automatic collision resolution failed")
606                # TODO: Should generate a new Handover Request message
607                return ''
608            if ((handover.my_crn & 1) == (handover.peer_crn & 1) and \
609                handover.my_crn > handover.peer_crn) or \
610               ((handover.my_crn & 1) != (handover.peer_crn & 1) and \
611                handover.my_crn < handover.peer_crn):
612                summary("I'm the Handover Selector Device")
613                handover.i_m_selector = True
614            else:
615                summary("Peer is the Handover Selector device")
616                summary("Ignore the received request.")
617                return ''
618
619        hs = ndef.HandoverSelectRecord('1.4')
620        sel = [hs]
621
622        found = False
623
624        for carrier in records:
625            if isinstance(carrier, ndef.HandoverRequestRecord):
626                continue
627            summary("Remote carrier type: " + carrier.type)
628            if carrier.type == "application/vnd.wfa.dpp":
629                summary("DPP carrier type match - add DPP carrier record")
630                if len(carrier.data) == 0 or carrier.data[0] != 0:
631                    summary("URI Identifier Code 'None' not seen", color=C_RED)
632                    continue
633                uri = carrier.data[1:].decode("utf-8")
634                summary("Received DPP URI: " + uri)
635
636                global test_uri, test_alt_uri
637                if test_uri:
638                    summary("TEST MODE: Using specified URI")
639                    data = test_sel_uri if test_sel_uri else test_uri
640                elif handover.alt_proposal and handover.altchanlist:
641                    summary("Use alternative channel list while processing alternative proposal from peer")
642                    data = wpas_get_nfc_uri(start_listen=False,
643                                            chan_override=handover.altchanlist,
644                                            pick_channel=True)
645                else:
646                    data = wpas_get_nfc_uri(start_listen=False,
647                                            pick_channel=True)
648                summary("Own URI (pre-processing): %s" % data)
649
650                if test_uri:
651                    summary("TEST MODE: Fake processing")
652                    res = "OK"
653                    data += " [%s]" % uri
654                else:
655                    res = wpas_report_handover_req(uri)
656                if res is None or "FAIL" in res:
657                    summary("DPP handover request processing failed",
658                            color=C_RED)
659                    if handover.altchanlist:
660                        data = wpas_get_nfc_uri(start_listen=False,
661                                                chan_override=handover.altchanlist)
662                        summary("Own URI (try another channel list): %s" % data)
663                    continue
664
665                if test_alt_uri:
666                    summary("TEST MODE: Reject initial proposal")
667                    continue
668
669                found = True
670
671                if not test_uri:
672                    wpas = wpas_connect()
673                    if wpas is None:
674                        continue
675                    global own_id
676                    data = wpas.request("DPP_BOOTSTRAP_GET_URI %d" % own_id).rstrip()
677                    if "FAIL" in data:
678                        continue
679                summary("Own URI (post-processing): %s" % data)
680                handover.my_uri = data
681                handover.peer_uri = uri
682                uri = ndef.UriRecord(data)
683                summary("Own bootstrapping NFC URI record: " + str(uri))
684
685                if not test_uri:
686                    info = wpas.request("DPP_BOOTSTRAP_INFO %d" % own_id)
687                    freq = None
688                    for line in info.splitlines():
689                        if line.startswith("use_freq="):
690                            freq = int(line.split('=')[1])
691                    if freq is None or freq == 0:
692                        summary("No channel negotiated over NFC - use channel 6")
693                        freq = 2437
694                    else:
695                        summary("Negotiated channel: %d MHz" % freq)
696                    if not dpp_start_listen(wpas, freq):
697                        break
698
699                carrier = ndef.Record('application/vnd.wfa.dpp', 'A', uri.data)
700                summary("Own DPP carrier record: " + str(carrier))
701                hs.add_alternative_carrier('active', carrier.name)
702                sel = [hs, carrier]
703                break
704
705        summary("Sending handover select: " + str(sel))
706        if found:
707            summary("Handover completed successfully")
708            handover.terminate_on_hs_send_completion = True
709            self.success = True
710            handover.hs_sent = True
711            handover.i_m_selector = True
712        elif handover.no_alt_proposal:
713            summary("Do not try alternative proposal anymore - handover failed",
714                    color=C_RED)
715            handover.hs_sent = True
716        else:
717            summary("Try to initiate with alternative parameters")
718            handover.try_own = True
719            handover.hs_sent = False
720            handover.no_alt_proposal = True
721            if handover.client_thread:
722                handover.start_client_alt = True
723            else:
724                handover.client_thread = threading.Thread(target=llcp_worker,
725                                                          args=(self.llc, True))
726                handover.client_thread.start()
727        return sel
728
729def clear_raw_mode():
730    import sys, tty, termios
731    global prev_tcgetattr, in_raw_mode
732    if not in_raw_mode:
733        return
734    fd = sys.stdin.fileno()
735    termios.tcsetattr(fd, termios.TCSADRAIN, prev_tcgetattr)
736    in_raw_mode = False
737
738def getch():
739    import sys, tty, termios, select
740    global prev_tcgetattr, in_raw_mode
741    fd = sys.stdin.fileno()
742    prev_tcgetattr = termios.tcgetattr(fd)
743    ch = None
744    try:
745        tty.setraw(fd)
746        in_raw_mode = True
747        [i, o, e] = select.select([fd], [], [], 0.05)
748        if i:
749            ch = sys.stdin.read(1)
750    finally:
751        termios.tcsetattr(fd, termios.TCSADRAIN, prev_tcgetattr)
752        in_raw_mode = False
753    return ch
754
755def dpp_tag_read(tag):
756    success = False
757    for record in tag.ndef.records:
758        summary(record)
759        summary("record type " + record.type)
760        if record.type == "application/vnd.wfa.dpp":
761            summary("DPP HS tag - send to wpa_supplicant")
762            success = dpp_hs_tag_read(record)
763            break
764        if isinstance(record, ndef.UriRecord):
765            summary("URI record: uri=" + record.uri)
766            summary("URI record: iri=" + record.iri)
767            if record.iri.startswith("DPP:"):
768                summary("DPP URI")
769                if not dpp_nfc_uri_process(record.iri):
770                    break
771                success = True
772            else:
773                summary("Ignore unknown URI")
774            break
775
776    if success:
777        success_report("Tag read succeeded")
778
779    return success
780
781def rdwr_connected_write_tag(tag):
782    summary("Tag found - writing - " + str(tag))
783    if not tag.ndef:
784        summary("Not a formatted NDEF tag", color=C_RED)
785        return
786    if not tag.ndef.is_writeable:
787        summary("Not a writable tag", color=C_RED)
788        return
789    global dpp_tag_data
790    if tag.ndef.capacity < len(dpp_tag_data):
791        summary("Not enough room for the message")
792        return
793    try:
794        tag.ndef.records = dpp_tag_data
795    except ValueError as e:
796        summary("Writing the tag failed: %s" % str(e), color=C_RED)
797        return
798    success_report("Tag write succeeded")
799    summary("Tag writing completed - remove tag", color=C_GREEN)
800    global only_one, operation_success
801    operation_success = True
802    if only_one:
803        global continue_loop
804        continue_loop = False
805    global dpp_sel_wait_remove
806    return dpp_sel_wait_remove
807
808def write_nfc_uri(clf, wait_remove=True):
809    summary("Write NFC URI record")
810    data = wpas_get_nfc_uri()
811    if data is None:
812        summary("Could not get NFC URI from wpa_supplicant", color=C_RED)
813        return
814
815    global dpp_sel_wait_remove
816    dpp_sel_wait_remove = wait_remove
817    summary("URI: %s" % data)
818    uri = ndef.UriRecord(data)
819    summary(uri)
820
821    summary("Touch an NFC tag to write URI record", color=C_CYAN)
822    global dpp_tag_data
823    dpp_tag_data = [uri]
824    clf.connect(rdwr={'on-connect': rdwr_connected_write_tag})
825
826def write_nfc_hs(clf, wait_remove=True):
827    summary("Write NFC Handover Select record on a tag")
828    data = wpas_get_nfc_uri()
829    if data is None:
830        summary("Could not get NFC URI from wpa_supplicant", color=C_RED)
831        return
832
833    global dpp_sel_wait_remove
834    dpp_sel_wait_remove = wait_remove
835    summary("URI: %s" % data)
836    uri = ndef.UriRecord(data)
837    summary(uri)
838    carrier = ndef.Record('application/vnd.wfa.dpp', 'A', uri.data)
839    hs = ndef.HandoverSelectRecord('1.4')
840    hs.add_alternative_carrier('active', carrier.name)
841    summary(hs)
842    summary(carrier)
843
844    summary("Touch an NFC tag to write HS record", color=C_CYAN)
845    global dpp_tag_data
846    dpp_tag_data = [hs, carrier]
847    summary(dpp_tag_data)
848    clf.connect(rdwr={'on-connect': rdwr_connected_write_tag})
849
850def rdwr_connected(tag):
851    global only_one, no_wait
852    summary("Tag connected: " + str(tag))
853
854    if tag.ndef:
855        summary("NDEF tag: " + tag.type)
856        summary(tag.ndef.records)
857        success = dpp_tag_read(tag)
858        if only_one and success:
859            global continue_loop
860            continue_loop = False
861    else:
862        summary("Not an NDEF tag - remove tag", color=C_RED)
863        return True
864
865    return not no_wait
866
867def llcp_worker(llc, try_alt):
868    global handover
869    print("Start of llcp_worker()")
870    if try_alt:
871        summary("Starting handover client (try_alt)")
872        dpp_handover_client(handover, alt=True)
873        summary("Exiting llcp_worker thread (try_alt)")
874        return
875    global init_on_touch
876    if init_on_touch:
877        summary("Starting handover client (init_on_touch)")
878        dpp_handover_client(handover)
879        summary("Exiting llcp_worker thread (init_on_touch)")
880        return
881
882    global no_input
883    if no_input:
884        summary("Wait for handover to complete")
885    else:
886        print("Wait for handover to complete - press 'i' to initiate")
887    while not handover.wait_connection and handover.srv.sent_carrier is None:
888        if handover.try_own:
889            handover.try_own = False
890            summary("Try to initiate another handover with own parameters")
891            handover.my_crn_ready = False
892            handover.my_crn = None
893            handover.peer_crn = None
894            handover.hs_sent = False
895            dpp_handover_client(handover, alt=True)
896            summary("Exiting llcp_worker thread (retry with own parameters)")
897            return
898        if handover.srv.ho_server_processing:
899            time.sleep(0.025)
900        elif no_input:
901            time.sleep(0.5)
902        else:
903            res = getch()
904            if res != 'i':
905                continue
906            clear_raw_mode()
907            summary("Starting handover client")
908            dpp_handover_client(handover)
909            summary("Exiting llcp_worker thread (manual init)")
910            return
911
912    global in_raw_mode
913    was_in_raw_mode = in_raw_mode
914    clear_raw_mode()
915    if was_in_raw_mode:
916        print("\r")
917    summary("Exiting llcp_worker thread")
918
919class ConnectionHandover():
920    def __init__(self):
921        self.client = None
922        self.client_thread = None
923        self.reset()
924        self.exit_thread = None
925
926    def reset(self):
927        self.wait_connection = False
928        self.my_crn_ready = False
929        self.my_crn = None
930        self.peer_crn = None
931        self.hs_sent = False
932        self.no_alt_proposal = False
933        self.alt_proposal_used = False
934        self.i_m_selector = False
935        self.start_client_alt = False
936        self.terminate_on_hs_send_completion = False
937        self.try_own = False
938        self.my_uri = None
939        self.peer_uri = None
940        self.connected = False
941        self.alt_proposal = False
942
943    def start_handover_server(self, llc):
944        summary("Start handover server")
945        self.llc = llc
946        self.srv = HandoverServer(self, llc)
947
948    def close(self):
949        if self.client:
950            self.client.close()
951            self.client = None
952
953    def run_delayed_exit(self):
954        summary("Trying to exit (delayed)..")
955        time.sleep(0.25)
956        summary("Trying to exit (after wait)..")
957        global terminate_now
958        terminate_now = True
959
960    def delayed_exit(self):
961        global only_one
962        if only_one:
963            self.exit_thread = threading.Thread(target=self.run_delayed_exit)
964            self.exit_thread.start()
965
966def llcp_startup(llc):
967    global handover
968    handover.start_handover_server(llc)
969    return llc
970
971def llcp_connected(llc):
972    summary("P2P LLCP connected")
973    global handover
974    handover.connected = True
975    handover.srv.start()
976    if init_on_touch or not no_input:
977        handover.client_thread = threading.Thread(target=llcp_worker,
978                                                  args=(llc, False))
979        handover.client_thread.start()
980    return True
981
982def llcp_release(llc):
983    summary("LLCP release")
984    global handover
985    handover.close()
986    return True
987
988def terminate_loop():
989    global terminate_now
990    return terminate_now
991
992def main():
993    clf = nfc.ContactlessFrontend()
994
995    parser = argparse.ArgumentParser(description='nfcpy to wpa_supplicant integration for DPP NFC operations')
996    parser.add_argument('-d', const=logging.DEBUG, default=logging.INFO,
997                        action='store_const', dest='loglevel',
998                        help='verbose debug output')
999    parser.add_argument('-q', const=logging.WARNING, action='store_const',
1000                        dest='loglevel', help='be quiet')
1001    parser.add_argument('--only-one', '-1', action='store_true',
1002                        help='run only one operation and exit')
1003    parser.add_argument('--init-on-touch', '-I', action='store_true',
1004                        help='initiate handover on touch')
1005    parser.add_argument('--no-wait', action='store_true',
1006                        help='do not wait for tag to be removed before exiting')
1007    parser.add_argument('--ifname', '-i',
1008                        help='network interface name')
1009    parser.add_argument('--no-input', '-a', action='store_true',
1010                        help='do not use stdout input to initiate handover')
1011    parser.add_argument('--tag-read-only', '-t', action='store_true',
1012                        help='tag read only (do not allow connection handover)')
1013    parser.add_argument('--handover-only', action='store_true',
1014                        help='connection handover only (do not allow tag read)')
1015    parser.add_argument('--enrollee', action='store_true',
1016                        help='run as Enrollee-only')
1017    parser.add_argument('--configurator', action='store_true',
1018                        help='run as Configurator-only')
1019    parser.add_argument('--config-params', default='',
1020                        help='configurator parameters')
1021    parser.add_argument('--ctrl', default='/var/run/wpa_supplicant',
1022                        help='wpa_supplicant/hostapd control interface')
1023    parser.add_argument('--summary',
1024                        help='summary file for writing status updates')
1025    parser.add_argument('--success',
1026                        help='success file for writing success update')
1027    parser.add_argument('--device', default='usb', help='NFC device to open')
1028    parser.add_argument('--chan', default=None, help='channel list')
1029    parser.add_argument('--altchan', default=None, help='alternative channel list')
1030    parser.add_argument('--netrole', default=None, help='netrole for Enrollee')
1031    parser.add_argument('--test-uri', default=None,
1032                        help='test mode: initial URI')
1033    parser.add_argument('--test-alt-uri', default=None,
1034                        help='test mode: alternative URI')
1035    parser.add_argument('--test-sel-uri', default=None,
1036                        help='test mode: handover select URI')
1037    parser.add_argument('--test-crn', default=None,
1038                        help='test mode: hardcoded crn')
1039    parser.add_argument('command', choices=['write-nfc-uri',
1040                                            'write-nfc-hs'],
1041                        nargs='?')
1042    args = parser.parse_args()
1043    summary(args)
1044
1045    global handover
1046    handover = ConnectionHandover()
1047
1048    global only_one
1049    only_one = args.only_one
1050
1051    global no_wait
1052    no_wait = args.no_wait
1053
1054    global chanlist, netrole, test_uri, test_alt_uri, test_sel_uri
1055    global test_crn
1056    chanlist = args.chan
1057    handover.altchanlist = args.altchan
1058    netrole = args.netrole
1059    test_uri = args.test_uri
1060    test_alt_uri = args.test_alt_uri
1061    test_sel_uri = args.test_sel_uri
1062    if args.test_crn:
1063        test_crn = struct.pack('>H', int(args.test_crn))
1064    else:
1065        test_crn = None
1066
1067    logging.basicConfig(level=args.loglevel)
1068    for l in ['nfc.clf.rcs380',
1069              'nfc.clf.transport',
1070              'nfc.clf.device',
1071              'nfc.clf.__init__',
1072              'nfc.llcp',
1073              'nfc.handover']:
1074        log = logging.getLogger(l)
1075        log.setLevel(args.loglevel)
1076
1077    global init_on_touch
1078    init_on_touch = args.init_on_touch
1079
1080    global enrollee_only
1081    enrollee_only = args.enrollee
1082
1083    global configurator_only
1084    configurator_only = args.configurator
1085
1086    global config_params
1087    config_params = args.config_params
1088
1089    if args.ifname:
1090        global ifname
1091        ifname = args.ifname
1092        summary("Selected ifname " + ifname)
1093
1094    if args.ctrl:
1095        global wpas_ctrl
1096        wpas_ctrl = args.ctrl
1097
1098    if args.summary:
1099        global summary_file
1100        summary_file = args.summary
1101
1102    if args.success:
1103        global success_file
1104        success_file = args.success
1105
1106    if args.no_input:
1107        global no_input
1108        no_input = True
1109
1110    clf = nfc.ContactlessFrontend()
1111
1112    try:
1113        if not clf.open(args.device):
1114            summary("Could not open connection with an NFC device", color=C_RED)
1115            raise SystemExit(1)
1116
1117        if args.command == "write-nfc-uri":
1118            write_nfc_uri(clf, wait_remove=not args.no_wait)
1119            if not operation_success:
1120                raise SystemExit(1)
1121            raise SystemExit
1122
1123        if args.command == "write-nfc-hs":
1124            write_nfc_hs(clf, wait_remove=not args.no_wait)
1125            if not operation_success:
1126                raise SystemExit(1)
1127            raise SystemExit
1128
1129        global continue_loop
1130        while continue_loop:
1131            global in_raw_mode
1132            was_in_raw_mode = in_raw_mode
1133            clear_raw_mode()
1134            if was_in_raw_mode:
1135                print("\r")
1136            if args.handover_only:
1137                summary("Waiting a peer to be touched", color=C_MAGENTA)
1138            elif args.tag_read_only:
1139                summary("Waiting for a tag to be touched", color=C_BLUE)
1140            else:
1141                summary("Waiting for a tag or peer to be touched",
1142                        color=C_GREEN)
1143            handover.wait_connection = True
1144            try:
1145                if args.tag_read_only:
1146                    if not clf.connect(rdwr={'on-connect': rdwr_connected}):
1147                        break
1148                elif args.handover_only:
1149                    if not clf.connect(llcp={'on-startup': llcp_startup,
1150                                             'on-connect': llcp_connected,
1151                                             'on-release': llcp_release},
1152                                       terminate=terminate_loop):
1153                        break
1154                else:
1155                    if not clf.connect(rdwr={'on-connect': rdwr_connected},
1156                                       llcp={'on-startup': llcp_startup,
1157                                             'on-connect': llcp_connected,
1158                                             'on-release': llcp_release},
1159                                       terminate=terminate_loop):
1160                        break
1161            except Exception as e:
1162                summary("clf.connect failed: " + str(e))
1163                break
1164
1165            if only_one and handover.connected:
1166                role = "selector" if handover.i_m_selector else "requestor"
1167                summary("Connection handover result: I'm the %s" % role,
1168                        color=C_YELLOW)
1169                if handover.peer_uri:
1170                    summary("Peer URI: " + handover.peer_uri, color=C_YELLOW)
1171                if handover.my_uri:
1172                    summary("My URI: " + handover.my_uri, color=C_YELLOW)
1173                if not (handover.peer_uri and handover.my_uri):
1174                    summary("Negotiated connection handover failed",
1175                            color=C_YELLOW)
1176                break
1177
1178    except KeyboardInterrupt:
1179        raise SystemExit
1180    finally:
1181        clf.close()
1182
1183    raise SystemExit
1184
1185if __name__ == '__main__':
1186    main()
1187