#!/usr/bin/env python3 # SPDX-License-Identifier: MPL-2.0 """Nokia RIL control broker. Socket clients are Gecko's uid, never web content. Parcel layouts follow Gecko 52 dom/system/gonk/ril_worker.js. The vendor socket stays in Android's network namespace; Gecko only sees this broker. """ import copy import hashlib import json import os from pathlib import Path import select import signal import socket import struct import subprocess import sys import time import uuid from call_audio import CallAudio from stream_volume import StreamVolume from hardware_control import HardwareControl from radio_status import Radio, battery, REQUESTS from sms_pdu import submit, deliver, number # Fixed allowlist excludes every SIM PIN/PUK, identity and provisioning request. CONTROL_REQUESTS={9,10,12,23,25,37,40} SOCKET='/work/phone-control.sock' STORE=Path('/work/phone-private/inbox.json') def integer(*values):return struct.pack('<'+'i'*len(values),*values) def string(value): if value is None:return integer(-1) raw=value.encode('utf-16-le');body=integer(len(raw)//2)+raw+b'\0\0' return body+b'\0'*((-len(body))%4) def current_calls(parcel): count=parcel.integer() if parcel.offset=500:raise ValueError('SMS_STORE_FULL') self.value['seen']={k:v for k,v in self.value['seen'].items() if now-v<7*86400} self.value['parts']={k:v for k,v in self.value['parts'].items() if now-v['created']<86400} concat=row.pop('concat') if concat: ref,total,part=concat group=hashlib.sha256((row['sender']+':'+str(ref)+':'+str(total)).encode()).hexdigest() if group not in self.value['parts'] and len(self.value['parts'])>=100:raise ValueError('SMS_STORE_FULL') record=self.value['parts'].setdefault(group,{'created':now,'pieces':{}}) record['pieces'][str(part)]=row['body'] if len(record['pieces'])==total: row['body']=''.join(record['pieces'][str(i)] for i in range(1,total+1)) row['receipt']=str(uuid.uuid4());self.value['inbox'].append(row) del self.value['parts'][group] else: row['receipt']=str(uuid.uuid4());self.value['inbox'].append(row) self.value['seen'][key]=now;self.save() def acknowledge(self,ids): if not isinstance(ids,list) or len(ids)>100 or any(not isinstance(i,str) or len(i)!=36 for i in ids):raise ValueError('INVALID_REQUEST') previous=copy.deepcopy(self.value) try: self.value['inbox']=[r for r in self.value['inbox'] if r['receipt'] not in ids];self.save() except Exception: self.value=previous;raise return True class ControlRadio(Radio): def __init__(self,path,slot,inbox): super().__init__(path,slot);self.inbox=inbox;self.result=None;self.waiting=None def send(self,request,payload=b''): if request not in CONTROL_REQUESTS:raise ValueError('RADIO_INVALID_REQUEST') if not self.sock:raise RuntimeError('RADIO_NOT_READY') self.serial+=1;body=integer(request,self.serial)+payload self.sock.sendall(struct.pack('>I',len(body))+body) self.pending[self.serial]=(request,time.monotonic()) return self.serial def response(self,request,error,parcel,serial=None): if request in REQUESTS:return super().response(request,error,parcel) if request==37:return if (request,serial)!=self.waiting:return if error:self.result=(False,'RADIO_REQUEST_FAILED');return if request==9: try:result=current_calls(parcel) except (ValueError,UnicodeError) as error: print('Call list decode:',str(error),'offset',parcel.offset,'length',len(parcel.data),flush=True) self.result=(False,'RADIO_INVALID_CALL_LIST');return elif request==25: parcel.integer();parcel.string();parcel.integer() result={'state':'sent'} else:result=True self.result=(True,result) def unsolicited(self,event,parcel): if event==1003 and self.slot==0: try: self.inbox.receive(parcel.string());success,cause=1,0 except (OSError,ValueError,UnicodeError): # Do not acknowledge success until the message is durably stored. print('Incoming SMS not stored',flush=True);success,cause=0,255 self.send(37,integer(2,success,cause)) else:super().unsolicited(event,parcel) class Broker: def __init__(self,radios,inbox,audio=None,volume=None,hardware=None): self.radios=radios;self.inbox=inbox;self.session=uuid.uuid4().hex;self.published=0;self.audio=audio;self.volume=volume;self.hardware=hardware;self.original_power={} def step(self,timeout=.1): for radio in self.radios: try:radio.poll() except (OSError,ValueError):radio.close() sockets={r.sock:r for r in self.radios if r.sock} ready,_,_=select.select(list(sockets),[],[],timeout) for sock in ready: try:sockets[sock].receive() except (OSError,ValueError,UnicodeError) as error: print('RIL receive:',type(error).__name__,str(error),flush=True);sockets[sock].close() if time.monotonic()-self.published>=1: dest=Path('/run/tundra-hardware/status.json');dest.parent.mkdir(exist_ok=True,parents=True) try:wifi=json.loads(Path('/run/tundra-wifi/state.json').read_text()) except (OSError,ValueError):wifi={'available':False,'state':'unavailable'} value={'schema':1,'recordedAt':time.time(),'battery':battery(), 'wifi':wifi,'radio':[r.state for r in self.radios]} temp=dest.with_suffix('.tmp');temp.write_text(json.dumps(value));temp.chmod(0o644);temp.replace(dest) self.published=time.monotonic() def exchange(self,request,payload=b'',timeout=12,slot=0): radio=self.radios[slot] # A UI timeout does not cancel the request in the vendor daemon. Do not # enqueue more copies while waiting for its reply or transport recovery. if any(kind==request for kind,_ in radio.pending.values()): raise RuntimeError('RADIO_BUSY') if request==9:timeout=min(timeout,3) radio.result=None;radio.waiting=None try: radio.waiting=(request,radio.send(request,payload));until=time.monotonic()+timeout while radio.result is None and time.monotonic()8192:raise ValueError('INVALID_REQUEST') return json.loads(data) def serve(): os.umask(0o022);inbox=Inbox() radios=[ControlRadio(os.environ.get('VULPES_RIL_SOCKET_DIR','/dev/socket')+'/'+name,i,inbox) for i,name in enumerate(('rild','rild2'))] audio=CallAudio();volume=StreamVolume();hardware=HardwareControl();broker=Broker(radios,inbox,audio,volume,hardware);running=True def stop(*_): nonlocal running running=False signal.signal(signal.SIGTERM,stop) wifi_log=open('/work/wifi-control.log','w') wifi_process=subprocess.Popen([sys.executable,'/work/wifi_control.py'],stdout=wifi_log,stderr=wifi_log) path=Path(SOCKET);path.unlink(missing_ok=True) with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as server: server.bind(SOCKET);os.chown(SOCKET,0,2000);os.chmod(SOCKET,0o660);server.listen(8);server.setblocking(False) print('Nokia phone broker ready; slot 0; Android 6 audio adapter',flush=True) try: while running: broker.step() try:hardware.tick() except OSError: hardware.pattern=[] print('Nokia vibration control unavailable',flush=True) if not select.select([server],[],[],0)[0]:continue conn,_=server.accept() with conn: conn.settimeout(2) pid,uid,gid=struct.unpack('3i',conn.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12)) if uid not in (0,2000):continue try:response={'ok':True,'result':broker.request(read_request(conn))} except (ValueError,RuntimeError,OSError,subprocess.SubprocessError) as error: allowed={'INVALID_REQUEST','INVALID_NUMBER','INVALID_SMS','SMS_TOO_LONG','INVALID_CALL','RADIO_NOT_REGISTERED','RADIO_NOT_READY','RADIO_BUSY','RADIO_REQUEST_FAILED','RADIO_READBACK_FAILED','RADIO_INVALID_CALL_LIST','RADIO_TIMEOUT','NOT_SUPPORTED','CALL_AUDIO_UNAVAILABLE','HARDWARE_READBACK_FAILED'} message=str(error);response={'ok':False,'error':message if message in allowed else 'RADIO_REQUEST_FAILED'} try:conn.sendall(json.dumps(response).encode()+b'\n') except OSError:pass finally: path.unlink(missing_ok=True) broker.restore_power() wifi_process.terminate() try:wifi_process.wait(timeout=5) except subprocess.TimeoutExpired:wifi_process.kill();wifi_process.wait() wifi_log.close() hardware.restore() volume.restore() audio.restore() for radio in radios:radio.close() def client(): raw=sys.stdin.buffer.read(8193) if len(raw)>8192:raise ValueError('INVALID_REQUEST') value=json.loads(raw) with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as conn: conn.settimeout(25);conn.connect(SOCKET) conn.sendall(json.dumps(value).encode()+b'\n') response=b'' while b'\n' not in response: chunk=conn.recv(8192) if not chunk:raise RuntimeError('RADIO_INVALID_REPLY') response+=chunk if len(response)>65536:raise RuntimeError('RADIO_INVALID_REPLY') sys.stdout.write(response.decode()) if __name__=='__main__': if sys.argv[1:]==['--client-envelope']:client() elif not sys.argv[1:]:serve() else:raise SystemExit('Unsupported arguments')