whisperback (1.8.3)
[whisperback.git] / whisperBack / utils.py
blob9c9c42e06bf71772cad08c62ce3f75115716d35f
1 #!/usr/bin/env python3
2 # -*- coding: UTF-8 -*-
4 ########################################################################
5 # WhisperBack - Send feedback in an encrypted mail
6 # Copyright (C) 2009-2018 Tails developers <tails@boum.org>
8 # This file is part of WhisperBack
10 # WhisperBack is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or (at
13 # your option) any later version.
15 # This program is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 # General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 ########################################################################
24 """various WhisperBack utility functions
26 """
28 import logging
29 import os
30 import re
31 import urllib.parse
32 import locale
33 from textwrap import TextWrapper
35 LOG = logging.getLogger(__name__)
36 # Ugly pathes finder utilities
39 def guess_prefix ():
40 """Tries to guess the prefix
42 @return The guessed prefix"""
44 # XXX: hardcoded path !
45 if os.path.exists ("/usr/local/share/whisperback"):
46 return "/usr/local"
47 elif os.path.exists ("/usr/share/whisperback"):
48 return "/usr"
49 else:
50 return None
53 def get_sharedir ():
54 """Tries to guess the shared data directiry
56 @return The guessed shared data directiry"""
58 if guess_prefix():
59 return os.path.join (guess_prefix(), "share")
60 else:
61 return "data"
64 def get_datadir ():
65 """Tries to guess the datadir
67 @return The guessed datadir"""
68 if guess_prefix():
69 return os.path.join (get_sharedir(), "whisperback")
70 else:
71 return "data"
74 def get_pixmapdir ():
75 """Tries to guess the pixmapdir
77 @return The guessed pixmapdir"""
79 if guess_prefix():
80 return os.path.join (get_sharedir(), "pixmaps")
81 else:
82 return "data"
84 # Input validation fuctions
87 def is_valid_link(candidate):
88 """Check if candidate seems to be a internet link
90 @param candidate the URL to be checked
92 @returns true if candidate is an URL with:
93 - an hostname of the form domain.tld
94 - a scheme http(s) or ftp(S)
95 """
96 LOG.debug("Validating link %s", candidate)
97 parseresult = urllib.parse.urlparse(candidate)
98 #pylint: disable=E1101
99 if (re.search(r'^(ht|f)tp(s)?$', parseresult.scheme) and
100 re.search(r'^(\w{1,}\.){1,}\w{1,}$', parseresult.hostname)):
101 return True
102 else:
103 return False
106 def is_valid_pgp_block(candidate):
107 """Check if candidate seems to be a PGP public key block
109 @param candidate the string to be checked
111 @returns true if candidate starts with `-----BEGIN PGP PUBLIC KEY BLOCK----`
112 and ends with `-----END PGP PUBLIC KEY BLOCK-----`
114 LOG.debug("Validating pgp block %s", candidate)
115 #pylint: disable=C0301
116 if re.search(r"-----BEGIN PGP PUBLIC KEY BLOCK-----\n(?:.*\n)+-----END PGP PUBLIC KEY BLOCK-----",
117 candidate):
118 return True
119 else:
120 return False
123 def is_valid_pgp_id(candidate):
124 """Check if candidate looks like a pgp key ID
126 @param candidate the string to be checked
128 @returns true if candidate is either an 8 or 16 digit hex number or a 40
129 digit hex fingerprint
131 #pylint: disable=C0301
132 LOG.debug("Validating pgp id %s", candidate)
133 if re.search(r"(?:^(?:0x)?(?:[0-9a-fA-F]{8}){1,2}$)|(?:^(?:[0-9f-zA-F]{4} {0,2}){10}$)",
134 candidate):
135 return True
136 else:
137 return False
140 def is_valid_email(candidate):
141 """Check if candidate looks like an email address
143 @param candidate the string to be checked
145 @returns true if candidate is in the form test@example.com
147 LOG.debug("Validating email %s", candidate)
148 if re.search(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", candidate):
149 return True
150 else:
151 return False
154 def is_valid_port(candidate):
155 """Check if candidate is a valid port number (integer between 1 and 65535)
157 @param candidate the port number to be checked
159 LOG.debug("Validating port %s", candidate)
160 try:
161 int(candidate)
162 except ValueError:
163 return False
165 if candidate >= 1 and candidate < 65535:
166 return True
167 else:
168 return False
171 def is_valid_hostname_or_ipv4(candidate):
172 """Check if candidate is a valid hostname or IPv4 address
174 pySocks is not compatible with IPv6
175 hostname specs follow RFC 1123
177 @param candidate the hostname or IPv4 address to validate
179 LOG.debug("Validating host or IP %s", candidate)
180 # XXX: must be updated once IPv6 is enabled
182 if not isinstance(candidate, str):
183 return False
184 if len(candidate) > 255:
185 return False
187 # regex from http://stackoverflow.com/a/106223
188 ip_address_regex = re.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$");
189 hostname_regex = re.compile("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$");
191 if ip_address_regex.match(candidate) or hostname_regex.match(candidate):
192 return True
193 else:
194 return False
197 def sanitize_hardware_info(log_string):
198 """Sanitize hardware-identifying info from a string
200 Removes strings:
202 - labeled as serial numbers and UUID;
203 - looking like IPs or MAC addresses.
205 @param log_string the string to be sanitized
207 @returns a sanitized version of log_string
209 # XXX: must be updated once IPv6 is enabled
211 # DMI
212 log_string = re.sub(r'(DMI:)[\s].*',
213 r'\1[DMI REMOVED]',
214 log_string)
216 # Serial Numbers
217 log_string = re.sub(r'(Serial Number:?[\s]+|'
218 'SerialNo=|'
219 'iSerial[\s]+[\d]+\s+|'
220 'SerialNumber:[\s]+|'
221 'SerialNumber=|'
222 'Serial#:[\s+]|'
223 'serial#[\s+]|'
224 'Serial No:[\s]+'
225 ')[^\s].*',
226 r'\1[SN REMOVED]',
227 log_string)
228 # UUIDs
229 log_string = re.sub(r'(UUID:[\s]+)[^\s].+',
230 r'\1[UUID REMOVED]',
231 log_string)
233 # IPv4s
234 log_string = re.sub(r'([\d]{1,3}\.){3}[\d]{1,3}',
235 r'[IPV4 REMOVED]',
236 log_string)
237 # IPv6s
238 log_string = re.sub(r'(?i)[0-9A-F]{4}(:[0-9A-F]{4}){7}',
239 r'[IPV6 REMOVED]',
240 log_string)
241 # MAC addresses
242 log_string = re.sub(r'(?i)([0-9A-F]{2}:){5,}[0-9A-F]{2}',
243 r'[MAC REMOVED]',
244 log_string)
245 return log_string
248 def wrap_text(text):
249 """Wraps long lines of text to a width of 70 chars
250 @param text the string to be wrapped
252 @return The wrapped text"""
253 LOG.debug("Wrapping text")
255 wrapper = TextWrapper()
256 wrapped = [wrapper.fill(line) for line in text.split('\n')]
257 return '\n'.join(wrapped)