|
1 | 1 | # |
2 | | -# Copyright (C) 2021 Sébastien Helleu <flashcode@flashtux.org> |
| 2 | +# SPDX-FileCopyrightText: 2021-2026 Sébastien Helleu <flashcode@flashtux.org> |
| 3 | +# |
| 4 | +# SPDX-License-Identifier: GPL-3.0-or-later |
3 | 5 | # |
4 | 6 | # This program is free software; you can redistribute it and/or modify |
5 | 7 | # it under the terms of the GNU General Public License as published by |
|
18 | 20 | # Prevent a password from being accidentally sent to a buffer |
19 | 21 | # (requires WeeChat ≥ 0.4.0 and WeeChat ≥ 3.1 to check secured data). |
20 | 22 | # |
21 | | -# History: |
22 | | -# |
23 | | -# 2021-03-13, Sébastien Helleu <flashcode@flashtux.org>: |
24 | | -# version 1.2.1: simplify regex condition |
25 | | -# 2021-03-12, Sébastien Helleu <flashcode@flashtux.org>: |
26 | | -# version 1.2.0: add option "allowed_regex" |
27 | | -# 2021-02-26, Sébastien Helleu <flashcode@flashtux.org>: |
28 | | -# version 1.1.0: add options "check_secured_data" and "max_rejects" |
29 | | -# 2021-02-24, Sébastien Helleu <flashcode@flashtux.org>: |
30 | | -# version 1.0: first official version |
31 | 23 |
|
32 | 24 | """Anti password script.""" |
33 | 25 |
|
34 | 26 | import re |
35 | 27 |
|
| 28 | +IMPORT_OK = True |
| 29 | + |
36 | 30 | try: |
37 | | - import weechat |
38 | | - IMPORT_OK = True |
| 31 | + import weechat # type: ignore[import] |
39 | 32 | except ImportError: |
40 | | - print('This script must be run under WeeChat.') |
41 | | - print('Get WeeChat now at: https://weechat.org/') |
| 33 | + print("This script must be run under WeeChat.") |
| 34 | + print("Get WeeChat now at: https://weechat.org/") |
42 | 35 | IMPORT_OK = False |
43 | 36 |
|
44 | | -SCRIPT_NAME = 'anti_password' |
45 | | -SCRIPT_AUTHOR = 'Sébastien Helleu <flashcode@flashtux.org>' |
46 | | -SCRIPT_VERSION = '1.2.1' |
47 | | -SCRIPT_LICENSE = 'GPL3' |
48 | | -SCRIPT_DESC = 'Prevent a password from being accidentally sent to a buffer' |
| 37 | +SCRIPT_NAME = "anti_password" |
| 38 | +SCRIPT_AUTHOR = "Sébastien Helleu <flashcode@flashtux.org>" |
| 39 | +SCRIPT_VERSION = "1.2.2" |
| 40 | +SCRIPT_LICENSE = "GPL3" |
| 41 | +SCRIPT_DESC = "Prevent a password from being accidentally sent to a buffer" |
49 | 42 |
|
50 | 43 | # script options |
51 | | -ap_settings_default = { |
52 | | - 'allowed_regex': { |
53 | | - 'default': '^(http|https|ftp|file|irc)://', |
54 | | - 'help': ( |
55 | | - 'allowed regular expression (case is ignored); this is checked ' |
56 | | - 'first: if the input matched this regular expression, it is ' |
57 | | - 'considered safe and sent immediately to the buffer; ' |
58 | | - 'if empty, nothing specific is allowed' |
| 44 | +AP_SETTINGS_DEFAULT = { |
| 45 | + "allowed_regex": { |
| 46 | + "default": "^(http|https|ftp|file|irc)://", |
| 47 | + "help": ( |
| 48 | + "allowed regular expression (case is ignored); this is checked " |
| 49 | + "first: if the input matched this regular expression, it is " |
| 50 | + "considered safe and sent immediately to the buffer; " |
| 51 | + "if empty, nothing specific is allowed" |
59 | 52 | ), |
60 | 53 | }, |
61 | | - 'password_condition': { |
62 | | - 'default': ( |
63 | | - '${words} == 1 && ${lower} >= 1 && ${upper} >= 1 ' |
64 | | - '&& ${digits} >= 1 && ${special} >= 1' |
| 54 | + "password_condition": { |
| 55 | + "default": ( |
| 56 | + "${words} == 1 && ${lower} >= 1 && ${upper} >= 1 " |
| 57 | + "&& ${digits} >= 1 && ${special} >= 1" |
65 | 58 | ), |
66 | | - 'help': ( |
67 | | - 'condition evaluated to check if the input string is a password; ' |
68 | | - 'allowed variables: ' |
69 | | - '${words} = number of words, ' |
70 | | - '${lower} = number of lower case letters, ' |
71 | | - '${upper} = number of upper case letters, ' |
72 | | - '${digits} = number of digits, ' |
73 | | - '${special} = number of other chars (not letter/digits/spaces), ' |
| 59 | + "help": ( |
| 60 | + "condition evaluated to check if the input string is a password; " |
| 61 | + "allowed variables: " |
| 62 | + "${words} = number of words, " |
| 63 | + "${lower} = number of lower case letters, " |
| 64 | + "${upper} = number of upper case letters, " |
| 65 | + "${digits} = number of digits, " |
| 66 | + "${special} = number of other chars (not letter/digits/spaces), " |
74 | 67 | ), |
75 | 68 | }, |
76 | | - 'check_secured_data': { |
77 | | - 'default': 'equal', |
78 | | - 'help': ( |
79 | | - 'consider that all secured data values are passwords and can not ' |
80 | | - 'be sent to buffers, possible values: ' |
81 | | - 'off = do not check secured data at all, ' |
82 | | - 'equal = reject input if stripped input is equal to a secured ' |
83 | | - 'data value, ' |
84 | | - 'include = reject input if a secured data value is part of input' |
| 69 | + "check_secured_data": { |
| 70 | + "default": "equal", |
| 71 | + "help": ( |
| 72 | + "consider that all secured data values are passwords and can not " |
| 73 | + "be sent to buffers, possible values: " |
| 74 | + "off = do not check secured data at all, " |
| 75 | + "equal = reject input if stripped input is equal to a secured " |
| 76 | + "data value, " |
| 77 | + "include = reject input if a secured data value is part of input" |
85 | 78 | ), |
86 | 79 | }, |
87 | | - 'max_rejects': { |
88 | | - 'default': '3', |
89 | | - 'help': ( |
90 | | - 'max number of rejects for a given input text; if you press Enter ' |
91 | | - 'more than N times with exactly the same input, the text is ' |
92 | | - 'finally sent to the buffer; ' |
93 | | - 'if set to 0, the input is never sent to the buffer when it is ' |
94 | | - 'considered harmful (be careful, according to the other settings, ' |
95 | | - 'this can completely block the input)' |
| 80 | + "max_rejects": { |
| 81 | + "default": "3", |
| 82 | + "help": ( |
| 83 | + "max number of rejects for a given input text; if you press Enter " |
| 84 | + "more than N times with exactly the same input, the text is " |
| 85 | + "finally sent to the buffer; " |
| 86 | + "if set to 0, the input is never sent to the buffer when it is " |
| 87 | + "considered harmful (be careful, according to the other settings, " |
| 88 | + "this can completely block the input)" |
96 | 89 | ), |
97 | 90 | }, |
98 | 91 | } |
99 | 92 | ap_settings = {} |
100 | 93 | ap_reject = { |
101 | | - 'input': '', |
102 | | - 'count': 0, |
| 94 | + "input": "", |
| 95 | + "count": 0, |
103 | 96 | } |
104 | 97 |
|
105 | 98 |
|
106 | | -def ap_config_cb(data, option, value): |
| 99 | +def ap_config_cb(_data: str, option: str, value: str) -> int: |
107 | 100 | """Called when a script option is changed.""" |
108 | | - pos = option.rfind('.') |
| 101 | + pos = option.rfind(".") |
109 | 102 | if pos > 0: |
110 | | - name = option[pos+1:] |
| 103 | + name = option[pos + 1 :] |
111 | 104 | if name in ap_settings: |
112 | 105 | ap_settings[name] = value |
113 | 106 | return weechat.WEECHAT_RC_OK |
114 | 107 |
|
115 | 108 |
|
116 | | -def ap_input_is_secured_data(input_text): |
| 109 | +def ap_input_is_secured_data(input_text: str) -> bool: |
117 | 110 | """Check if input_text is any value of a secured data.""" |
118 | | - check = ap_settings['check_secured_data'] |
119 | | - if check == 'off': |
| 111 | + check = ap_settings["check_secured_data"] |
| 112 | + if check == "off": |
120 | 113 | return False |
121 | | - sec_data = weechat.info_get_hashtable('secured_data', {}) or {} |
122 | | - if check == 'include': |
| 114 | + sec_data = weechat.info_get_hashtable("secured_data", {}) or {} |
| 115 | + if check == "include": |
123 | 116 | for value in sec_data.values(): |
124 | 117 | if value and value in input_text: |
125 | 118 | return True |
126 | | - if check == 'equal': |
| 119 | + if check == "equal": |
127 | 120 | return input_text.strip() in sec_data.values() |
128 | 121 | return False |
129 | 122 |
|
130 | 123 |
|
131 | | -def ap_input_matches_condition(input_text): |
| 124 | +def ap_input_matches_condition(input_text: str) -> bool: |
132 | 125 | """Check if input_text matches the password condition.""" |
133 | 126 | # count chars in the input text |
134 | | - words = len(list(filter(None, re.split(r'\s+', input_text)))) |
| 127 | + words = len(list(filter(None, re.split(r"\s+", input_text)))) |
135 | 128 | lower = sum(1 for c in input_text if c.islower()) |
136 | 129 | upper = sum(1 for c in input_text if c.isupper()) |
137 | 130 | digits = sum(1 for c in input_text if c.isdigit()) |
138 | 131 | special = sum(1 for c in input_text if not (c.isalnum() or c.isspace())) |
139 | 132 |
|
140 | 133 | # evaluate password condition |
141 | 134 | extra_vars = { |
142 | | - 'words': str(words), |
143 | | - 'lower': str(lower), |
144 | | - 'upper': str(upper), |
145 | | - 'digits': str(digits), |
146 | | - 'special': str(special), |
| 135 | + "words": str(words), |
| 136 | + "lower": str(lower), |
| 137 | + "upper": str(upper), |
| 138 | + "digits": str(digits), |
| 139 | + "special": str(special), |
147 | 140 | } |
148 | 141 | ret = weechat.string_eval_expression( |
149 | | - ap_settings['password_condition'], |
| 142 | + ap_settings["password_condition"], |
150 | 143 | {}, |
151 | 144 | extra_vars, |
152 | | - {'type': 'condition'}, |
| 145 | + {"type": "condition"}, |
153 | 146 | ) |
154 | | - return ret == '1' |
| 147 | + return ret == "1" |
155 | 148 |
|
156 | 149 |
|
157 | | -def ap_input_is_password(input_text): |
| 150 | +def ap_input_is_password(input_text: str) -> bool: |
158 | 151 | """Check if input_text looks like a password.""" |
159 | | - return (ap_input_is_secured_data(input_text) |
160 | | - or ap_input_matches_condition(input_text)) |
| 152 | + return ap_input_is_secured_data(input_text) or ap_input_matches_condition( |
| 153 | + input_text |
| 154 | + ) |
161 | 155 |
|
162 | 156 |
|
163 | | -def ap_input_return_cb(data, buf, command): |
| 157 | +def ap_input_return_cb(_data: str, buf: str, _command: str) -> int: |
164 | 158 | """Callback called when Return key is pressed in a buffer.""" |
165 | 159 | try: |
166 | | - max_rejects = int(ap_settings['max_rejects']) |
| 160 | + max_rejects = int(ap_settings["max_rejects"]) |
167 | 161 | except ValueError: |
168 | 162 | max_rejects = 3 |
169 | 163 |
|
170 | | - input_text = weechat.buffer_get_string(buf, 'input') |
| 164 | + input_text = weechat.buffer_get_string(buf, "input") |
171 | 165 |
|
172 | 166 | # check if input is a command |
173 | 167 | if not weechat.string_input_for_buffer(input_text): |
174 | 168 | # commands are ignored |
175 | | - ap_reject['input'] = '' |
176 | | - ap_reject['count'] = 0 |
| 169 | + ap_reject["input"] = "" |
| 170 | + ap_reject["count"] = 0 |
177 | 171 | return weechat.WEECHAT_RC_OK |
178 | 172 |
|
179 | 173 | # check if input matches the allowed regex |
180 | | - regex = ap_settings['allowed_regex'] |
| 174 | + regex = ap_settings["allowed_regex"] |
181 | 175 | if regex and re.search(regex, input_text, re.IGNORECASE): |
182 | 176 | # allowed regex |
183 | | - ap_reject['input'] = '' |
184 | | - ap_reject['count'] = 0 |
| 177 | + ap_reject["input"] = "" |
| 178 | + ap_reject["count"] = 0 |
185 | 179 | return weechat.WEECHAT_RC_OK |
186 | 180 |
|
187 | 181 | if ap_input_is_password(input_text): |
188 | | - if ap_reject['input'] == input_text: |
189 | | - if ap_reject['count'] >= max_rejects > 0: |
| 182 | + if ap_reject["input"] == input_text: |
| 183 | + if ap_reject["count"] >= max_rejects > 0: |
190 | 184 | # it looks like a password but send anyway after N rejects |
191 | | - ap_reject['input'] = '' |
192 | | - ap_reject['count'] = 0 |
| 185 | + ap_reject["input"] = "" |
| 186 | + ap_reject["count"] = 0 |
193 | 187 | return weechat.WEECHAT_RC_OK |
194 | | - ap_reject['count'] += 1 |
| 188 | + ap_reject["count"] += 1 |
195 | 189 | else: |
196 | | - ap_reject['input'] = input_text |
197 | | - ap_reject['count'] = 1 |
| 190 | + ap_reject["input"] = input_text |
| 191 | + ap_reject["count"] = 1 |
198 | 192 | # password detected, do NOT send it to the buffer! |
199 | 193 | return weechat.WEECHAT_RC_OK_EAT |
200 | 194 |
|
201 | 195 | # not a password |
202 | | - ap_reject['input'] = '' |
203 | | - ap_reject['count'] = 0 |
| 196 | + ap_reject["input"] = "" |
| 197 | + ap_reject["count"] = 0 |
204 | 198 | return weechat.WEECHAT_RC_OK |
205 | 199 |
|
206 | 200 |
|
207 | | -def main(): |
| 201 | +def main() -> None: |
208 | 202 | """Main function.""" |
209 | | - if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, |
210 | | - SCRIPT_LICENSE, SCRIPT_DESC, '', ''): |
| 203 | + if weechat.register( |
| 204 | + SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "" |
| 205 | + ): |
211 | 206 | # set default settings |
212 | | - for name, option in ap_settings_default.items(): |
| 207 | + for name, option in AP_SETTINGS_DEFAULT.items(): |
213 | 208 | if weechat.config_is_set_plugin(name): |
214 | 209 | ap_settings[name] = weechat.config_get_plugin(name) |
215 | 210 | else: |
216 | | - weechat.config_set_plugin(name, option['default']) |
217 | | - ap_settings[name] = option['default'] |
| 211 | + weechat.config_set_plugin(name, option["default"]) |
| 212 | + ap_settings[name] = option["default"] |
218 | 213 | weechat.config_set_desc_plugin( |
219 | | - name, |
220 | | - '%s (default: "%s")' % (option['help'], option['default'])) |
| 214 | + name, f'{option["help"]} (default: "{option["default"]}")' |
| 215 | + ) |
221 | 216 |
|
222 | 217 | # detect config changes |
223 | | - weechat.hook_config('plugins.var.python.%s.*' % SCRIPT_NAME, |
224 | | - 'ap_config_cb', '') |
| 218 | + weechat.hook_config(f"plugins.var.python.{SCRIPT_NAME}.*", "ap_config_cb", "") |
225 | 219 |
|
226 | 220 | # hook Return key |
227 | | - weechat.hook_command_run('/input return', 'ap_input_return_cb', '') |
| 221 | + weechat.hook_command_run("/input return", "ap_input_return_cb", "") |
228 | 222 |
|
229 | 223 |
|
230 | | -if __name__ == '__main__' and IMPORT_OK: |
| 224 | +if __name__ == "__main__" and IMPORT_OK: |
231 | 225 | main() |
0 commit comments