|
66 | 66 | CENSOR_MESSAGE = b'[[ Credentials removed from proxy log ]]' # replaces credentials; must be a byte-type string |
67 | 67 |
|
68 | 68 | CONFIG_FILE_PATH = '%s/%s.config' % (os.path.dirname(os.path.realpath(__file__)), APP_SHORT_NAME) |
69 | | -CONFIG_SERVER_MATCHER = re.compile(r'^(?P<type>(IMAP|SMTP))-(?P<port>\d+)') |
| 69 | +CONFIG_SERVER_MATCHER = re.compile(r'^(?P<type>(IMAP|SMTP|POP))-(?P<port>\d+)') |
70 | 70 |
|
71 | 71 | MAX_CONNECTIONS = 0 # maximum concurrent IMAP/SMTP connections; 0 = no limit; limit is per server |
72 | 72 |
|
@@ -807,6 +807,40 @@ def send_authentication_request(self): |
807 | 807 | super().process_data(b'AUTH XOAUTH2\r\n') |
808 | 808 |
|
809 | 809 |
|
| 810 | +class POPOAuth2ClientConnection(OAuth2ClientConnection): |
| 811 | + """The client side of the connection - watch for USER and PASS commands and replace with OAuth 2.0""" |
| 812 | + |
| 813 | + class AUTH(enum.Enum): |
| 814 | + PENDING = 1 |
| 815 | + AWAITING_PASS = 2 |
| 816 | + AWAITING_AUTH = 3 |
| 817 | + CREDENTIALS_SENT = 5 |
| 818 | + |
| 819 | + def __init__(self, connection, socket_map, connection_info, server_connection, proxy_parent, custom_configuration): |
| 820 | + super().__init__('POP', connection, socket_map, connection_info, server_connection, proxy_parent, |
| 821 | + custom_configuration) |
| 822 | + self.authentication_state = self.AUTH.PENDING |
| 823 | + |
| 824 | + def process_data(self, byte_data, censor_server_log=False): |
| 825 | + str_data = byte_data.decode('utf-8', 'replace').rstrip('\r\n') |
| 826 | + str_data_lower = str_data.lower() |
| 827 | + |
| 828 | + if self.authentication_state is self.AUTH.PENDING and str_data_lower.startswith('user'): |
| 829 | + self.server_connection.username = str_data[5:] # 5 = len('USER ') |
| 830 | + self.authentication_state = self.AUTH.AWAITING_PASS |
| 831 | + self.censor_next_log = True |
| 832 | + self.send(b'+OK\r\n') # request password |
| 833 | + |
| 834 | + elif self.authentication_state is self.AUTH.AWAITING_PASS and str_data_lower.startswith('pass'): |
| 835 | + self.server_connection.password = str_data[5:] # 5 = len('PASS ') |
| 836 | + self.authentication_state = self.AUTH.AWAITING_AUTH |
| 837 | + super().process_data(b'AUTH XOAUTH2\r\n') |
| 838 | + |
| 839 | + # some other command that we don't handle - pass directly to server |
| 840 | + else: |
| 841 | + super().process_data(byte_data) |
| 842 | + |
| 843 | + |
810 | 844 | class OAuth2ServerConnection(asyncore.dispatcher_with_send): |
811 | 845 | """The base server-side connection, setting up STARTTLS if requested, subclassed for IMAP/SMTP server interaction""" |
812 | 846 |
|
@@ -1049,6 +1083,52 @@ def process_data(self, byte_data): |
1049 | 1083 | super().process_data(byte_data) # a server->client interaction we don't handle; ignore |
1050 | 1084 |
|
1051 | 1085 |
|
| 1086 | +class POPOAuth2ServerConnection(OAuth2ServerConnection): |
| 1087 | + """The POP server side - submit credentials, then watch for +OK and ignore subsequent data""" |
| 1088 | + |
| 1089 | + def __init__(self, socket_map, server_address, connection_info, proxy_parent, custom_configuration): |
| 1090 | + super().__init__('POP', socket_map, server_address, connection_info, proxy_parent, custom_configuration) |
| 1091 | + self.username = None |
| 1092 | + self.password = None |
| 1093 | + |
| 1094 | + def process_data(self, byte_data): |
| 1095 | + str_data = byte_data.decode('utf-8', 'replace').rstrip('\r\n') |
| 1096 | + |
| 1097 | + if self.client_connection.authentication_state is POPOAuth2ClientConnection.AUTH.AWAITING_AUTH and \ |
| 1098 | + self.username is not None and self.password is not None: |
| 1099 | + if str_data.startswith('+'): # + = 'please send credentials' |
| 1100 | + (success, result) = OAuth2Helper.get_oauth2_credentials(self.username, self.password, |
| 1101 | + self.connection_info) |
| 1102 | + if success: |
| 1103 | + self.client_connection.authentication_state = POPOAuth2ClientConnection.AUTH.CREDENTIALS_SENT |
| 1104 | + self.send(b'%s\r\n' % OAuth2Helper.encode_oauth2_string(result), censor_log=True) |
| 1105 | + self.authenticated_username = self.username |
| 1106 | + |
| 1107 | + self.username = None |
| 1108 | + self.password = None |
| 1109 | + if not success: |
| 1110 | + # a local authentication error occurred - send details to the client and exit |
| 1111 | + super().process_data(b'-ERR Authentication failed. %s\r\n' % result.encode('utf-8')) |
| 1112 | + self.client_connection.close() |
| 1113 | + |
| 1114 | + else: |
| 1115 | + super().process_data(byte_data) # an error occurred - just send to the client and exit |
| 1116 | + self.client_connection.close() |
| 1117 | + |
| 1118 | + elif self.client_connection.authentication_state is POPOAuth2ClientConnection.AUTH.CREDENTIALS_SENT: |
| 1119 | + if str_data.startswith('+OK'): |
| 1120 | + Log.info(self.proxy_type, self.connection_info, |
| 1121 | + '[ Successfully authenticated POP connection - removing proxy ]') |
| 1122 | + self.client_connection.authenticated = True |
| 1123 | + super().process_data(byte_data) |
| 1124 | + else: |
| 1125 | + super().process_data(byte_data) # an error occurred - just send to the client and exit |
| 1126 | + self.client_connection.close() |
| 1127 | + |
| 1128 | + else: |
| 1129 | + super().process_data(byte_data) # a server->client interaction we don't handle; ignore |
| 1130 | + |
| 1131 | + |
1052 | 1132 | class OAuth2Proxy(asyncore.dispatcher): |
1053 | 1133 | """Listen on local_address, creating an OAuth2ServerConnection + OAuth2ClientConnection for each new connection""" |
1054 | 1134 |
|
|
0 commit comments