Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1'''
2Relay Filter List
3=================
5Parse a relay filter list file and decide whether or not we should measure a
6relay based on its fingerprint.
8Files are line and word based and read left-to-right, top-to-bottom. First word
9to match a given fingerprint wins.
11Syntax
12------
14Everything after a ``#`` is a comment:
16.. code-block:: text
18 # This is a comment
19 this is not a comment # but this is
20 other stuff
22A word starting with ``!`` is a negative match, meaning that if the rest of
23the word matches, then the relay should **not** be measured. Normally a match
24means *yes, do measure*.
26.. code-block:: text
28 !DoNotMeasureThisFP
29 DoMeasureThisFP
31A word containing a ``*`` is a wildcard word, meaning it matches all
32fingerprints.
34.. code-block:: text
36 * # means measure all relays
37 !* # means do not measure any relay
39Relay fingerprints are the only other valid non-comment text that should be in
40this type of file.
42.. warning::
43 You may find that non-fingerprint text is parsed as fingerprints if they
44 are not in a comment. Be careful. Don't do this.
46You can have multiple words/fingerprints per line. These snippets are parsed
47the exactly same way. They demonstrate a config where 3 relays have opted-in to
48being measured and no other relay should be measured.
50.. code-block:: text
52 # First
53 RelayFP1
54 RelayFP2
55 RelayFP3
56 !*
58 # Second
59 RelayFP1 RelayFP2 RelayFP3
60 !*
62 # Third
63 RelayFP1 RelayFP2 RelayFP3 !*
65 RelayFP1 RelayFP2 RelayFP3 !* # Forth
68Examples
69--------
71.. note::
72 In these examples, pretend that relay fingerprints are for alphanumeric
73 characters.
75Do not measure any relay, ever::
77 !*
79Measure all relays except one::
81 !FFFF
82 *
84Only measure one relay::
86 AAAA !*
88Maybe two people have opted in to be measured and you want to organize their
89relays by their families::
91 # Jacob's relays
92 AAAA BBBB CCCC
93 DDDD EEEE FFFF
95 # Paul's relays
96 PPPP1 PPPP2 PPPP3
97 # Paul said we shouldn't measure this one
98 !PPPP4
100 !*
102'''
103from typing import List
106def normalize_fp(s: str) -> str:
107 ''' Normalize a fingerprint so no matter the format in which it is
108 received, it will be in a consistent format for later comparisons to
109 work.
111 Currently this function is used for things that may not be exactly a
112 fingerprint: comment lines, and wildcard words are two examples. Don't edit
113 this function without verifying this is no longer the case or that what you
114 want to do won't break those other things.
115 '''
116 return s.strip().lower()
119class RFLWord:
120 ''' A single word read from a file. You should not not need to use this
121 directly. '''
122 #: Whether this is a negative-match word or not. If ``True``, then if the
123 #: fingerprint matches, the relay should **NOT** be measured.
124 is_negative: bool = False
125 #: Whether this is a wildcard word or not. If ``True``, then this word
126 #: matches all fingerprints.
127 is_wildcard: bool = False
129 def __init__(self, s: str):
130 # Perhaps a redundant action, but better safe than sorry.
131 s = normalize_fp(s)
132 assert not s.startswith('#')
133 # Determine if this is a negative-match word, and remove the '!' if
134 # needed
135 self.is_negative = False
136 if s.startswith('!'):
137 self.is_negative = True
138 s = s[1:]
139 # Determine if this is a wildcard word, and remove the '*' if needed
140 self.is_wildcard = False
141 if s.startswith('*'):
142 self.is_wildcard = True
143 s = s[1:]
144 # Save what remains of the word
145 self.s = s
147 def matches(self, s: str) -> bool:
148 ''' Determine if the given string matches this :class:`RFLWord` '''
149 if self.is_wildcard:
150 return True
151 return normalize_fp(s) == self.s
154class RelayFilterList:
155 #: Ordered list of word we read from the file
156 words: List[RFLWord]
158 def __init__(self):
159 self.words = []
161 @staticmethod
162 def from_str(s: str) -> 'RelayFilterList':
163 ''' Given the entire string contents of a file, return a new
164 :class:`RelayFilterList`
165 '''
166 filter_list = RelayFilterList()
167 for line in s.split('\n'):
168 line = normalize_fp(line)
169 # Remove both entire comment lines and end-of-line comments.
170 if '#' in line:
171 line = line[:line.find('#')]
172 assert '#' not in line
173 # Non-comment line. If empty, nothing to do
174 if not len(line):
175 continue
176 # To support multiple fingerprints per line, act on each word
177 # individually
178 for word in line.split():
179 filter_list.words.append(RFLWord(normalize_fp(word)))
180 return filter_list
182 def should_measure(self, fp: str, default: bool) -> bool:
183 ''' Determine whether or not the given ``fp`` should be measured. If no
184 match is found, then return ``default`` '''
185 for word in self.words:
186 if word.matches(fp):
187 return not word.is_negative
188 return default