]> www.average.org Git - loctrkd.git/blob - gps303/collector.py
3cb7b0bda98b09a961af7f5cc834ea95ae46a76a
[loctrkd.git] / gps303 / collector.py
1 """ TCP server that communicates with terminals """
2
3 from configparser import ConfigParser
4 from logging import getLogger
5 from os import umask
6 from socket import (
7     socket,
8     AF_INET6,
9     SOCK_STREAM,
10     SOL_SOCKET,
11     SO_KEEPALIVE,
12     SO_REUSEADDR,
13 )
14 from struct import pack
15 from time import time
16 from typing import Dict, List, Optional, Tuple
17 import zmq
18
19 from . import common
20 from .gps303proto import (
21     GPS303Conn,
22     HIBERNATION,
23     LOGIN,
24     inline_response,
25     parse_message,
26     proto_of_message,
27 )
28 from .zmsg import Bcast, Resp
29
30 log = getLogger("gps303/collector")
31
32 MAXBUFFER: int = 4096
33
34
35 class Client:
36     """Connected socket to the terminal plus buffer and metadata"""
37
38     def __init__(self, sock: socket, addr: Tuple[str, int]) -> None:
39         self.sock = sock
40         self.addr = addr
41         self.stream = GPS303Conn()
42         self.imei: Optional[str] = None
43
44     def close(self) -> None:
45         log.debug("Closing fd %d (IMEI %s)", self.sock.fileno(), self.imei)
46         self.sock.close()
47         rest = self.stream.close()
48         if rest:
49             log.warning("%d bytes in buffer on close: %s", len(rest), rest)
50
51     def recv(self) -> Optional[List[Tuple[float, Tuple[str, int], bytes]]]:
52         """Read from the socket and parse complete messages"""
53         try:
54             segment = self.sock.recv(MAXBUFFER)
55         except OSError as e:
56             log.warning(
57                 "Reading from fd %d (IMEI %s): %s",
58                 self.sock.fileno(),
59                 self.imei,
60                 e,
61             )
62             return None
63         if not segment:  # Terminal has closed connection
64             log.info(
65                 "EOF reading from fd %d (IMEI %s)",
66                 self.sock.fileno(),
67                 self.imei,
68             )
69             return None
70         when = time()
71         msgs = []
72         for elem in self.stream.recv(segment):
73             if isinstance(elem, bytes):
74                 msgs.append((when, self.addr, elem))
75             else:
76                 log.warning(
77                     "%s from fd %d (IMEI %s)",
78                     elem,
79                     self.sock.fileno(),
80                     self.imei,
81                 )
82         return msgs
83
84     def send(self, buffer: bytes) -> None:
85         try:
86             self.sock.send(self.stream.enframe(buffer))
87         except OSError as e:
88             log.error(
89                 "Sending to fd %d (IMEI %s): %s",
90                 self.sock.fileno(),
91                 self.imei,
92                 e,
93             )
94
95     def set_imei(self, imei: str) -> None:
96         self.imei = imei
97
98
99 class Clients:
100     def __init__(self) -> None:
101         self.by_fd: Dict[int, Client] = {}
102         self.by_imei: Dict[str, Client] = {}
103
104     def add(self, clntsock: socket, clntaddr: Tuple[str, int]) -> int:
105         fd = clntsock.fileno()
106         log.info("Start serving fd %d from %s", fd, clntaddr)
107         self.by_fd[fd] = Client(clntsock, clntaddr)
108         return fd
109
110     def stop(self, fd: int) -> None:
111         clnt = self.by_fd[fd]
112         log.info("Stop serving fd %d (IMEI %s)", clnt.sock.fileno(), clnt.imei)
113         clnt.close()
114         if clnt.imei:
115             del self.by_imei[clnt.imei]
116         del self.by_fd[fd]
117
118     def recv(
119         self, fd: int
120     ) -> Optional[List[Tuple[Optional[str], float, Tuple[str, int], bytes]]]:
121         clnt = self.by_fd[fd]
122         msgs = clnt.recv()
123         if msgs is None:
124             return None
125         result = []
126         for when, peeraddr, packet in msgs:
127             if proto_of_message(packet) == LOGIN.PROTO:
128                 msg = parse_message(packet)
129                 if isinstance(msg, LOGIN):  # Can be unparseable
130                     if clnt.imei is None:
131                         clnt.imei = msg.imei
132                         log.info(
133                             "LOGIN from fd %d (IMEI %s)",
134                             clnt.sock.fileno(),
135                             clnt.imei,
136                         )
137                         oldclnt = self.by_imei.get(clnt.imei)
138                         if oldclnt is not None:
139                             log.info(
140                                 "Orphaning fd %d with the same IMEI",
141                                 oldclnt.sock.fileno(),
142                             )
143                             oldclnt.imei = None
144                     self.by_imei[clnt.imei] = clnt
145                 else:
146                     log.warning(
147                         "Login message from %s: %s, but client imei unfilled",
148                         peeraddr,
149                         packet,
150                     )
151             result.append((clnt.imei, when, peeraddr, packet))
152             log.debug(
153                 "Received from %s (IMEI %s): %s",
154                 peeraddr,
155                 clnt.imei,
156                 packet.hex(),
157             )
158         return result
159
160     def response(self, resp: Resp) -> None:
161         if resp.imei in self.by_imei:
162             self.by_imei[resp.imei].send(resp.packet)
163         else:
164             log.info("Not connected (IMEI %s)", resp.imei)
165
166
167 def runserver(conf: ConfigParser, handle_hibernate: bool = True) -> None:
168     # Is this https://github.com/zeromq/pyzmq/issues/1627 still not fixed?!
169     zctx = zmq.Context()  # type: ignore
170     zpub = zctx.socket(zmq.PUB)  # type: ignore
171     zpull = zctx.socket(zmq.PULL)  # type: ignore
172     oldmask = umask(0o117)
173     zpub.bind(conf.get("collector", "publishurl"))
174     zpull.bind(conf.get("collector", "listenurl"))
175     umask(oldmask)
176     tcpl = socket(AF_INET6, SOCK_STREAM)
177     tcpl.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
178     tcpl.bind(("", conf.getint("collector", "port")))
179     tcpl.listen(5)
180     tcpfd = tcpl.fileno()
181     poller = zmq.Poller()  # type: ignore
182     poller.register(zpull, flags=zmq.POLLIN)
183     poller.register(tcpfd, flags=zmq.POLLIN)
184     clients = Clients()
185     try:
186         while True:
187             tosend = []
188             topoll = []
189             tostop = []
190             events = poller.poll(1000)
191             for sk, fl in events:
192                 if sk is zpull:
193                     while True:
194                         try:
195                             msg = zpull.recv(zmq.NOBLOCK)
196                             zmsg = Resp(msg)
197                             tosend.append(zmsg)
198                         except zmq.Again:
199                             break
200                 elif sk == tcpfd:
201                     clntsock, clntaddr = tcpl.accept()
202                     clntsock.setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1)
203                     topoll.append((clntsock, clntaddr))
204                 elif fl & zmq.POLLIN:
205                     received = clients.recv(sk)
206                     if received is None:
207                         log.debug("Terminal gone from fd %d", sk)
208                         tostop.append(sk)
209                     else:
210                         for imei, when, peeraddr, packet in received:
211                             proto = proto_of_message(packet)
212                             zpub.send(
213                                 Bcast(
214                                     proto=proto,
215                                     imei=imei,
216                                     when=when,
217                                     peeraddr=peeraddr,
218                                     packet=packet,
219                                 ).packed
220                             )
221                             if proto == HIBERNATION.PROTO and handle_hibernate:
222                                 log.debug(
223                                     "HIBERNATION from fd %d (IMEI %s)",
224                                     sk,
225                                     imei,
226                                 )
227                                 tostop.append(sk)
228                             respmsg = inline_response(packet)
229                             if respmsg is not None:
230                                 tosend.append(
231                                     Resp(imei=imei, when=when, packet=respmsg)
232                                 )
233                 else:
234                     log.debug("Stray event: %s on socket %s", fl, sk)
235             # poll queue consumed, make changes now
236             for zmsg in tosend:
237                 zpub.send(
238                     Bcast(
239                         is_incoming=False,
240                         proto=proto_of_message(zmsg.packet),
241                         when=zmsg.when,
242                         imei=zmsg.imei,
243                         packet=zmsg.packet,
244                     ).packed
245                 )
246                 log.debug("Sending to the client: %s", zmsg)
247                 clients.response(zmsg)
248             for fd in tostop:
249                 poller.unregister(fd)  # type: ignore
250                 clients.stop(fd)
251             for clntsock, clntaddr in topoll:
252                 fd = clients.add(clntsock, clntaddr)
253                 poller.register(fd, flags=zmq.POLLIN)
254     except KeyboardInterrupt:
255         zpub.close()
256         zpull.close()
257         zctx.destroy()  # type: ignore
258         tcpl.close()
259
260
261 if __name__.endswith("__main__"):
262     runserver(common.init(log))