andrej@2544: #!/usr/bin/env python
andrej@2544: # -*- coding: utf-8 -*-
andrej@2544: 
andrej@2544: # This file is part of Beremiz, a Integrated Development Environment for
andrej@2544: # programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
andrej@2544: #
andrej@2544: # Copyright (C) 2019: Edouard TISSERANT
andrej@2544: #
andrej@2544: # See COPYING file for copyrights details.
andrej@2544: #
andrej@2544: # This program is free software; you can redistribute it and/or
andrej@2544: # modify it under the terms of the GNU General Public License
andrej@2544: # as published by the Free Software Foundation; either version 2
andrej@2544: # of the License, or (at your option) any later version.
andrej@2544: #
andrej@2544: # This program is distributed in the hope that it will be useful,
andrej@2544: # but WITHOUT ANY WARRANTY; without even the implied warranty of
andrej@2544: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
andrej@2544: # GNU General Public License for more details.
andrej@2544: #
andrej@2544: # You should have received a copy of the GNU General Public License
andrej@2544: # along with this program; if not, write to the Free Software
andrej@2544: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
andrej@2544: 
andrej@2544: 
andrej@2544: """
andrej@2544: The TLS-PSK adapter that handles SSL connections instead of regular sockets,
andrej@2544: but using Pre Shared Keys instead of Certificates
andrej@2544: """
andrej@2544: 
Edouard@2313: from __future__ import absolute_import
Edouard@2313: from __future__ import print_function
Edouard@2312: 
Edouard@2313: import socket
Edouard@2314: import re
andrej@2537: import ssl
Edouard@2312: import Pyro
Edouard@2314: from Pyro.core import PyroURI
edouard@2492: from Pyro.protocol import _connect_socket, TCPConnection, PYROAdapter
Edouard@2312: from Pyro.errors import ConnectionDeniedError, ProtocolError
Edouard@2312: from Pyro.util import Log
Edouard@2312: 
edouard@2588: try:
edouard@2588:     import sslpsk
edouard@2588: except ImportError as e:
edouard@2588:     print(str(e))
edouard@2588:     sslpsk = None
edouard@2492: 
edouard@2598: 
Edouard@2312: class PYROPSKAdapter(PYROAdapter):
andrej@2544:     """
andrej@2544:     This is essentialy the same as in Pyro/protocol.py
andrej@2544:     only raw_sock wrapping into sock through sslpsk.wrap_socket was added
andrej@2544:     Pyro unfortunately doesn't allow cleaner customization
andrej@2544:     """
andrej@2544: 
edouard@2492:     def bindToURI(self, URI):
Edouard@2312:         with self.lock:   # only 1 thread at a time can bind the URI
Edouard@2312:             try:
edouard@2492:                 self.URI = URI
Edouard@2312: 
Edouard@2312:                 # This are the statements that differ from Pyro/protocol.py
Edouard@2312:                 raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Edouard@2312:                 _connect_socket(raw_sock, URI.address, URI.port, self.timeout)
Edouard@2312:                 sock = sslpsk.wrap_socket(
Edouard@2313:                     raw_sock, psk=Pyro.config.PYROPSK, server_side=False,
edouard@2492:                     ciphers="PSK-AES256-CBC-SHA",  # available in openssl 1.0.2
Edouard@2316:                     ssl_version=ssl.PROTOCOL_TLSv1)
edouard@2492:                 # all the rest is the same as in Pyro/protocol.py
Edouard@2312: 
edouard@2492:                 conn = TCPConnection(sock, sock.getpeername())
Edouard@2312:                 # receive the authentication challenge string, and use that to build the actual identification string.
Edouard@2312:                 try:
edouard@2492:                     authChallenge = self.recvAuthChallenge(conn)
andrej@2536:                 except ProtocolError as x:
Edouard@2312:                     # check if we were denied
edouard@2492:                     if hasattr(x, "partialMsg") and x.partialMsg[:len(self.denyMSG)] == self.denyMSG:
Edouard@2312:                         raise ConnectionDeniedError(Pyro.constants.deniedReasons[int(x.partialMsg[-1])])
Edouard@2312:                     else:
Edouard@2312:                         raise
Edouard@2312:                 # reply with our ident token, generated from the ident passphrase and the challenge
edouard@2492:                 msg = self._sendConnect(sock, self.newConnValidator.createAuthToken(self.ident, authChallenge, conn.addr, self.URI, None))
edouard@2492:                 if msg == self.acceptMSG:
edouard@2492:                     self.conn = conn
edouard@2492:                     self.conn.connected = 1
edouard@2492:                     Log.msg('PYROAdapter', 'connected to', str(URI))
edouard@2492:                     if URI.protocol == 'PYROLOCPSK':
edouard@2492:                         self.resolvePYROLOC_URI("PYROPSK")  # updates self.URI
edouard@2492:                 elif msg[:len(self.denyMSG)] == self.denyMSG:
Edouard@2312:                     try:
Edouard@2312:                         raise ConnectionDeniedError(Pyro.constants.deniedReasons[int(msg[-1])])
edouard@2492:                     except (KeyError, ValueError):
Edouard@2312:                         raise ConnectionDeniedError('invalid response')
Edouard@2312:             except socket.error:
edouard@2492:                 Log.msg('PYROAdapter', 'connection failed to URI', str(URI))
Edouard@2312:                 raise ProtocolError('connection failed')
Edouard@2312: 
edouard@2492: 
Edouard@2312: _getProtocolAdapter = Pyro.protocol.getProtocolAdapter
edouard@2492: 
edouard@2492: 
Edouard@2312: def getProtocolAdapter(protocol):
Edouard@2312:     if protocol in ('PYROPSK', 'PYROLOCPSK'):
Edouard@2312:         return PYROPSKAdapter()
Edouard@2318:     return _getProtocolAdapter(protocol)
Edouard@2312: 
edouard@2492: 
Edouard@2314: _processStringURI = Pyro.core.processStringURI
edouard@2492: 
edouard@2492: 
Edouard@2314: def processStringURI(URI):
edouard@2492:     x = re.match(r'(?P<protocol>PYROLOCPSK)://(?P<hostname>[^\s:]+):?(?P<port>\d+)?/(?P<name>\S*)', URI)
Edouard@2314:     if x:
edouard@2492:         protocol = x.group('protocol')
edouard@2492:         hostname = x.group('hostname')
edouard@2492:         port = x.group('port')
Edouard@2314:         if port:
edouard@2492:             port = int(port)
Edouard@2314:         else:
edouard@2492:             port = 0
edouard@2492:         name = x.group('name')
edouard@2492:         return PyroURI(hostname, name, port, protocol)
Edouard@2314:     return _processStringURI(URI)
edouard@2492: 
edouard@2492: 
andrej@2543: def setupPSKAdapter():
andrej@2543:     """
andrej@2543:     Add PyroAdapter to the list of available in
andrej@2543:     Pyro adapters and handle new supported protocols
andrej@2543: 
andrej@2543:     This function should be called after
andrej@2543:     reimport of Pyro module to enable PYROS:// again.
andrej@2543:     """
edouard@2588:     if sslpsk is not None:
edouard@2588:         Pyro.protocol.getProtocolAdapter = getProtocolAdapter
edouard@2588:         Pyro.core.processStringURI = processStringURI
edouard@2588:     else:
edouard@2588:         raise Exception("sslpsk python module unavailable")