├── .dockerignore
├── .editorconfig
├── .gitignore
├── .php_cs.php
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── assets
├── ONB.csv
├── cellular.php
├── countrycodes.php
└── washing.png
├── composer.json
├── config.example.php
├── docker-compose.yml
├── fbcallrouter
├── fbcallrouter.service
└── src
├── ConfigTrait.php
├── RunCommand.php
├── callrouter
├── callmonitor.php
├── callrouter.php
├── dialercheck.php
├── infomail.php
├── logging.php
└── phonetools.php
└── functions.php
/.dockerignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .dockerignore
3 | .editorconfig
4 | .gitignore
5 | .php_cs.php
6 | .travis.yml
7 | config.php
8 | Dockerfile
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # This file is for unifying the coding style for different editors and IDEs
2 | # editorconfig.org
3 |
4 | # PHP PSR-2 Coding Standards
5 | # http://www.php-fig.org/psr/psr-2/
6 |
7 | root = true
8 |
9 | [*.php]
10 | charset = utf-8
11 | end_of_line = lf
12 | insert_final_newline = true
13 | trim_trailing_whitespace = true
14 | indent_style = space
15 | indent_size = 4
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /composer.lock
2 | /config.php
3 | /vendor/
4 | /assets/phonebookerror.jpg
5 | /assets/phonebckgrnd.jpg
6 | /.vscode/launch.json
7 | /.vscode/settings.json
8 |
--------------------------------------------------------------------------------
/.php_cs.php:
--------------------------------------------------------------------------------
1 | in('src')
5 | ;
6 |
7 | return PhpCsFixer\Config::create()
8 | ->setRules([
9 | '@PSR2' => true,
10 | 'array_syntax' => ['syntax' => 'short'],
11 | ])
12 | ->setFinder($finder)
13 | ->setUsingCache(false)
14 | ;
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: php
4 |
5 | cache:
6 | directories:
7 | - $HOME/.composer/cache
8 |
9 | install:
10 | - composer install
11 |
12 | matrix:
13 | fast_finish: true
14 |
15 | jobs:
16 | include:
17 | - stage: Test
18 | php: 7.1
19 | script:
20 | phpunit
21 | - stage: Static Analysis
22 | php: 7.1
23 | install:
24 | - rm -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini
25 | - composer require --dev "phpstan/phpstan"
26 | script:
27 | # Static analyzer check
28 | - ./vendor/bin/phpstan analyze --level=5 --no-progress src
29 | - stage: Code Style
30 | php: 7.1
31 | install:
32 | - rm -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini
33 | - composer require --dev "friendsofphp/php-cs-fixer:^2.8"
34 | script:
35 | # Check the code style
36 | - IFS=$'\n'; COMMIT_SCA_FILES=($(git diff --name-only --diff-filter=ACMRTUXB "${TRAVIS_COMMIT_RANGE}")); unset IFS
37 | - ./vendor/bin/php-cs-fixer fix --config=.php_cs.php -v --dry-run --diff --stop-on-violation --using-cache=no --path-mode=intersection -- "${COMMIT_SCA_FILES[@]}"
38 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:8.1-cli AS base
2 | WORKDIR /app
3 |
4 | RUN apt-get update && \
5 | apt-get install -y \
6 | libonig-dev \
7 | libxml2-dev \
8 | libcurl4-openssl-dev && \
9 | apt-get purge -y --autoremove
10 | RUN docker-php-ext-install -j$(nproc) \
11 | curl \
12 | mbstring \
13 | soap \
14 | sockets \
15 | xml
16 |
17 | FROM base AS build
18 |
19 | RUN apt-get update && \
20 | apt-get install -y \
21 | wget \
22 | unzip
23 |
24 | COPY composer.json .
25 | # COPY composer.lock .
26 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
27 | RUN composer install --no-dev --no-scripts
28 |
29 | COPY . .
30 | RUN composer dumpautoload --optimize
31 |
32 | RUN wget -O vorwahlen.zip "https://www.bundesnetzagentur.de/SharedDocs/Downloads/DE/Sachgebiete/Telekommunikation/Unternehmen_Institutionen/Nummerierung/Rufnummern/ONRufnr/Vorwahlverzeichnis_ONB.zip.zip?__blob=publicationFile&v=298" && \
33 | unzip vorwahlen.zip && \
34 | mv *.ONB.csv assets/ONB.csv && \
35 | rm vorwahlen.zip
36 |
37 | FROM base AS final
38 | ENV DOCKER_CONTAINER=true
39 |
40 | COPY --from=build /app /app
41 |
42 | CMD ["php", "fbcallrouter", "run"]
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Black Senator
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # An extended call routing for AVM FRITZ!Box
2 |
3 |
4 |
5 | The program is **trying to identify spam calls**. So it is listen to the [FRITZ!Box](#disclaimer) call monitor and does several wash cycles to figure out whether it is spam or not.
6 |
7 | **Please remember to update the [master data](#master-data) regularly so that the program works correctly!**
8 |
9 | ## Release notes
10 |
11 | If you already use an older version please refer to the [update section](#update) and be aware that the **structure of the configuration file differ** (see [`config.example.php`](/config.example.php)).
12 |
13 | ## Preconditions
14 |
15 | * the program only works in the German telephone network!
16 | * you use a Fritz!Box for landline telephoning
17 | * you need to have a [separate spam phone book](https://avm.de/service/wissensdatenbank/dok/FRITZ-Box-7590/142_Rufsperren-fur-ankommende-und-ausgehende-Anrufe-z-B-0900-Nummern-einrichten/) beside your phone book!
18 | * you have a microcomputer (e.g. Raspberry Pi) running 24/7 in your network
19 |
20 | ## Description
21 |
22 | For an incoming call a cascaded check takes place:
23 |
24 | * First, it is checked, whether the number is **already known** in your telephone books (`'whitelist'`) and (`'blacklist'`) or (`'newlist'`). This also includes [central numbers](https://github.com/blacksenator/carddav2fb/wiki/Rufnummern-mit-wildcards) used, whose various extensions are defined with '*' in the phone book. Of course, all these known telephone numbers are not analyzed any further.
25 |
26 | * If unknown, it is checked if it is a **foreign number**. If you have set (`'blockForeign'`) the number will be direct transferred to the corresponding phone book (`'blacklist'`) for future rejections. If not, then foreign numbers are
27 | * screened to see whether they exceed or fall short of the expected lengths,
28 | * whether they transmit country codes that are not (or no longer) used or
29 | * if area code starts with invalid "0" (zero).
30 |
31 | * The following screenings are carried out for **domestic numbers**, which are essentially aimed at "CLIP - no screening":
32 | * transmission of a [**unvalid area code** ONB](#onb) or [cellular code](#rnb)
33 | * unvalid subscribers number, if a landline number starts with an zero
34 | * concealment of a mobile phone number using a prefix consisting of your own area code and country code,
35 | * transmission of the actual phone number in brackets after a user provided number and
36 | * falling below or exceeding expected lengths.
37 |
38 | * If all this passed, it is checked at various scoring sites (currently five: [WERRUFT](https://www.werruft.info), [Clever Dialer](https://www.cleverdialer.de), [Telefonspion](https://www.telefonspion.de/), [WerHatAngerufen](https://www.werhatangerufen.com) and [tellows](https://www.tellows.de/)), if this number has received a **bad online rating**. If this is the case, the number - also as with pattern errors - is included in the blacklist.
39 |
40 | * Finally, of course, there is the possibility that the caller is known in a positive sense and can be identified via a public telephone book ([Das Örtliche](https://www.dasoertliche.de/rueckwaertssuche/)). Then he/she/it is optionally entered in a dedicated phone book (`'newlist'`) with the determined name.
41 | **This feature is also used for outgoing calls to unknown numbers!**
42 |
43 | * In addition, the program offers the option of being **informed** about unknown incoming (and outgoing) phone numbers **by email**. In particular, the information on the extent to which the caller could be identified and possibly added to one of the telephone books is convinient without having to be able to trace this using the caller list in the FRITZ!Box or in the logging file.
44 |
45 | ## Requirements
46 |
47 | * PHP >= 7.3 (php-cli, php-curl, php-mbstring, php-soap, php-xml)
48 | * call monitor (port 1012) is open - if not: dial `#96*5*` to open it
49 | * Composer (follow the [installation guide](https://getcomposer.org/download/))
50 |
51 | ## Installation
52 |
53 | ### Programm
54 |
55 | Install requirements are:
56 |
57 | ```console
58 | git clone https://github.com/blacksenator/fbcallrouter.git
59 | cd fbcallrouter
60 | ```
61 |
62 | Install composer (see for newer instructions):
63 |
64 | ```console
65 | composer install --no-dev
66 | ```
67 |
68 | [Edit](#configuration) the `config.example.php` and save as `config.php` or use an other name of your choice (but than keep in mind to use the -c option to define your renamed file)
69 |
70 | ### Preconditions on the FRITZ!Box
71 |
72 | #### Phone book for rejections (bad apples)
73 |
74 | If you do not have your own phone book for spam numbers, it is essential to add a new one (e.g. "Spamnummern"). Note that the first phone book ("Telefonbuch") has the index "0" and the numbers are ascending according to the index tabs.
75 | Then you have to [link this phone book for call handling](https://avm.de/service/wissensdatenbank/dok/FRITZ-Box-7590/142_Rufsperren-fur-ankommende-und-ausgehende-Anrufe-z-B-0900-Nummern-einrichten/).
76 |
77 | #### Phone book for trustworthy new numbers
78 |
79 | If you want to add previously unknown but trustworthy umbers as contacts, you can do this optionally: e.g. in a separate phone book that you have created in the FRITZ!Box. However, you can also have these additions added to your standard telephone book..
80 |
81 | #### FRITZ!Box user
82 |
83 | The programm accessed the Fritz!Box via TR-064. Make sure that the user you choose in configuration (see [next topic](#configuration)) is granted for this interface. The user needs authorization for "voice messages, fax messages, FRITZ!App Fon and the call list"!
84 |
85 | ### Configuration
86 |
87 | The configuration file (default `config.php`) offers various customization options. For example: Adapted to the tellows rating model a bad score is determined as six to nine and needs at least more than three comments. The second parameter is for quality purposes: not a single opinion should block a service provider whose call you might expect.
88 | You can adapt the values in the configuration file. **But be carefull! If you choose score values equal or smaler then five (5), than numbers with impeccable reputation where written to your rejection list!**
89 | Because the rating websites use different scales, they are normalized to the tellows scale.
90 |
91 | If you set `'log'` in your configuration and the `'logPath'` is valid, the essential process steps for verification are written to the log file `callrouter_logging.txt`. If `'logPath'` is empty the programm directory is default.
92 |
93 | ## Usage
94 |
95 | As mentioned, the program is designed to run permanently in the background. In order to ensure this, it is necessary to make sure that this is possible without interruption and should be tested accordingly.
96 |
97 | ### Test
98 |
99 | It is highly recommended test the setting by following the next steps:
100 |
101 | #### 1. Function test
102 |
103 | ```console
104 | php fbcallrouter run
105 | ```
106 |
107 | If `On guard...` appears, the program is armed.
108 |
109 | Make a function test in which you call your landline from your cell phone: your mobile phone number **should not** end up in the spam phone book!
110 | If the number is in your phone book, nothing should happen anyway (on whitelist).
111 | Otherwise: your mobile phone number is not a foreign number, the [ONB](#onb)/[RNB](#rnb) is correct and you certainly do not have a bad entry in online directories. Therefore these tests should not lead to any sorting out.
112 | Press `CTRL+C` to terminate the programm.
113 |
114 | If logging is enabled (which is highly recommended for this test), than `nano callrouter_logging.txt` will show you what happend at the call monitor interface.
115 |
116 | #### 2. Integration test
117 |
118 | There are five exemplary `'numbers'` stored in the configuration file with which you can test the wash cycles integratively. You can change these test numbers according to your own ideas and quantity. Starting the programm with the `-t` option, these numbers will be injected as substitutes one after the other for the next calls the FRITZ!Box receives and its call monitor port will broadcast.
119 |
120 | It is highly recommended to proceed like this:
121 |
122 | 1. check if none of the substitutes are already in your phone book! **Especially if you repeat the test!**
123 |
124 | 2. if you want to a web query, make crosscheck at the named providers that the choosen test phone number(s) actually **has/ve the desired score and comments!**
125 |
126 | 3. start the programm
127 |
128 | ```console
129 | php fbcallrouter run -t
130 | ```
131 |
132 | 4. use your cellular phone to call your landline. The incoming mobil number will be replaced with the first/next number from this array and passes through the inspection process. Additional information is output like this:
133 |
134 | ```console
135 | Starting FRITZ!Box call router...
136 | On guard...
137 | Running test case 1/5
138 | ```
139 |
140 | 5. let it **ring at least twice** before hanging up. Repeat calling from your cellular your landline number. So you have to call as many times as there are numbers in the array to check all test cases (or quit programm execution with `CTRL+C`). The program then ends this number replacement.
141 |
142 | 6. check the blacklist phone book whether all numbers have been entered as expected. If logging is enabled, than `nano callrouter_logging.txt` will show you what happend. If e-mail notification is choosen check your inbox.
143 | To cancel, press `CTRL+C`.
144 |
145 | ### 3. Permanent background processing
146 |
147 | The **main concept** of this tool is to **continuously check incoming calls**. Therefore it should ideally run permanently as a background process on a single-board computer (e.g. Raspberry Pi) in the home network. A corresponding definition file is prepared: [`fbcallrouter.service`](/fbcallrouter.service)
148 |
149 | 1. edit `[youruser]` in this file with your device user (e.g. `pi`) and save the file
150 |
151 | ```console
152 | nano fbcallrouter.service
153 | ```
154 |
155 | 2. copy the file into `/etc/systemd/system`
156 |
157 | ```console
158 | sudo cp fbcallrouter.service /etc/systemd/system/fbcallrouter.service
159 | ```
160 |
161 | 3. enable the service unit:
162 |
163 | ```console
164 | sudo systemctl enable fbcallrouter.service
165 | ```
166 |
167 | 4. check the status:
168 |
169 | ```console
170 | sudo systemctl status fbcallrouter.service
171 | ```
172 |
173 | ## Update
174 |
175 | As noted at the beginning, functional enhancements usually go hand in hand with additions to the configuration file. To install the current version, please proceed as follows:
176 |
177 | 1. change to the installation directory:
178 |
179 | ```console
180 | cd /home/[youruser]/fbcallrouter
181 | ```
182 |
183 | 2. stop the service:
184 |
185 | ```console
186 | sudo systemctl stop fbcallrouter.service
187 | ```
188 |
189 | 3. delete the old logging file (if you used it here):
190 |
191 | ```console
192 | rm callrouter_logging.txt
193 | ```
194 |
195 | 4. get the latest version from:
196 |
197 | ```console
198 | git pull https://github.com/blacksenator/fbcallrouter.git
199 | ```
200 |
201 | 5. bring all used libraries up to date:
202 |
203 | ```console
204 | composer update --no-dev
205 | ```
206 |
207 | 6. check for changes in the configuration file...
208 |
209 | ```console
210 | nano config.example.php
211 | ```
212 |
213 | 7. ...and eventually make necessary changes/additions in your configuration:
214 |
215 | ```console
216 | nano config.example.php
217 | ```
218 |
219 | 8. restart the service...
220 |
221 | ```console
222 | sudo systemctl start fbcallrouter.service
223 | ```
224 |
225 | 9. ...and wait few seconds before you check if the service is running:
226 |
227 | ```console
228 | sudo systemctl status fbcallrouter.service
229 | ```
230 |
231 | ### Master data
232 |
233 | #### ONB
234 |
235 | ONB = OrtsNetzBereiche (Vorwahlbereiche/Vorwahlen). The list used comes from the [BNetzA](https://www.bundesnetzagentur.de/DE/Fachthemen/Telekommunikation/Nummerierung/ONRufnr/ortsnetze_node.html) and should be valid for a limited period of time. If you want to update them, then download the offered **CSV file** [Vorwahlverzeichnis](https://www.bundesnetzagentur.de/SharedDocs/Downloads/DE/Sachgebiete/Telekommunikation/Unternehmen_Institutionen/Nummerierung/Rufnummern/ONRufnr/Vorwahlverzeichnis_ONB.zip.zip?__blob=publicationFile&v=298). Unpack the archive and save the file as `ONB.csv` in the `./assets` directory.
236 |
237 | #### RNB
238 |
239 | RNB = numbers for mobile services. The BNetzA do not provide a list for download with cellular codes [(RNB)](https://www.bundesnetzagentur.de/DE/Fachthemen/Telekommunikation/Nummerierung/MobileDienste/start.html). The currently used ones were transferred to the `./assets` directory as `$cellularNumber` in `cellular.php`.
240 |
241 | #### Country codes
242 |
243 | The currently used ones were transferred to the `./assets` directory as `$countryCode` in `countrycode.php`. The list was mainly compiled from information from [wikipedia](https://de.wikipedia.org/wiki/L%C3%A4ndervorwahlliste_sortiert_nach_Nummern).
244 |
245 | ## Does the programm works propperly? Troubleshooting
246 |
247 | First of all: of course the program can contain bugs and not work as expected. If you are convinced, please open an issue here.
248 | In addition, it can of course be more due to the selected settings for the configuration of the `filter`.
249 |
250 | Last but not least I myself have made some observations that led me to suspect that the program would not work correctly. In principle, it is advisable in such cases to **switch on logging** (`'log' => true,`) or to compare the active logging with the call list of the FRITZ!Box. You may need to go a step further and correlate dates with reverse search sites.
251 |
252 | An example:
253 | I had calls that should have been identified as spam at first glance through web research. A closer look showed that this spammer only appeared in the lists at exactly that time and had not yet received a sufficient number of negative comments at the time of the call.
254 |
255 | ## Privacy
256 |
257 | No private data from this software will be passed on to third parties accepts with this exception:
258 | when using
259 |
260 | * [WERRUFT](https://www.werruft.info/bedingungen/),
261 | * [Clever Dialer](https://www.cleverdialer.de/datenschutzerklaerung-website)
262 | * [Telefonspion](https://www.telefonspion.de/datenschutz.php)
263 | * [WerHatAngerufen](https://www.werhatangerufen.com/terms)
264 | * [tellows](https://www.tellows.de/c/about-tellows-de/datenschutz/)
265 | * [Das Örtliche](https://www.dasoertliche.de/datenschutz)
266 |
267 | the incoming number is transmitted to the provider. Their data protection information must be observed!
268 |
269 | ## Feedback
270 |
271 | If you enjoy this software, then I would be happy to receive your feedback, also in the form of user comments and descriptions of your experiences, e.g. in the [IP Phone Forum](https://www.ip-phone-forum.de/threads/fbcallrouter-extended-call-routing-spam-filter-for-fritz-box.303211/). This puts the user community on a broader basis and their experiences and functional ideas can be incorporated into further development. In the end, these features will benefit everyone.
272 |
273 | ## Improvements
274 |
275 | As ever, this program [started with only a few lines of code](https://www.ip-phone-forum.de/threads/howto-werbeanrufe-automatisch-beenden-lassen-mit-freetz.298448/post-2309645) just to play with the call monitor interface provided and to figure out how it works...
276 |
277 | ...than more and more ideas came to my mind how this interface could solve some of my needs.
278 |
279 | As I have already written in the [fritzsoap documentation](https://github.com/blacksenator/fritzsoap#wishes), it would be an enormous relief **if AVM would provide functionality to terminate incoming calls**, just as FRITZ!OS itself does it with the handling of phone numbers to be blocked. The entry of more and more numbers in corresponding blacklists could be omitted or reduced.
280 |
281 | ## Disclaimer
282 |
283 | FRITZ!Box, FRITZ!fon, FRITZ!OS are trademarks of [AVM](https://avm.de/). This software is **in no way affiliated** with AVM and only uses the [interfaces published by them](https://avm.de/service/schnittstellen/).
284 |
285 | ## License
286 |
287 | This script is released under MIT license.
288 |
289 | ## Author
290 |
291 | Copyright© 2019 - 2024 Volker Püschel
292 |
293 |
294 | [def]: #master-data
--------------------------------------------------------------------------------
/assets/cellular.php:
--------------------------------------------------------------------------------
1 | 'Tismi BV',
16 | '15020' => 'Legos',
17 | // '151' => 'Telekom', reserviert
18 | '1511' => 'Telekom',
19 | '1512' => 'Telekom',
20 | '1514' => 'Telekom',
21 | '1515' => 'Telekom',
22 | '1516' => 'Telekom',
23 | '1517' => 'Telekom',
24 | '15180' => 'Telekom',
25 | '15181' => 'Telekom',
26 | // '152' => 'Vodafone', reserviert
27 | '1520' => 'Vodafone',
28 | '1521' => 'Lycamobile',
29 | '1522' => 'Vodafone',
30 | '1523' => 'Vodafone',
31 | '1525' => 'Vodafone',
32 | '1526' => 'Vodafone',
33 | '1529' => 'Vodafone',
34 | '15310' => 'MTEL',
35 | '15510' => 'Lebara',
36 | '15511' => 'Lebara',
37 | '15560' => 'Lebara',
38 | '15561' => 'Lebara',
39 | '15562' => '1&1',
40 | '15563' => '1&1',
41 | '15564' => '1&1',
42 | '15565' => '1&1',
43 | '15566' => '1&1',
44 | '15567' => '1&1',
45 | '15568' => '1&1',
46 | '15569' => '1&1',
47 | '15630' => 'multiConnect',
48 | '15678' => 'Argon',
49 | '15679' => 'Argon',
50 | // '157' => 'E-Plus', reserviert
51 | '15700' => 'Telefónica',
52 | '15701' => 'Telefónica',
53 | '15702' => 'Telefónica',
54 | '15703' => 'Telefónica',
55 | '15704' => 'Telefónica',
56 | '15706' => 'Telefónica',
57 | '1573' => 'Telefónica',
58 | '1575' => 'Telefónica',
59 | '1577' => 'Telefónica',
60 | '1578' => 'Telefónica',
61 | '1579' => 'Telefónica/SipGate',
62 | '15888' => 'TelcoVillage',
63 | // '159' => 'Telefónica', reserviert
64 | '1590' => 'Telefónica',
65 | '160' => 'Telekom',
66 | '162' => 'Vodafone',
67 | '163' => 'Telefónica',
68 | '170' => 'Telekom',
69 | '171' => 'Telekom',
70 | '172' => 'Vodafone',
71 | '173' => 'Vodafone',
72 | '174' => 'Vodafone',
73 | '175' => 'Telekom',
74 | '176' => 'Telefónica',
75 | '177' => 'Telefónica',
76 | '178' => 'Telefónica',
77 | '179' => 'Telefónica',
78 | ];
79 |
--------------------------------------------------------------------------------
/assets/countrycodes.php:
--------------------------------------------------------------------------------
1 | 'USA',
16 | '1204' => 'Kanada',
17 | '1226' => 'Kanada',
18 | '1236' => 'Kanada',
19 | '1242' => 'Bahamas',
20 | '1246' => 'Barbados',
21 | '1249' => 'Kanada',
22 | '1250' => 'Kanada',
23 | '1264' => 'Anguilla',
24 | '1268' => 'Antigua/Barbuda',
25 | '1284' => 'Britische Jungferninseln',
26 | '1289' => 'Kanada',
27 | '1306' => 'Kanada',
28 | '1340' => 'Amerikanische Jungferninseln',
29 | '1343' => 'Kanada',
30 | '1345' => 'Kaimaninseln',
31 | '1365' => 'Kanada',
32 | '1403' => 'Kanada',
33 | '1416' => 'Kanada',
34 | '1418' => 'Kanada',
35 | '1431' => 'Kanada',
36 | '1437' => 'Kanada',
37 | '1438' => 'Kanada',
38 | '1441' => 'Bermuda',
39 | '1450' => 'Kanada',
40 | '1473' => 'Grenada',
41 | '1506' => 'Kanada',
42 | '1514' => 'Kanada',
43 | '1519' => 'Kanada',
44 | '1579' => 'Kanada',
45 | '1581' => 'Kanada',
46 | '1587' => 'Kanada',
47 | '1604' => 'Kanada',
48 | '1613' => 'Kanada',
49 | '1639' => 'Kanada',
50 | '1647' => 'Kanada',
51 | '1649' => 'Turks-/Caicosinseln',
52 | '1664' => 'Montserrat',
53 | '1670' => 'Nördliche Marianen',
54 | '1671' => 'Guam',
55 | '1684' => 'Amerikanisch-Samoa',
56 | '1705' => 'Kanada',
57 | '1709' => 'Kanada',
58 | '1721' => 'Sint Maarten',
59 | '1758' => 'St. Lucia',
60 | '1767' => 'Dominica',
61 | '1778' => 'Kanada',
62 | '1780' => 'Kanada',
63 | '1782' => 'Kanada',
64 | '1784' => 'St. Vincent/Grenadinen',
65 | '1807' => 'Kanada',
66 | '1808' => 'Midwayinseln',
67 | '1819' => 'Kanada',
68 | '1829' => 'Dominikanische Republik',
69 | '1867' => 'Kanada',
70 | '1868' => 'Trinidad/Tobago',
71 | '1869' => 'St. Kitts/Nevis',
72 | '1873' => 'Kanada',
73 | '1876' => 'Jamaika',
74 | '1902' => 'Kanada',
75 | '1905' => 'Kanada',
76 | '1939' => 'Puerto Rico',
77 | '20' => 'Ägypten',
78 | '211' => 'Südsudan',
79 | '212' => 'Marokko',
80 | '213' => 'Algerien',
81 | '216' => 'Tunesien',
82 | '218' => 'Libyen',
83 | '220' => 'Gambia',
84 | '221' => 'Senegal',
85 | '222' => 'Mauretanien',
86 | '223' => 'Mali',
87 | '224' => 'Guinea',
88 | '225' => 'Côte d‘Ivoire',
89 | '226' => 'Burkina Faso (Obervolta)',
90 | '227' => 'Niger',
91 | '228' => 'Togo',
92 | '229' => 'Benin',
93 | '230' => 'Mauritius',
94 | '231' => 'Liberia',
95 | '232' => 'Sierra Leone',
96 | '233' => 'Ghana',
97 | '234' => 'Nigeria',
98 | '235' => 'Tschad',
99 | '236' => 'Zentralafrikanische Republik',
100 | '237' => 'Kamerun',
101 | '238' => 'Kap Verde',
102 | '239' => 'São Tomé/Príncipe',
103 | '240' => 'Äquatorialguinea',
104 | '241' => 'Gabun',
105 | '242' => 'Republik Kongo (Brazzaville)',
106 | '243' => 'Kongo',
107 | '244' => 'Angola',
108 | '245' => 'Guinea-Bissau',
109 | '246' => 'Chagos-Archipel (Diego-Garcia)',
110 | '247' => 'Ascension',
111 | '248' => 'Seychellen',
112 | '249' => 'Sudan',
113 | '250' => 'Ruanda',
114 | '251' => 'Äthiopien',
115 | '252' => 'Somalia',
116 | '253' => 'Dschibuti',
117 | '254' => 'Kenia',
118 | '255' => 'Tansania',
119 | '256' => 'Uganda',
120 | '257' => 'Burundi',
121 | '258' => 'Mosambik',
122 | '260' => 'Sambia',
123 | '261' => 'Madagaskar',
124 | '262' => 'Réunion und Mayotte',
125 | '263' => 'Simbabwe',
126 | '264' => 'Namibia',
127 | '265' => 'Malawi',
128 | '266' => 'Lesotho',
129 | '267' => 'Botswana',
130 | '268' => 'Swasiland',
131 | '269' => 'Komoren',
132 | '27' => 'Südafrika',
133 | '290' => 'St. Helena/Tristan da Cunha',
134 | '291' => 'Eritrea',
135 | '297' => 'Aruba',
136 | '298' => 'Färöer,',
137 | '299' => 'Grönland,',
138 | '30' => 'Griechenland',
139 | '31' => 'Niederlande',
140 | '32' => 'Belgien',
141 | '33' => 'Frankreich',
142 | '34' => 'Spanien',
143 | '350' => 'Gibraltar',
144 | '351' => 'Portugal',
145 | '352' => 'Luxemburg',
146 | '353' => 'Irland',
147 | '354' => 'Island',
148 | '355' => 'Albanien',
149 | '356' => 'Malta',
150 | '357' => 'Zypern',
151 | '358' => 'Finnland',
152 | '359' => 'Bulgarien',
153 | '36' => 'Ungarn',
154 | '370' => 'Litauen',
155 | '371' => 'Lettland',
156 | '372' => 'Estland',
157 | '373' => 'Moldawien',
158 | '374' => 'Armenien',
159 | '375' => 'Weißrussland',
160 | '376' => 'Andorra',
161 | '377' => 'Monaco',
162 | '378' => 'San Marino',
163 | '379' => 'Vatikanstadt',
164 | '380' => 'Ukraine', // stand with Ukraine
165 | '381' => 'Serbien',
166 | '382' => 'Montenegro',
167 | '383' => 'Kosovo',
168 | '385' => 'Kroatien',
169 | '386' => 'Slowenien',
170 | '387' => 'Bosnien Herzegowina',
171 | '389' => 'Mazedonien',
172 | '39' => 'Italien',
173 | '40' => 'Rumänien',
174 | '41' => 'Schweiz',
175 | '420' => 'Tschechien',
176 | '421' => 'Slowakei',
177 | '423' => 'Liechtenstein',
178 | '43' => 'Österreich',
179 | '44' => 'Vereinigtes Königreich/Isle of Man/Kanalinseln',
180 | '45' => 'Dänemark',
181 | '46' => 'Schweden',
182 | '47' => 'Norwegen',
183 | '48' => 'Polen',
184 | '49' => 'Deutschland',
185 | '500' => 'Falklandinseln',
186 | '501' => 'Belize',
187 | '502' => 'Guatemala',
188 | '503' => 'El Salvador',
189 | '504' => 'Honduras',
190 | '505' => 'Nicaragua',
191 | '506' => 'Costa Rica',
192 | '507' => 'Panama',
193 | '508' => 'Saint-Pierre und Miquelon',
194 | '509' => 'Haiti',
195 | '51' => 'Peru',
196 | '52' => 'Mexiko',
197 | '53' => 'Kuba',
198 | '54' => 'Argentinien',
199 | '55' => 'Brasilien',
200 | '56' => 'Chile',
201 | '57' => 'Kolumbien',
202 | '58' => 'Venezuela',
203 | '590' => 'Guadeloupe',
204 | '591' => 'Bolivien',
205 | '592' => 'Guyana',
206 | '593' => 'Ecuador',
207 | '594' => 'Französisch-Guayana',
208 | '595' => 'Paraguay',
209 | '596' => 'Martinique',
210 | '597' => 'Suriname',
211 | '598' => 'Uruguay',
212 | '599' => 'Bonaire/Curaçao/Sint Marteen/Saba/Sint Eustatius',
213 | '60' => 'Malaysia',
214 | '61' => 'Australien',
215 | '62' => 'Indonesien',
216 | '63' => 'Philippinen',
217 | '64' => 'Neuseeland',
218 | '65' => 'Singapur',
219 | '66' => 'Thailand',
220 | '670' => 'Osttimor',
221 | '672' => 'Antarktis/Norfolkinsel',
222 | '673' => 'Brunei',
223 | '674' => 'Nauru',
224 | '675' => 'Papua-Neuguinea',
225 | '676' => 'Tonga',
226 | '677' => 'Salomonen',
227 | '678' => 'Vanuatu',
228 | '679' => 'Fidschi',
229 | '680' => 'Palau',
230 | '681' => 'Wallis/Futuna',
231 | '682' => 'Cookinseln',
232 | '683' => 'Niue',
233 | '685' => 'Samoa',
234 | '686' => 'Kiribati, Gilbertinseln',
235 | '687' => 'Neukaledonien',
236 | '688' => 'Tuvalu, Elliceinseln',
237 | '689' => 'Französisch-Polynesien',
238 | '690' => 'Tokelau',
239 | '691' => 'Mikronesien',
240 | '692' => 'Marshallinseln',
241 | '730' => 'Russland',
242 | '734' => 'Russland',
243 | '735' => 'Russland',
244 | '7365' => 'Autonome Republik Krim (Ukraine)', // stand with Ukraine
245 | '738' => 'Russland',
246 | '739' => 'Russland',
247 | '740' => 'Russland',
248 | '741' => 'Russland',
249 | '742' => 'Russland',
250 | '747' => 'Russland',
251 | '748' => 'Russland',
252 | '749' => 'Russland',
253 | '77' => 'Kasachstan',
254 | '781' => 'Russland',
255 | '782' => 'Russland',
256 | '783' => 'Russland',
257 | '7840' => 'Georgien (Abchasien)',
258 | '7841' => 'Russland',
259 | '7842' => 'Russland',
260 | '7843' => 'Russland',
261 | '7844' => 'Russland',
262 | '7845' => 'Russland',
263 | '7846' => 'Russland',
264 | '7847' => 'Russland',
265 | '7848' => 'Russland',
266 | '7849' => 'Russland',
267 | '785' => 'Russland',
268 | '7860' => 'Russland',
269 | '7861' => 'Russland',
270 | '7862' => 'Russland',
271 | '7863' => 'Russland',
272 | '7864' => 'Russland',
273 | '7865' => 'Russland',
274 | '7866' => 'Russland',
275 | '7867' => 'Russland',
276 | '7868' => 'Russland',
277 | '7869' => 'Autonome Republik Krim (Ukraine)', // stand with Ukraine
278 | '787' => 'Russland',
279 | '790' => 'Russland',
280 | '791' => 'Russland',
281 | '792' => 'Russland',
282 | '793' => 'Russland',
283 | '7940' => 'Georgien (Abchasien)',
284 | '7941' => 'Russland',
285 | '7942' => 'Russland',
286 | '7943' => 'Russland',
287 | '7944' => 'Russland',
288 | '7945' => 'Russland',
289 | '7946' => 'Russland',
290 | '7947' => 'Russland',
291 | '7948' => 'Russland',
292 | '795' => 'Russland',
293 | '796' => 'Russland',
294 | '7970' => 'Russland',
295 | '7971' => 'Russland',
296 | '7972' => 'Russland',
297 | '7973' => 'Russland',
298 | '7974' => 'Russland',
299 | '7975' => 'Russland',
300 | '7976' => 'Russland',
301 | '7978' => 'Autonome Republik Krim (Ukraine)', // stand with Ukraine
302 | '7979' => 'Russland',
303 | '800' => 'Internationale Free-Phone-Dienste',
304 | '81' => 'Japan',
305 | '82' => 'Südkorea',
306 | '84' => 'Vietnam',
307 | '850' => 'Nordkorea',
308 | '852' => 'Hongkong',
309 | '853' => 'Macao',
310 | '855' => 'Kambodscha',
311 | '856' => 'Laos',
312 | '86' => 'Volksrepublik China',
313 | '870' => 'Inmarsat Single Number Access (SNAC)',
314 | '876' => 'maritime Mobiltelefonie',
315 | '878' => 'Persönliche Rufnummern',
316 | '879' => 'nationale mobile bezw. maritime Aufgaben',
317 | '880' => 'Bangladesch',
318 | '881' => 'Globales mobiles Satellitensystem',
319 | '882' => 'Internationale Netzwerke',
320 | '883' => 'Internationale Netzwerke (iNum)',
321 | '886' => 'Taiwan',
322 | '888' => 'Telecommunications for Disaster Relief',
323 | '90' => 'Türkei/Nordzypern',
324 | '91' => 'Indien',
325 | '92' => 'Pakistan',
326 | '93' => 'Afghanistan',
327 | '94' => 'Sri Lanka',
328 | '95' => 'Myanmar',
329 | '960' => 'Malediven',
330 | '961' => 'Libanon',
331 | '962' => 'Jordanien',
332 | '963' => 'Syrien',
333 | '964' => 'Irak',
334 | '965' => 'Kuwait',
335 | '966' => 'Saudi-Arabien',
336 | '967' => 'Jemen',
337 | '968' => 'Oman',
338 | '970' => 'Palästinensische Autonomiegebiete',
339 | '971' => 'Vereinigte Arabische Emirate',
340 | '972' => 'Israel',
341 | '973' => 'Bahrain',
342 | '974' => 'Katar',
343 | '975' => 'Bhutan',
344 | '976' => 'Mongolei',
345 | '977' => 'Nepal',
346 | '979' => 'Internationale Premium-Rate-Dienste',
347 | '98' => 'Iran',
348 | '991' => 'Service Trials (ITPCS)',
349 | '992' => 'Tadschikistan',
350 | '993' => 'Turkmenistan',
351 | '994' => 'Aserbaidschan',
352 | '995' => 'Georgien',
353 | '996' => 'Kirgisistan',
354 | '998' => 'Usbekistan',
355 | ];
356 |
--------------------------------------------------------------------------------
/assets/washing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/blacksenator/fbcallrouter/5f934c591fc252edadd71720a04b231ab8b2630b/assets/washing.png
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "blacksenator/fbcallrouter",
3 | "type": "project",
4 | "description": "",
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "blacksenator",
9 | "email": "knuffy@anasco.de"
10 | }
11 | ],
12 | "require": {
13 | "php": "^7.3 || ^8.0",
14 | "symfony/console": "^5.4",
15 | "guzzlehttp/guzzle": "^7.5",
16 | "phpmailer/phpmailer": "^6.8",
17 | "blacksenator/fritzsoap": "^2.8"
18 | },
19 | "autoload": {
20 | "psr-4": {
21 | "blacksenator\\": "src/",
22 | "Test\\": "tests/"
23 | },
24 | "files": ["src/functions.php"]
25 | },
26 | "require-dev": {
27 | "phpunit/phpunit": ">=9"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/config.example.php:
--------------------------------------------------------------------------------
1 | [
5 | 'url' => 'fritz.box', // your Fritz!Box IP
6 | 'user' => 'youruser', // your Fritz!Box user
7 | 'password' => 'xxxxxxxxxx', // your Fritz!Box user password
8 | ],
9 | 'phonebook' => [
10 | 'whitelist' => [0], // phone books number is already known (first index = 0!)
11 | 'blacklist' => 1, // phone book in which the spam number should be recorded
12 | 'newlist' => 0, // optional: phone book in which the reverse searchable entries should be recorded
13 | 'refresh' => 1, // after how many days the phone books should be read again
14 | ],
15 | 'contact' => [
16 | 'caller' => 'autom. gesperrt', // alias for the new unknown caller
17 | 'timestamp' => true, // adding timestamp to the caller: "[caller] ([timestamp])"
18 | 'type' => 'other', // type of phone line (home, work, mobil, fax etc.); 'other' = 'sonstige'
19 | ],
20 | 'filter' => [
21 | 'msn' => [], // MSNs to react on (['321765', '321766']; empty = all)
22 | 'blockForeign' => true, // block unknown foreign numbers
23 | 'score' => 6, // 5 = neutral, increase the value to be less sensitive (max. 9)
24 | 'comments' => 3, // decrease the value to be less sensitive (min 3)
25 | ],
26 | 'logging' => [
27 | 'log' => true,
28 | 'logPath' => '', // were callrouter_logging.txt schould be saved (default value is = './')
29 | ],
30 | /*
31 | 'email' => [
32 | 'url' => 'smtp...',
33 | 'port' => 587, // alternativ 465
34 | 'secure' => 'tls', // alternativ 'ssl'
35 | 'user' => '[USER]', // your sender email adress e.g. account
36 | 'password' => '[PASSWORD]',
37 | 'sender' => '', // OPTIONAL:your email adress who is sending this email
38 | 'receiver' => 'blacksenator@github.com', // your email adress to receive the secured contacts
39 | 'debug' => 0, // 0 = off (for production use)
40 | // 1 = client messages
41 | // 2 = client and server messages
42 | ],
43 | */
44 | 'test' => [ // if program is started with the -t option...
45 | 'numbers' => [ // ...the numbers are injected into the following calls
46 | '03681443300750', // tellows score > 5, comments > 3
47 | '0207565747377', // not existing NDC/STD (OKNz)
48 | '0618107162530', // valid NDC/STD, but invalid subscriber number
49 | '004433456778', // foreign number
50 | '', // unknown caler (uses CLIR)
51 | ],
52 | ],
53 | ];
54 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 |
3 | services:
4 | app:
5 | build: .
6 | volumes:
7 | - type: bind
8 | source: ./config.php
9 | target: /app/config.php
10 | logging:
11 | driver: "json-file"
12 | options:
13 | max-file: "5"
14 | max-size: "10m"
15 | restart: always
16 |
--------------------------------------------------------------------------------
/fbcallrouter:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | addCommands(array(
13 | new RunCommand()
14 | ));
15 |
16 | $app->run();
17 |
--------------------------------------------------------------------------------
/fbcallrouter.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=FRITZ!Box call router
3 | After=network.service
4 | StartLimitIntervalSec=0
5 |
6 | [Service]
7 | Type=simple
8 | Restart=always
9 | RestartSec=1
10 | WorkingDirectory=/home/[youruser]/fbcallrouter/
11 | ExecStart=/usr/bin/env php /home/[youruser]/fbcallrouter/fbcallrouter run
12 | User=[youruser]
13 |
14 | [Install]
15 | WantedBy=multi-user.target
16 |
--------------------------------------------------------------------------------
/src/ConfigTrait.php:
--------------------------------------------------------------------------------
1 | addOption('config', 'c', InputOption::VALUE_REQUIRED, 'config file', $default);
17 | }
18 |
19 | protected function loadConfig(InputInterface $input)
20 | {
21 | $configFile = $input->getOption('config');
22 |
23 | if (!file_exists($configFile)) {
24 | throw new \Exception('Config file ' . $configFile . ' does not exist');
25 | }
26 |
27 | $config = []; // make phpstan happy
28 | require_once($configFile);
29 | $this->config = $config;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/RunCommand.php:
--------------------------------------------------------------------------------
1 | setName('run')
18 | ->setDescription('perpetual')
19 | ->addOption('test', 't', InputOption::VALUE_NONE, 'test number(s)');
20 |
21 | $this->addConfig();
22 | }
23 |
24 | protected function execute(InputInterface $input, OutputInterface $output)
25 | {
26 | $this->loadConfig($input);
27 | error_log('Starting FRITZ!Box call router...');
28 | $testNumbers = [];
29 | if ($input->getOption('test')) {
30 | $testNumbers = $this->config['test']['numbers'] ?? [];
31 | }
32 | callRouting($this->config, $testNumbers);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/callrouter/callmonitor.php:
--------------------------------------------------------------------------------
1 | socketAdress = 'tcp://' . $url['host'] . ':' . self::CALLMONITORPORT;
35 | $this->getSocket();
36 | }
37 |
38 | /**
39 | * return the FRITZ!Box call monitor socket
40 | *
41 | * @param int $timeout
42 | * @return void
43 | */
44 | private function getSocket(int $timeout = 1)
45 | {
46 | $stream = stream_socket_client($this->socketAdress, $errno, $errstr);
47 | if (!$stream) {
48 | $message = sprintf("Can't reach the call monitor port! Error: %s (%s)!", $errstr, $errno);
49 | throw new \Exception($message);
50 | }
51 | $socket = socket_import_stream($stream);
52 | socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
53 | stream_set_timeout ($stream, $timeout);
54 | $this->nextSocketRefresh = time() + self::REFRESHTIME;
55 | $this->fritzBoxSocket = $stream;
56 | }
57 |
58 | /**
59 | * check and update socket status
60 | *
61 | * @return string
62 | */
63 | public function refreshSocket()
64 | {
65 | $msg = null;
66 | if (time() > $this->nextSocketRefresh) {
67 | $this->getSocket(); // refresh socket
68 | $msg = 'Status: Regular Socket refresh';
69 | } elseif (stream_get_meta_data($this->fritzBoxSocket)['eof']) { // socket died
70 | $this->getSocket(); // refresh socket
71 | $msg = 'Status: Dead Socket refreshed';
72 | }
73 |
74 | return $msg;
75 | }
76 |
77 | /**
78 | * returns the socket output
79 | *
80 | * @param array
81 | */
82 | public function getSocketStream()
83 | {
84 | if (($output = fgets($this->fritzBoxSocket)) != false) {
85 | return $this->parseCallString($output);
86 | }
87 |
88 | return [
89 | 'extern' => null,
90 | 'type' => null,
91 | ];
92 | }
93 |
94 |
95 | /**
96 | * parse a string from call monitor socket output. Four different strings
97 | * are known:
98 | * timestamp;CALL;connectionID;extension;MSN;extern;
99 | * timestamp;RING;connectionID;extern;MSN;
100 | * timestamp;CONNECT;connectionID;extension;extern/MSN;
101 | * timestamp;DISCONNECT;connectionID;duration;
102 | * e.g.
103 | * "01.01.20 10:10:10;RING;0;01701234567;987654;SIP0;\r\n"
104 | * "18.11.12 00:13:26;DISCONNECT;0;7;\r\n"
105 | *
106 | * @param string $line
107 | * @return array $result
108 | */
109 | private function parseCallString(string $line): array
110 | {
111 | $params = explode(';', str_replace(';\\r\\n', '', $line));
112 |
113 | $result = [
114 | 'timestamp' => $params[0],
115 | 'type' => $params[1],
116 | 'conID' => $params[2],
117 | ];
118 | if ($params[1] == 'RING') {
119 | $result += [
120 | 'extern' => $params[3],
121 | 'intern' => $params[4],
122 | 'device' => $params[5],
123 | ];
124 | } elseif ($params[1] == 'CALL') {
125 | $result += [
126 | 'extension' => $params[3],
127 | 'intern' => $params[4],
128 | 'extern' => $params[5],
129 | 'device' => $params[6],
130 | ];
131 | } elseif ($params[1] == 'CONNECT') {
132 | $result += [
133 | 'extension' => $params[3],
134 | 'number' => $params[4],
135 | ];
136 | } else { // DISCONNECT
137 | $result += [
138 | 'duration' => $params[3],
139 | ];
140 | }
141 |
142 | return $result;
143 | }
144 |
145 | /**
146 | * returns socket adress
147 | *
148 | * @return string $this->socketAdress
149 | */
150 | public function getSocketAdress()
151 | {
152 | return $this->socketAdress;
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/callrouter/callrouter.php:
--------------------------------------------------------------------------------
1 | '', // will be filled automatically
34 | 'type' => 'RING', // or 'CALL'
35 | 'conID' => '0', // not in use
36 | 'extern' => '00000000', // testnumber
37 | 'intern' => '0000000', // MSN
38 | 'device' => 'SIP0' // not in use
39 | ];
40 |
41 | private
42 | $contactConfig = [],
43 | $realName = '',
44 | $blockForeign,
45 | $mSNs = [],
46 | $proofList = [], // list of all telephone books to be checked against
47 | $blackList, // index of spam phone book
48 | $newList, // index of phone book for valid numbers (optional)
49 | $proofListNumbers = [], // all known numbers
50 | $centralNumbers = [],
51 | $testNumbers = [], // test case numbers
52 | $testCases = 0, // number of test cases
53 | $testCounter = 0,
54 | $callMonitor, // class instance
55 | $callMonitorValues = [], // keep call monitor values
56 | $phoneTools, // class instance
57 | $dialerCheck, // class instance
58 | $logging, // class instance
59 | $infoMail = null, // class instance
60 | $ownArea = [], // contains your area code
61 | $contactEntry = [], // contains the data for the telephone book entry
62 | $mailNotify = false,
63 | $mailText = [], // collector of logging info for email
64 | $elapse = 0,
65 | $nextUpdate = 0; // timestamp for refreshing phone books
66 |
67 | public function __construct(array $config, array $testNumbers = [])
68 | {
69 | $this->logging = new logging($config['logging']);
70 | $this->setLogging(99, ['Start the fbcallrouter initialization process']);
71 | $this->testNumbers = $testNumbers;
72 | $this->testCases = count($this->testNumbers);
73 | $this->contactConfig = $config['contact'];
74 | $this->realName = $this->contactConfig['caller'];
75 | $this->blockForeign = $config['filter']['blockForeign'] ?? false;
76 | $this->mSNs = $config['filter']['msn'] ?? [];
77 | $this->phoneTools = new phonetools($config['fritzbox']);
78 | $this->ownArea = $this->phoneTools->getOwnAreaCode();
79 | $this->setPhoneBooks($config['phonebook']);
80 | $this->refreshPhoneBooks(); // initial load
81 | $this->callMonitor = new callmonitor($this->phoneTools->getURL());
82 | $this->dialerCheck = new dialercheck($config['filter']);
83 | if (isset($config['email'])) {
84 | $this->infoMail = new infomail($config['email']);
85 | }
86 | echo 'On guard...' . PHP_EOL;
87 | $this->setLogging(0, [$this->callMonitor->getSocketAdress()]);
88 | }
89 |
90 | /**
91 | * sorting and arranging list of phone books
92 | *
93 | * @return void
94 | */
95 | private function sortProofList()
96 | {
97 | $this->proofList = array_unique($this->proofList);
98 | asort($this->proofList);
99 | array_values($this->proofList);
100 | }
101 |
102 | /**
103 | * set phone books
104 | *
105 | * @param array $config
106 | * @return void
107 | */
108 | private function setPhoneBooks(array $config)
109 | {
110 | $this->proofList = $config['whitelist'] ?? [0];
111 | $this->blackList = $config['blacklist'] ?? 1;
112 | $this->proofList[] = $this->blackList;
113 | $this->newList = $config['newlist'] ?? -1;
114 | if ($this->newList >= 0) {
115 | $this->proofList[] = $this->newList;
116 | }
117 | $this->sortProofList();
118 | $this->phoneTools->checkListOfPhoneBooks($this->proofList);
119 | $refresh = $config['refresh'] ?? 1;
120 | $this->elapse = ($refresh < 1) ? self::ONEDAY : $refresh * self::ONEDAY;
121 | }
122 |
123 | /**
124 | * set logging text
125 | *
126 | * @param int $stringID
127 | * @param array $infos
128 | * @return string
129 | */
130 | public function setLogging(int $stringID, array $infos)
131 | {
132 | return $this->logging->setLogging($stringID, $infos);
133 | }
134 |
135 | /**
136 | * returns rearranged timestamp data
137 | *
138 | * @param string $timestamp // dd.mm.yy hh:mm:ss
139 | * @return string // yyyy.mm.dd hh:mm:ss
140 | */
141 | private function getTimeStampReverse(string $timeStamp)
142 | {
143 | $parts = explode(' ', $timeStamp);
144 | $date = explode('.', $parts[0]);
145 |
146 | return '20' . $date[2] . '.' . $date[1] . '.' . $date[0] . ' ' . $parts[1];
147 | }
148 |
149 | /**
150 | * get real name
151 | *
152 | * @param string $timeStamp
153 | * @return string
154 | */
155 | private function getRealName(string $timeStamp)
156 | {
157 | if ($this->contactConfig['timestamp']) {
158 | return $this->realName . ' (' . $this->getTimeStampReverse($timeStamp) . ')';
159 | } else {
160 | return $this->realName;
161 | }
162 | }
163 |
164 | /**
165 | * returns phone numbers from phone books
166 | *
167 | * @param array $phoneBooks
168 | * @return array $phoneBookNumbers
169 | */
170 | private function getPhoneBookNumbers(array $phoneBooks = [0])
171 | {
172 | $phoneNumbers = [];
173 | foreach ($phoneBooks as $phoneBook) {
174 | if (empty($numbers = $this->phoneTools->getPhoneNumbers($phoneBook))) {
175 | $this->setLogging(8, [$phoneBook]);
176 | } else {
177 | $phoneNumbers = array_merge($phoneNumbers, $numbers);
178 | date_default_timezone_set('Europe/Berlin');
179 | $this->setLogging(1, [$phoneBook, date('d.m.Y H:i:s', $this->nextUpdate)]);
180 | }
181 | }
182 |
183 | return $phoneNumbers;
184 | }
185 |
186 | /**
187 | * returns and extracts the central numbers
188 | *
189 | * @return array $centralNumbers
190 | */
191 | private function getCentralNumbers()
192 | {
193 | $centralNumbers = [];
194 | foreach ($this->proofListNumbers as $key => $number) {
195 | if (substr($number, -1) == '*') {
196 | $centralNumbers[] = substr($number, 0, -1);
197 | unset($this->proofListNumbers[$key]);
198 | }
199 | }
200 | $this->proofListNumbers = array_values($this->proofListNumbers);
201 |
202 | return $centralNumbers;
203 | }
204 |
205 | /**
206 | * returns reread phone book numbers
207 | *
208 | * @return void
209 | */
210 | public function refreshPhoneBooks()
211 | {
212 | if (time() > $this->nextUpdate) {
213 | $this->nextUpdate = time() + $this->elapse;
214 | $this->phoneTools->refreshContactClient();
215 | $this->proofListNumbers = $this->getPhoneBookNumbers($this->proofList);
216 | $this->centralNumbers = $this->getCentralNumbers();
217 | // for debugging perposes uncomment the following line
218 | // file_put_contents('numbers.txt', print_r($this->proofListNumbers, true));
219 | }
220 | }
221 |
222 | /**
223 | * getting the array from the call monitor socket stream
224 | *
225 | * @return array $this->callMonitorValues
226 | */
227 | public function getCallMonitorStream()
228 | {
229 | $this->mailText = [];
230 | $this->setCallMonitorValues($this->callMonitor->getSocketStream());
231 |
232 | return $this->callMonitorValues;
233 | }
234 |
235 | /**
236 | * set call monitor values
237 | *
238 | * @return void
239 | */
240 | public function setCallMonitorValues(array $values = [])
241 | {
242 | $this->callMonitorValues = $values;
243 | }
244 |
245 | /**
246 | * set entry in phone book
247 | *
248 | * @param array $entry
249 | * @param int $logIndex
250 | * @return string
251 | */
252 | private function setPhoneBookEntry(int $logIndex, array $additional = [])
253 | {
254 | $this->phoneTools->setPhoneBookEntry($this->contactEntry);
255 | $this->proofListNumbers[] = $this->contactEntry['number'];
256 | $this->mailNotify = true;
257 | $logInfo = [$this->contactEntry['name'], $this->contactEntry['phonebook']];
258 | if (!empty($additional)) {
259 | $logInfo = array_merge($logInfo, $additional);
260 | }
261 |
262 | return $this->setLogging($logIndex, $logInfo);
263 | }
264 |
265 | /**
266 | * set contact data for phone book entry
267 | *
268 | * @param int $phonebook
269 | * @param string $number
270 | * @param string $name (optional)
271 | * @return array
272 | */
273 | private function setContactEntry(int $phonebook, string $number, string $name = null)
274 | {
275 | $realName = $this->getRealName($this->callMonitorValues['timestamp']);
276 |
277 | $this->contactEntry = [
278 | 'phonebook' => $phonebook,
279 | 'name' => $name ?? $realName,
280 | 'number' => $number,
281 | 'type' => $this->contactConfig['type'],
282 | ];
283 | }
284 |
285 | /**
286 | * returns true if MSN from call monitor stream is in the list of numbers to
287 | * react on
288 | */
289 | public function isMSNtoProof (string $msn)
290 | {
291 | if (count($this->mSNs) && !in_array($msn, $this->mSNs)) {
292 | return false;
293 | }
294 |
295 | return true;
296 | }
297 |
298 | /**
299 | * checking presumably foreign numbers
300 | *
301 | * @param string $number
302 | * @param int $numberLength
303 | * @return bool
304 | */
305 | private function parseForeignNumber(string $number, int $numberLength)
306 | {
307 | $result = true;
308 | $countryData = [];
309 | if ($this->blockForeign) {
310 | $this->mailText[] = $this->setPhoneBookEntry(4);
311 | } elseif ($numberLength < 7 || $numberLength > 17) {
312 | // see class comment at top of file
313 | $this->mailText[] = $this->setPhoneBookEntry(13);
314 | } elseif (($countryData = $this->phoneTools->getCountry($number)) == false) {
315 | // unknown country code
316 | $this->mailText[] = $this->setPhoneBookEntry(3);
317 | } elseif (substr($countryData['national'], 0) == '0'){
318 | // illegal start of area code
319 | $this->mailText[] = $this->setPhoneBookEntry(5);
320 | } else {
321 | $this->callMonitorValues += $countryData;
322 | $result = false;
323 | }
324 |
325 | return $result;
326 | }
327 |
328 | /**
329 | * decomposition of atypically composed cellular number consists of:
330 | * [AREACODE][COUNTRYCODE(49)][CELLULARPREFIX][NUMBER]
331 | *
332 | * @param string $number
333 | * @return bool
334 | */
335 | private function getVeiledCellular(string $number)
336 | {
337 | $nationalData = [];
338 | $rear = '0' . substr($number, $this->ownArea['length'] + 2);
339 | if (
340 | ($nationalData = $this->phoneTools->getArea($rear)) != false
341 | && $this->phoneTools->isCellularCode($nationalData['prefix'])
342 | ){
343 | // adding transmitted number
344 | $this->mailText[] = $this->setPhoneBookEntry(14);
345 | // adding derived (actual) number
346 | $this->contactEntry['number'] = $rear;
347 | $this->mailText[] = $this->setPhoneBookEntry(15, [$rear]);
348 | return true;
349 | }
350 |
351 | return false;
352 | }
353 |
354 | /**
355 | * decomposition of two transmitted numbers (second one bracketed):
356 | * [USER_PROVIDED_NUMBER]([NETWORK_PROVIDED_NUMBER])
357 | * @see https://avm.de/service/wissensdatenbank/dok/FRITZ-Box-7490/1613_In-Anrufliste-werden-zwei-Rufnummern-fur-einen-Anruf-angezeigt/
358 | *
359 | * @param string $number
360 | * @return void
361 | */
362 | private function separateBracketedNumber(string $number)
363 | {
364 | $result = false;
365 | $networkProvided = preg_replace('/[^0-9]/', '', substr(strstr($number, '('), 1, -1));
366 | $userProvided = trim(strstr($number, '(', true));
367 | // adding transmitted number
368 | if ($this->isNumberKnown($userProvided) == false) {
369 | $this->contactEntry['number'] = $userProvided;
370 | $this->mailText[] = $this->setPhoneBookEntry(16, [$userProvided]);
371 | $result = true;
372 | }
373 | // adding bracketed number
374 | if ($this->isNumberKnown($networkProvided) == false) {
375 | $this->contactEntry['number'] = $networkProvided;
376 | $this->mailText[] = $this->setPhoneBookEntry(17, [$networkProvided]);
377 | $result = true;
378 | }
379 |
380 | return $result;
381 | }
382 |
383 | /**
384 | * checking domestic numbers
385 | *
386 | * @param string $number
387 | * @param int $numberLength
388 | * @return bool
389 | */
390 | private function parseDomesticNumber(string $number, int $numberLength)
391 | {
392 | $nationalData = [];
393 | $result = true;
394 | if (substr($number, 0, 1) != '0') {
395 | // presumably obsolete, since the area code is always transmitted even with local calls?
396 | $this->setLogging(99, ['No trunk prefix (VAZ). Probably domestic call. No action possible']);
397 | }
398 | if (($nationalData = $this->phoneTools->getArea($number)) == false) {
399 | // unknown german area code
400 | $this->mailText[] = $this->setPhoneBookEntry(5);
401 | } elseif (
402 | // seldom: landline fake numbers starting with "0"; exclusiv cellular numbers
403 | $this->phoneTools->isCellularCode($nationalData['prefix']) == false
404 | && substr($nationalData['subscriber'], 0, 1) == '0'
405 | ) {
406 | $this->mailText[] = $this->setPhoneBookEntry(9);
407 | } elseif (preg_match('/^' . $this->ownArea['code'] . '49[1][5-7][0-9]+/', $number)) {
408 | // particularly observed case: [AREACODE]49[CELLUAR_NUMBER]
409 | $result = $this->getVeiledCellular($number);
410 | } elseif (strpos($number, '(') !== false) {
411 | // particularly observed case: [NUMBER]([ALTNUMBER])
412 | $result = $this->separateBracketedNumber($number);
413 | } elseif ($numberLength < 8 || $numberLength > 14) {
414 | // see class comment at top of file
415 | $this->mailText[] = $this->setPhoneBookEntry(13);
416 | } else {
417 | $result = false;
418 | }
419 |
420 | return $result;
421 | }
422 |
423 | /**
424 | * sarch for number in web directories
425 | *
426 | * @param string $number
427 | * @param bool $isForeign
428 | * @return void
429 | */
430 | private function webSearch(string $number, bool $isForeign)
431 | {
432 | $webResult = [];
433 | $checkDasOertliche = false;
434 | if ($webResult = $this->dialerCheck->getRating($number)) {
435 | $score = $webResult['score'];
436 | $comments = $webResult['comments'];
437 | if ($this->dialerCheck->proofRating($webResult)) {
438 | $this->mailText[] = $this->setPhoneBookEntry(6, [$score, $comments]
439 | );
440 | } else {
441 | $this->mailText[] = $this->setLogging(7, [$score, $comments]);
442 | $this->mailNotify = true;
443 | $isForeign ?: $checkDasOertliche = true;
444 | }
445 | } else {
446 | $this->mailText[] = $this->setLogging(99, ['Request in spam databases failed!']);
447 | $this->mailNotify = true;
448 | $isForeign ?: $checkDasOertliche = true;
449 | }
450 | if ($checkDasOertliche) {
451 | $webResult = $this->checkDasOertliche($number);
452 | }
453 | if (isset($webResult['url'])) {
454 | $this->setLogging(11, [$webResult['url']]);
455 | $this->mailText[] = $webResult['deeplink'];
456 | $this->mailNotify = true;
457 | }
458 | }
459 |
460 | /**
461 | * perform the validation of incomming calls ('RING')
462 | *
463 | * @return void
464 | */
465 | public function runInboundValidation()
466 | {
467 | if ($this->isMSNtoProof($this->callMonitorValues['intern'])) {
468 | $this->mailNotify = false;
469 | $isSortedOut = true;
470 | $number = $this->callMonitorValues['extern'];
471 | $numberLength = strlen($number);
472 | if ($numberLength == 0) {
473 | $this->setLogging(99, ['Caller uses CLIR - no action possible']);
474 | } elseif ($this->isNumberKnown($number) === false) {
475 | $this->setContactEntry($this->blackList, $number);
476 | $this->mailText[] = $this->setLogging(2, [$number, $this->callMonitorValues['intern']]);
477 | $isForeign = (substr($number, 0, 2) === '00') ? true : false;
478 | if ($isForeign) { // foreign number specific
479 | $isSortedOut = $this->parseForeignNumber($number, $numberLength);
480 | } else { // domestic numbers specific
481 | $isSortedOut = $this->parseDomesticNumber($number, $numberLength);
482 | }
483 | }
484 | if (!$isSortedOut) {
485 | $this->webSearch($number, $isForeign);
486 | }
487 | }
488 | }
489 |
490 | /**
491 | * perform the validation of out going calls ('CALL')
492 | *
493 | * @return void
494 | */
495 | public function runOutboundValidation()
496 | {
497 | if ($this->isMSNtoProof($this->callMonitorValues['intern'])) {
498 | $number = str_replace('#', '', $this->callMonitorValues['extern']);
499 | $message = $this->setLogging(12, [$number]);
500 | if ($this->isNumberKnown($number) === false) {
501 | $this->mailNotify = false;
502 | $this->mailText[] = $message;
503 | $webResult = $this->checkDasOertliche($number);
504 | if (isset($webResult['url'])) {
505 | $this->setLogging(11, [$webResult['url']]);
506 | $this->mailText[] = $webResult['deeplink'];
507 | $this->mailNotify = true;
508 | }
509 | }
510 | }
511 | }
512 |
513 | /**
514 | * checks if number is known in Das Örtliche public phone book
515 | *
516 | * @param string $number
517 | * @return array|bool
518 | */
519 | public function checkDasOertliche($number)
520 | {
521 | if (
522 | $this->newList >= 0
523 | && ($webResult = $this->dialerCheck->getDasOertliche($number))
524 | ) {
525 | $this->setContactEntry($this->newList, $number, $webResult['name']);
526 | $this->mailText[] = $this->setPhoneBookEntry(10);
527 | }
528 |
529 | return $webResult;
530 | }
531 |
532 | /**
533 | * returns true if a number starts with a known central number
534 | *
535 | * @param string $number
536 | * @return bool
537 | */
538 | private function isDirectInwardDial(string $number)
539 | {
540 | foreach ($this->centralNumbers as $centralNumber) {
541 | if (strpos($number, $centralNumber) === 0) {
542 | return true;
543 | }
544 | }
545 |
546 | return false;
547 | }
548 |
549 | /**
550 | * returns if number is known in one of the phone books or begins with an
551 | * already known central number
552 | *
553 | * @param string $number
554 | * @return bool
555 | */
556 | private function isNumberKnown(string $number)
557 | {
558 | if (in_array($number, $this->proofListNumbers)) {
559 | return true;
560 | }
561 |
562 | return $this->isDirectInwardDial($number);
563 | }
564 |
565 | /**
566 | * get current staus of socket and refresh
567 | *
568 | * @return void
569 | */
570 | public function refreshSocket()
571 | {
572 | $socketStatus = $this->callMonitor->refreshSocket();
573 | $socketStatus == null ?: $this->setLogging(99, [$socketStatus]);
574 | }
575 |
576 | /**
577 | * send email
578 | *
579 | * @return void
580 | */
581 | public function sendMail()
582 | {
583 | if (isset($this->infoMail) && $this->mailNotify) {
584 | $msg = $this->infoMail->sendMail($this->callMonitorValues, $this->mailText);
585 | if ($msg <> null) {
586 | $this->setLogging(99, [$msg]);
587 | }
588 | }
589 | $this->mailNotify = false;
590 | $this->contactEntry = [];
591 | }
592 |
593 | /**
594 | * setting call monitor values for debugging purposes
595 | *
596 | * @return array
597 | */
598 | public function setDebugStream()
599 | {
600 | $this->callMonitorValues = self::DBGSTRM;
601 | $this->callMonitorValues['timestamp'] = date('d.m.y H:i:s');
602 |
603 | return $this->callMonitorValues;
604 | }
605 |
606 | /**
607 | * set one of n test numbers in sequence, as specified in the configuration
608 | * when executed with the "-t" parameter
609 | *
610 | * @return void
611 | */
612 | public function getTestInjection()
613 | {
614 | if ($this->testCases > 0) {
615 | if ($this->testCounter === 0) {
616 | $this->setLogging(99, ['START OF TEST OPERATION']);
617 | } elseif ($this->testCounter === $this->testCases) {
618 | $this->setLogging(99, ['END OF TEST OPERATION']);
619 | $this->testCases = 0;
620 | }
621 | }
622 | if ($this->testCounter < $this->testCases) { // as long as the test case was not used
623 | echo sprintf('Running test case %s of %s', $this->testCounter + 1, $this->testCases) . PHP_EOL;
624 | // inject the next sanitized number from row
625 | $this->callMonitorValues['extern'] = $this->phoneTools->sanitizeNumber($this->testNumbers[$this->testCounter]);
626 | $this->testCounter++;
627 | }
628 | }
629 | }
630 |
--------------------------------------------------------------------------------
/src/callrouter/dialercheck.php:
--------------------------------------------------------------------------------
1 | '9',
25 | 'Verwirrend' => '7',
26 | 'Unbekannte' => '5',
27 | 'Egal' => '3',
28 | 'Positiv' => '1',
29 | ],
30 | ];
31 | const CLVRDLR = [
32 | 'https://www.cleverdialer.de/telefonnummer/',
33 | 'Clever Dailer',
34 | ];
35 | const TELSPIO = [
36 | 'https://www.telefonspion.de/',
37 | 'Telefonspion',
38 | ];
39 | //const WRRFTAN = 'https://wer-ruftan.de/Nummer/'; // offen
40 | //const WMGEHRT = 'https://www.wemgehoert.de/nummer/' // offen
41 | const WRHTANG = [
42 | 'https://www.werhatangerufen.com/',
43 | 'WerHatAngerufen',
44 | ];
45 | const TELLOWS = [
46 | 'http://www.tellows.de/basic/num/%s?xml=1&partner=test&apikey=test123',
47 | 'https://www.tellows.de/num/',
48 | 'tellows',
49 | ];
50 | const DSOERTL = [
51 | 'https://www.dasoertliche.de/rueckwaertssuche/?ph=',
52 | 'https://www.dasoertliche.de/?form_name=search_inv&ph=',
53 | 'Das Örtliche'
54 | ];
55 |
56 | private $score; // rating normalized to tellows (1 - 9)
57 | private $comments; // in most cases number of valuations
58 |
59 | public function __construct(array $filter)
60 | {
61 | $this->score = ($filter['score'] > 9) ? 9 : $filter['score'];
62 | $this->comments = ($filter['comments'] < 3) ? 3 : $filter['comments'];
63 | }
64 |
65 | /**
66 | * assamble the deeplink string
67 | *
68 | * @param string $url
69 | * @param string $label
70 | * @return string
71 | */
72 | private function getDeepLinkString(string $url, string $label)
73 | {
74 | return 'Number traced in: ' . $label . '';
75 | }
76 |
77 | /**
78 | * return the tellows rating and number of comments
79 | *
80 | * @param string $number phone number
81 | * @return array|bool $score array of rating and number of comments or false
82 | */
83 | private function getTellowsRating(string $number)
84 | {
85 | $url = sprintf(self::TELLOWS[0], $number);
86 | if (($rating = @simplexml_load_file($url)) == false) {
87 | return false;
88 | }
89 | $rating->asXML();
90 | $url = self::TELLOWS[1] . $number;
91 |
92 | return [
93 | 'score' => intval($rating->score),
94 | 'comments' => intval($rating->comments),
95 | 'url' => $url,
96 | 'deeplink' => $this->getDeepLinkString($url, self::TELLOWS[2]),
97 | ];
98 | }
99 |
100 | /**
101 | * converting an HTML response into a SimpleXMLElement
102 | *
103 | * @param string $response
104 | * @return SimpleXMLElement $xmlSite
105 | */
106 | private function convertHTMLtoXML($response)
107 | {
108 | $dom = new DOMDocument();
109 | $dom->preserveWhiteSpace = false;
110 | @$dom->loadHTML($response);
111 |
112 | return simplexml_import_dom($dom);
113 | }
114 |
115 | /**
116 | * returns a websites HTML as XML
117 | *
118 | * @param string $url
119 | * @return SimpleXMLelement|bool
120 | */
121 | private function getWebsiteAsXML(string $url)
122 | {
123 | $html = @file_get_contents($url);
124 |
125 | return !$html ? false : $this->convertHTMLtoXML($html);
126 | }
127 |
128 | /**
129 | * returns the equivalent from one of/to five stars to the score from one to
130 | * nine, where five stars are a score of one and one star is the score of nine
131 | *
132 | * @param string|float $stars
133 | * @return float as 1 .. 9
134 | */
135 | private function convertStarsToScore($stars)
136 | {
137 | return round(intval($stars) * 2) / 2 * -2 + 11;
138 | }
139 |
140 | /**
141 | * return the werruft.info rating and number of comments with screen
142 | * scraping of
143 | *
150 | *
151 | * @param string $number phone number
152 | * @return array|bool $score array of rating and number of comments or false
153 | */
154 | private function getWerRuftInfoRating(string $number)
155 | {
156 | $url = self::WERRUFT[0] . $number . '/';
157 | if (($rawXML = $this->getWebsiteAsXML($url)) == false) {
158 | return false;
159 | }
160 | if (count($comments = $rawXML->xpath('//i[contains(@class, "comop")]')) != 5) {
161 | return false;
162 | }
163 | $totalComment = 0;
164 | foreach($comments as $comment) {
165 | $totalComment += (int)$comment;
166 | }
167 | $title = $rawXML->xpath('//title');
168 | if (preg_match('/\((.*?)\:/', $title[0], $match)) {
169 | $score = strtr($match[1], self::WERRUFT[2]);
170 | } else {
171 | return false;
172 | }
173 |
174 | return [
175 | 'score' => intval($score),
176 | 'comments' => $totalComment,
177 | 'url' => $url,
178 | 'deeplink' => $this->getDeepLinkString($url, self::WERRUFT[1]),
179 | ];
180 | }
181 |
182 | /**
183 | * return the cleverdialer rating and number of comments
184 | *
185 | * @param string $number phone number
186 | * @return array|bool $score array of rating and number of comments or false
187 | */
188 | private function getCleverDialerRating(string $number)
189 | {
190 | $url = self::CLVRDLR[0] . $number;
191 | if (($rawXML = $this->getWebsiteAsXML($url)) == false) {
192 | return false;
193 | }
194 | if (count($valuation = $rawXML->xpath('//div[@class = "rating-text"]'))) {
195 | $stars = intval(str_replace(' von 5 Sternen', '', $valuation[0]->span[0]));
196 | if ($stars > 0) {
197 | $comments = preg_replace('/[^0-9]/', '', $valuation[0]->span[1]);
198 | return [
199 | 'score' => $this->convertStarsToScore($stars),
200 | 'comments' => intval($comments),
201 | 'url' => $url,
202 | 'deeplink' => $this->getDeepLinkString($url, self::CLVRDLR[1]),
203 | ];
204 | }
205 | }
206 |
207 | return false;
208 | }
209 |
210 | /**
211 | * return the telefonspion rating and number of comments
212 | *
213 | * @param string $number phone number
214 | * @return array|bool $score array of rating and number of comments or false
215 | */
216 | private function getTelefonSpionRating(string $number)
217 | {
218 | $url = self::TELSPIO[0] . $number;
219 | if (($rawXML = $this->getWebsiteAsXML($url)) == false) {
220 | return false;
221 | }
222 | $title = (string)$rawXML->xpath('//title')[0];
223 | $titleParts = explode(' - ', $title);
224 | if (count($titleParts) == 3) {
225 | $values = [];
226 | preg_match_all('/\d+/', $titleParts[2], $values);
227 | if (count($values[0]) > 3 && intval($values[0][0]) > 0) {
228 | return [
229 | 'score' => -0.8 * intval($values[0][2]) + 9,
230 | 'comments' => intval($values[0][1]),
231 | 'url' => $url,
232 | 'deeplink' => $this->getDeepLinkString($url, self::TELSPIO[1]),
233 | ];
234 | }
235 | }
236 |
237 | return false;
238 | }
239 |
240 | /**
241 | * return the werhatangerufen rating and number of comments
242 | *
243 | * @param string $number phone number
244 | * @return array|bool $score array of rating and number of comments or false
245 | */
246 | private function getWerHatAngerufenRating(string $number)
247 | {
248 | $url = self::WRHTANG[0] . $number;
249 | if (($rawXML = $this->getWebsiteAsXML($url)) == false) {
250 | return false;
251 | }
252 | $valuation = $rawXML->xpath('//span[@class="rating"]');
253 | if (count($valuation)) {
254 | $stars = intval(substr(str_replace('Bewertung: ', '', $valuation[0]), 0, 1));
255 | if ($stars > 0) {
256 | $titleParts = explode(' // ', $rawXML->xpath('//title')[0]);
257 | if (count($titleParts) == 2) {
258 | return [
259 | 'score' => $this->convertStarsToScore($stars),
260 | 'comments' => intval(str_replace(' Bewertungen', '', $titleParts[1])),
261 | 'url' => $url,
262 | 'deeplink' => $this->getDeepLinkString($url, self::WRHTANG[1]),
263 | ];
264 | }
265 | }
266 | }
267 |
268 | return false;
269 | }
270 |
271 | /**
272 | * returns if rating is above or equal to the user limits
273 | *
274 | * @param array $rating
275 | * @return bool
276 | */
277 | public function proofRating(array $rating)
278 | {
279 | if ($rating['score'] >= $this->score &&
280 | $rating['comments'] >= $this->comments) {
281 | return true;
282 | }
283 |
284 | return false;
285 | }
286 |
287 | /**
288 | * proofs cascading if the number is known in online list as bad rated
289 | *
290 | * @param string $number
291 | * @param string $score
292 | * @param string $comments
293 | * @return array|bool
294 | */
295 | public function getRating(string $number)
296 | {
297 | $proofedRating['score'] = null;
298 | if ($rating = $this->getWerRuftInfoRating($number)) {
299 | if ($this->proofRating($rating)) {
300 | return $rating;
301 | } else {
302 | $proofedRating = $rating;
303 | }
304 | }
305 | if ($rating = $this->getCleverDialerRating($number)) {
306 | if ($this->proofRating($rating)) {
307 | return $rating;
308 | } else {
309 | ($rating['score'] <= $proofedRating['score']) ?: $proofedRating = $rating;
310 | }
311 | }
312 | if ($rating = $this->getTelefonSpionRating($number)) {
313 | if ($this->proofRating($rating)) {
314 | return $rating;
315 | } else {
316 | ($rating['score'] <= $proofedRating['score']) ?: $proofedRating = $rating;
317 | }
318 | }
319 | if ($rating = $this->getWerHatAngerufenRating($number)) {
320 | if ($this->proofRating($rating)) {
321 | return $rating;
322 | } else {
323 | ($rating['score'] <= $proofedRating['score']) ?: $proofedRating = $rating;
324 | }
325 | }
326 | if ($rating = $this->getTellowsRating($number)) {
327 | if ($this->proofRating($rating)) {
328 | return $rating;
329 | } else {
330 | ($rating['score'] <= $proofedRating['score']) ?: $proofedRating = $rating;
331 | }
332 | }
333 |
334 | $result = (isset($proofedRating['score'])) ? $proofedRating : false;
335 |
336 | return $result;
337 | }
338 |
339 | /**
340 | * return the name from Das Örtliche
341 | *
342 | * @param string $number phone number
343 | * @return array|bool $score array of rating and number of comments or false
344 | */
345 | public function getDasOertliche(string $number)
346 | {
347 | $url = self::DSOERTL[0] . $number;
348 | if (($rawXML = $this->getWebsiteAsXML($url)) == false) {
349 | $url = self::DSOERTL[1] . $number; // second attemp
350 | if (($rawXML = $this->getWebsiteAsXML($url)) == false) {
351 | return false;
352 | }
353 | }
354 | if ($rawXML->xpath('//div[@class="nonumber01"]')) { // unknown
355 | return false;
356 | }
357 | if ($result = $rawXML->xpath('//div[@class="nonumber"]')) { // alternative
358 | if (($altNumber = filter_var($result[0]->p, FILTER_SANITIZE_NUMBER_INT)) != '') {
359 | $url = self::DSOERTL[1] . $altNumber;
360 | if (($rawXML = $this->getWebsiteAsXML($url)) == false) {
361 | return false;
362 | }
363 | }
364 | }
365 | if ($result = $rawXML->xpath('//a[@class="hitlnk_name"]')) {
366 | return [
367 | 'name' => trim($result[0]),
368 | 'url' => $url,
369 | 'deeplink' => $this->getDeepLinkString($url, self::DSOERTL[2]),
370 | ];
371 | }
372 |
373 | return false;
374 | }
375 | }
376 |
--------------------------------------------------------------------------------
/src/callrouter/infomail.php:
--------------------------------------------------------------------------------
1 | mail = new PHPMailer(true);
23 | $this->mail->CharSet = "text/html; charset=UTF-8;";
24 | $this->mail->isSMTP(); // tell PHPMailer to use SMTP
25 | $this->mail->SMTPDebug = $account['debug'];
26 | $this->mail->Host = $account['url']; // set the hostname of the mail server
27 | $this->mail->Port = $account['port']; // set the SMTP port number - likely to be 25, 465 or 587
28 | $this->mail->SMTPSecure = $account['secure'];
29 | $this->mail->SMTPAuth = true; // whether to use SMTP authentication
30 | $this->mail->Username = $account['user']; // username to use for SMTP authentication
31 | $this->mail->Password = $account['password']; // password to use for SMTP authentication
32 | $sender = $account['sender'] ?? $account['user']; // if optional sender adress is choosen
33 | $this->mail->setFrom($sender, 'fbcallrouter'); // set who the message is to be sent fromly-to address
34 | $this->mail->addAddress($account['receiver']); // set who the message is to be sent to
35 | }
36 |
37 | /**
38 | * send email
39 | *
40 | * @param array $callMonitorValues
41 | * @param array $protocoll
42 | * @return string|void
43 | */
44 | public function sendMail(array $callMonitorValues, array $protocol)
45 | {
46 | $number = $callMonitorValues['extern'];
47 | $way = ($callMonitorValues['type'] == 'RING') ? 'from' : 'to';
48 | $body = nl2br('The following results and actions have been recorded:' . PHP_EOL . PHP_EOL);
49 | foreach ($protocol as $lines) {
50 | $body .= nl2br($lines . PHP_EOL);
51 | }
52 | $this->mail->Subject = sprintf('fbcallrouter traced a call %s number %s', $way, $number);
53 | $this->mail->Body = $body;
54 | $this->mail->isHTML(true);
55 | if (!$this->mail->send()) { // send the message, check for errors
56 | return 'Mailer Error: ' . $this->mail->ErrorInfo;
57 | }
58 |
59 | return null;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/callrouter/logging.php:
--------------------------------------------------------------------------------
1 | logging = $config['log'] ?? false;
26 | $this->loggingPath = $config['logPath'] ?: dirname(__DIR__, 2);
27 | }
28 |
29 | /**
30 | * set logging
31 | *
32 | * @param int $stringID
33 | * @param array $infos
34 | * @return void
35 | */
36 | public function setLogging(int $stringID = null, array $infos = [])
37 | {
38 | if ($stringID == 0) {
39 | $message = sprintf('Guarding started: listen to FRITZ!Box call monitor at %s', $infos[0]);
40 | } elseif ($stringID == 1) {
41 | $message = sprintf('Phone book %s (re-)loaded; next refresh: %s', $infos[0], $infos[1]);
42 | } elseif ($stringID == 2) {
43 | $message = sprintf('CALL IN from number %s to MSN %s', $infos[0], $infos[1]);
44 | } elseif ($stringID == 3) {
45 | $message = sprintf('Caller uses an unknown country code! Added to spam phone book #%s', $infos[1]);
46 | } elseif ($stringID == 4) {
47 | $message = sprintf('Foreign number. Added to spam phone book #%s', $infos[1]);
48 | } elseif ($stringID == 5) {
49 | $message = sprintf('Caller uses a nonexistent area code! Added to spam phone book #%s', $infos[1]);
50 | } elseif ($stringID == 6) {
51 | $message = sprintf('Caller has a bad reputation (%s/%s)! Added to spam phone book #%s', $infos[2], $infos[3], $infos[1]);
52 | } elseif ($stringID == 7) {
53 | $message = sprintf('Caller has a rating of %s out of 9 and %s valuations.', $infos[0], $infos[1]);
54 | } elseif ($stringID == 8) {
55 | $message = sprintf('Phone book %s could not be read', $infos[0]);
56 | } elseif ($stringID == 9) {
57 | $message = sprintf('Caller is using an illegal subscriber number! Added to spam phone book #%s', $infos[1]);
58 | } elseif ($stringID == 10) {
59 | $message = sprintf('Called number identified as: %s. Entry added to phone book #%s', $infos[0], $infos[1]);
60 | } elseif ($stringID == 11) {
61 | $message = sprintf('Number traced in: %s', $infos[0]);
62 | } elseif ($stringID == 12) {
63 | $message = sprintf('CALL OUT to number %s', $infos[0]);
64 | } elseif ($stringID == 13) {
65 | $message = sprintf('Number length is not in range. Added to spam phone book #%s', $infos[1]);
66 | } elseif ($stringID == 14) {
67 | $message = sprintf('Caller transmits an unusual number combination. Added to spam phone book #%s', $infos[1]);
68 | } elseif ($stringID == 15) {
69 | $message = sprintf('Derived actual phone number: %s Added to spam phone book #%s', $infos[2], $infos[1]);
70 | } elseif ($stringID == 16) {
71 | $message = sprintf('User provided phone number: %s Added to spam phone book #%s', $infos[2], $infos[1]);
72 | } elseif ($stringID == 17) {
73 | $message = sprintf('Network provided phone number: %s Added to spam phone book #%s', $infos[2], $infos[1]);
74 | } elseif ($stringID == 99) {
75 | $message = $infos[0];
76 | }
77 |
78 | return $this->writeLogging($message);
79 | }
80 |
81 | /**
82 | * write logging info
83 | *
84 | * @param string $info
85 | * @return string $message
86 | */
87 | private function writeLogging ($info)
88 | {
89 | date_default_timezone_set('Europe/Berlin');
90 | $message = date('d.m.Y H:i:s') . ' => ' . $info . PHP_EOL;
91 | if ($this->logging) {
92 | if (getenv('DOCKER_CONTAINER')) {
93 | echo($info . PHP_EOL);
94 | } else {
95 | file_put_contents($this->loggingPath . '/callrouter_logging.txt', $message, FILE_APPEND);
96 | }
97 | }
98 |
99 | return $message;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/callrouter/phonetools.php:
--------------------------------------------------------------------------------
1 | 'neuartige Dienste',
31 | '137' => 'Televoting, Gewinnspiel',
32 | '138' => 'Televoting, Gewinnspiel',
33 | '164' => 'Pager',
34 | '168' => 'Cityruf/Pager',
35 | '169' => 'Cityruf/Pager',
36 | '18' => 'Service-Dienste',
37 | '190' => 'Mehrwertdienste', */
38 | '3212' => 'nationale Teilnehmernummer',
39 | '3221' => 'nationale Teilnehmernummer',
40 | '3222' => 'nationale Teilnehmernummer',
41 | '700' => 'persönliche Rufnummer',
42 | '800' => 'Mehrwertdienste',
43 | /* '900' => 'Mehrwertdienste',
44 | '902' => 'Televoting, Gewinnspiel', */
45 | ];
46 |
47 | private
48 | $fritzContact, // SOAP client
49 | $fritzVoIP, // SOAP client
50 | $prefixes = [], // area codes incl. mobile codes ($cellular)
51 | $cellular = [], // cellular prefixes
52 | $countryCodes = [], // country codes
53 | $ownAreaCode,
54 | $fritzBoxPhoneBooks = [];
55 |
56 | /**
57 | * @param array $config
58 | * @return void
59 | */
60 | public function __construct(array $fritzBox)
61 | {
62 | $this->fritzContact = new x_contact($fritzBox['url'], $fritzBox['user'], $fritzBox['password']);
63 | $this->fritzBoxPhoneBooks = $this->getPhonebookList();
64 | $this->fritzVoIP = new x_voip($fritzBox['url'], $fritzBox['user'], $fritzBox['password']);
65 | $this->ownAreaCode = $this->fritzVoIP->getVoIPCommonAreaCode();
66 | $this->getPhoneCodes();
67 | $this->getCountryCodes();
68 | }
69 |
70 | /**
71 | * returns list of phone books
72 | *
73 | * @return array
74 | */
75 | public function getPhoneBookList()
76 | {
77 | return explode(',', $this->fritzContact->getPhonebookList());
78 | }
79 |
80 | /**
81 | * returns URL data
82 | *
83 | * @return array
84 | */
85 | public function getURL()
86 | {
87 | return $this->fritzContact->getURL();
88 | }
89 |
90 | /**
91 | * get a fresh x_contact client with new SID
92 | *
93 | * @return void
94 | */
95 | public function refreshContactClient()
96 | {
97 | $this->fritzContact->getClient();
98 | }
99 |
100 | /**
101 | * returns area code from FRITZ!Box settings
102 | *
103 | * @return array
104 | */
105 | public function getOwnAreaCode()
106 | {
107 | return [
108 | 'code' => $this->ownAreaCode,
109 | 'length' => strlen($this->ownAreaCode)
110 | ];
111 | }
112 |
113 | /**
114 | * checks phone book indices against list of phone books from FRITZ!Box
115 | *
116 | * @param array $phoneBooks to proof
117 | * @return void
118 | */
119 | public function checkListOfPhoneBooks(array $phoneBooks)
120 | {
121 | $message = '';
122 | foreach ($phoneBooks as $phoneBook) {
123 | if (!in_array($phoneBook, $this->fritzBoxPhoneBooks)) {
124 | $message = sprintf('Could not find phone book #%s on FRITZ!Box!', $phoneBook);
125 | }
126 | }
127 | if (!empty($message)) {
128 | throw new \Exception($message);
129 | }
130 | }
131 |
132 | /**
133 | * get numbers from a phone book
134 | *
135 | * @param int $phonebookID
136 | * @return array
137 | */
138 | public function getPhoneNumbers(int $phonebookID = 0)
139 | {
140 | $phoneBook = $this->fritzContact->getPhonebook($phonebookID);
141 | if ($phoneBook == false) {
142 | return [];
143 | }
144 |
145 | return $this->fritzContact->getListOfPhoneNumbers($phoneBook);
146 | }
147 |
148 | /**
149 | * set new entry in phonebook
150 | *
151 | * @param array $entry
152 | * @return void
153 | */
154 | public function setPhoneBookEntry(array $entry)
155 | {
156 | $this->fritzContact->getClient();
157 | $this->fritzContact->setContact(
158 | $entry['phonebook'],
159 | $entry['name'],
160 | $entry['number'],
161 | $entry['type']
162 | );
163 | }
164 |
165 | /**
166 | * Clean up test number string to ensure no characters other than those are
167 | * used by the call monitor
168 | *
169 | * @param string $number
170 | * @return string $number
171 | */
172 | public function sanitizeNumber(string $number): string
173 | {
174 | return $this->fritzContact->sanitizeNumber($number);
175 | }
176 |
177 | /**
178 | * get an array, where the area code (ONB) is key and area name is value
179 | * ONB stands for OrtsNetzBereich(e)
180 | *
181 | * @return array
182 | */
183 | private function getAreaCodes()
184 | {
185 | if (!$onbData = file(dirname(__DIR__, 2) . self::ONB_SOURCE)) {
186 | throw new \exception('Could not read ONB data from local file!');
187 | }
188 | $areaCodes = [];
189 | end($onbData) != "\x1a" ?: array_pop($onbData); // usually file comes with this char at eof
190 | $rows = array_map(function($row) { return str_getcsv($row, self::DELIMITER); }, $onbData);
191 | array_shift($rows); // delete header
192 | foreach($rows as $row) {
193 | ($row[2] == 0) ?: $areaCodes[$row[0]] = $row[1]; // only use active ONBs ("1")
194 | }
195 |
196 | return $areaCodes;
197 | }
198 |
199 | /**
200 | * sets the arrays with area codes and codes of mobile providers: from
201 | * longest and highest key [39999] to the shortest [30]
202 | *
203 | * @return void
204 | */
205 | private function getPhoneCodes()
206 | {
207 | require_once (self::CELLULAR);
208 |
209 | $this->cellular = $cellularNumbers; // we need them also seperatly
210 | $this->prefixes = $this->getAreaCodes() + $this->cellular + self::SRVCSNMBR;
211 | krsort($this->prefixes, SORT_NUMERIC);
212 | }
213 |
214 | /**
215 | * sets the array with country codes: from longest and highest key [7979]
216 | * (Russia) to the shortest [1] (USA)
217 | *
218 | * @return void
219 | */
220 | private function getCountryCodes()
221 | {
222 | require_once (self::COUNTRYCDS);
223 |
224 | $this->countryCodes = $countryCodes;
225 | krsort($this->countryCodes, SORT_NUMERIC);
226 | }
227 |
228 | /**
229 | * returns the prefixes
230 | *
231 | * @return array $prefixes
232 | */
233 | public function getPrefixes()
234 | {
235 | return $this->prefixes;
236 | }
237 |
238 | /**
239 | * return the area code and subscribers number from a phone number:
240 | * ['prefix'] => area code
241 | * ['designation'] => area name (~NDC: national destination code)
242 | * ['subscriber'] => subscribers number (base number plus direct dial in)
243 | *
244 | * Germany currently has around 5200 area codes, length between two and five
245 | * digits (except the leading zero).
246 | * @see https://de.wikipedia.org/wiki/Rufnummer#/media/Datei:Telefonnummernaufbau.png
247 | *
248 | * @param string $phoneNumber to extract the area code from
249 | * @return array|bool $area code data or false
250 | */
251 | public function getArea(string $phoneNumber)
252 | {
253 | for ($i = 5; $i > 1; $i--) {
254 | $needle = substr($phoneNumber, 1, $i);
255 | if (isset($this->prefixes[$needle])) {
256 | return [
257 | 'prefix' => $needle,
258 | 'designation' => $this->prefixes[$needle],
259 | 'subscriber' => substr($phoneNumber, 1 + $i),
260 | ];
261 | }
262 | }
263 |
264 | return false;
265 | }
266 |
267 | /**
268 | * return the country code and country from a phone number:
269 | * ['countrycode'] => country code
270 | * ['country'] => country name
271 | * ['national'] => NSN: national significant number
272 | * @see https://de.wikipedia.org/wiki/Rufnummer#/media/Datei:Telefonnummernaufbau.png
273 | *
274 | * @param string $phoneNumber to identify the country code from
275 | * @return array|bool $country code data or false
276 | */
277 | public function getCountry(string $phoneNumber)
278 | {
279 | for ($i = 4; $i > 0; $i--) {
280 | $needle = substr($phoneNumber, 2, $i);
281 | if (isset($this->countryCodes[$needle])) {
282 | $countryCode = '00' . $needle;
283 | if ($this->countryCodes[$needle] == 'Kanada') {
284 | $countryCode = '001';
285 | } elseif (substr($needle, 0, 1) == '7') {
286 | $countryCode = '007';
287 | }
288 | return [
289 | 'countrycode' => $countryCode,
290 | 'country' => $this->countryCodes[$needle],
291 | 'national' => substr($phoneNumber, strlen($countryCode)),
292 | ];
293 | }
294 | }
295 |
296 | return false;
297 | }
298 |
299 | /**
300 | * returns true if prefix is a cellular code
301 | *
302 | * @param string $prefix
303 | * @return bool
304 | */
305 | public function isCellularCode($prefix)
306 | {
307 | return isset($this->cellular[$prefix]);
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/src/functions.php:
--------------------------------------------------------------------------------
1 | getCallMonitorStream();
19 | // uncomment next line for debugging
20 | // $values = $callRouter->setDebugStream();
21 | if ($values['type'] == 'RING') { // inbound call
22 | // start test case injection (if you use the -t option)
23 | empty($testNumbers) ?: $callRouter->getTestInjection();
24 | $callRouter->runInboundValidation(); // central validation routine
25 | } elseif ($values['type'] == 'CALL') {
26 | $callRouter->runOutboundValidation();
27 | } elseif ($values['type'] != null) {
28 | $callRouter->setLogging(99, [$values['type']]);
29 | }
30 | $callRouter->sendMail();
31 | $callRouter->refreshSocket(); // do life support during idle
32 | $callRouter->refreshPhonebooks();
33 | $callRouter->setCallMonitorValues();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------