├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── packages ├── packages~ ├── platforms ├── release └── versions ├── DISCLAIMER ├── LICENSE ├── README.md ├── client ├── fonts │ ├── Coda-Heavy_gdi.eot │ ├── Coda-Heavy_gdi.svg │ ├── Coda-Heavy_gdi.ttf │ └── Coda-Heavy_gdi.woff ├── index.css ├── index.html ├── index.js └── templates │ ├── arfcn-bands │ ├── arfcnBands.html │ └── arfcnBands.js │ ├── arfcns │ ├── arfcns.html │ └── arfcns.js │ ├── basestations │ ├── basestations.html │ └── basestations.js │ ├── country-codes │ ├── countrycodes.html │ └── countrycodes.js │ ├── games │ ├── games.html │ └── games.js │ ├── gsm-readings │ ├── gsm-readings.html │ └── gsm-readings.js │ ├── paging │ ├── paging.html │ └── paging.js │ └── scanners │ ├── scanners.html │ └── scanners.js ├── example_settings.json ├── grcs ├── DISCLAIMER ├── LICENSE ├── Readme ├── airprobe_rtlsdr.grc └── mcc-mnc-table.csv ├── package.json ├── packages ├── arfcn-bands │ ├── arfcn-bands-tests.js │ ├── arfcn-bands.js │ └── package.js ├── arfcns │ ├── arfcns-tests.js │ ├── arfcns.js │ └── package.js ├── basestations │ ├── basestations-tests.js │ ├── basestations.js │ └── package.js ├── bts-broadcasts │ ├── bts-broadcasts-tests.js │ ├── bts-broadcasts.js │ └── package.js ├── countrycodes │ ├── countrycodes-test.js │ ├── countrycodes.js │ ├── package.js │ ├── private │ │ └── mcc-mnc-table.json │ └── seed.js ├── detectors │ ├── detectors-server.js │ ├── detectors │ │ ├── changing-basestation-tests.js │ │ ├── changing-basestation.js │ │ ├── missing-channel-tests.js │ │ ├── missing-channel.js │ │ ├── new-channel-tests.js │ │ ├── new-channel.js │ │ ├── paging-tests.js │ │ ├── paging.js │ │ ├── signal-strength-tests.js │ │ └── signal-strength.js │ └── package.js ├── gsm-readings │ ├── gsm-readings-tests.js │ ├── gsm-readings.js │ └── package.js ├── gsm-scanners │ ├── gsm-scanners-server.js │ ├── gsm-scanners-tests.js │ ├── gsm-scanners.js │ ├── package.js │ └── scanners │ │ ├── p1-kal-tests.js │ │ ├── p1-kal.js │ │ ├── p1-rtlsdr-scanner-tests.csv │ │ ├── p1-rtlsdr-scanner-tests.js │ │ ├── p1-rtlsdr-scanner.js │ │ ├── p2-bts-test-file.pcapng │ │ ├── p2-bts-tests.js │ │ ├── p2-bts.js │ │ └── test-scanner.js ├── status │ ├── package.js │ ├── status-tests.js │ └── status.js └── threats │ ├── package.js │ ├── threats-tests.js │ └── threats.js └── server ├── app.js ├── fixtures.js ├── games ├── hide-and-seek.js └── trick-or-treat.js ├── main.js └── scanners ├── helpers.js ├── p1KalHack.js ├── p1KalRTL.js ├── p1RTLSDRScanner.js ├── p2Airprobe.js └── p3Airprobe.js /.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | 1.3.0-split-minifiers-package 14 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | j1w24ezfzul1802sib 8 | -------------------------------------------------------------------------------- /.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base # Packages every Meteor app needs to have 8 | mobile-experience # Packages for a great mobile UX 9 | mongo # The database Meteor supports right now 10 | blaze-html-templates # Compile .html files into Meteor Blaze views 11 | reactive-var # Reactive variable for tracker 12 | jquery # Helpful client-side library 13 | tracker # Meteor's client-side reactive programming library 14 | 15 | standard-minifier-css # CSS minifier run for production mode 16 | standard-minifier-js # JS minifier run for production mode 17 | es5-shim # ECMAScript 5 compatibility for older browsers. 18 | ecmascript # Enable ECMAScript2015+ syntax in app code 19 | 20 | autopublish # Publish all data to the clients (for prototyping) 21 | insecure # Allow all DB writes from clients (for prototyping)harrison:papa-parse 22 | twbs:bootstrap 23 | mrt:moment 24 | ecwyne:mathjs 25 | underscore 26 | marvin:arfcns 27 | marvin:arfcn-bands 28 | marvin:basestations 29 | marvin:gsm-readings 30 | marvin:gsm-scanners 31 | marvin:status 32 | marvin:bts-broadcasts 33 | marvin:detectors 34 | marvin:threats 35 | marvin:countrycodes 36 | http 37 | -------------------------------------------------------------------------------- /.meteor/packages~: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base # Packages every Meteor app needs to have 8 | mobile-experience # Packages for a great mobile UX 9 | mongo # The database Meteor supports right now 10 | blaze-html-templates # Compile .html files into Meteor Blaze views 11 | reactive-var # Reactive variable for tracker 12 | jquery # Helpful client-side library 13 | tracker # Meteor's client-side reactive programming library 14 | 15 | standard-minifier-css # CSS minifier run for production mode 16 | standard-minifier-js # JS minifier run for production mode 17 | es5-shim # ECMAScript 5 compatibility for older browsers. 18 | ecmascript # Enable ECMAScript2015+ syntax in app code 19 | 20 | autopublish # Publish all data to the clients (for prototyping) 21 | insecure # Allow all DB writes from clients (for prototyping) 22 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3.2.4 2 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | aldeed:collection2@2.5.0 2 | aldeed:simple-schema@1.4.0 3 | allow-deny@1.0.4 4 | autopublish@1.0.7 5 | autoupdate@1.2.9 6 | babel-compiler@6.6.4 7 | babel-runtime@0.1.8 8 | base64@1.0.8 9 | binary-heap@1.0.8 10 | blaze@2.1.7 11 | blaze-html-templates@1.0.4 12 | blaze-tools@1.0.8 13 | boilerplate-generator@1.0.8 14 | caching-compiler@1.0.4 15 | caching-html-compiler@1.0.6 16 | callback-hook@1.0.8 17 | check@1.2.1 18 | ddp@1.2.5 19 | ddp-client@1.2.7 20 | ddp-common@1.2.5 21 | ddp-server@1.2.6 22 | deps@1.0.12 23 | diff-sequence@1.0.5 24 | ecmascript@0.4.3 25 | ecmascript-runtime@0.2.10 26 | ecwyne:mathjs@2.7.0 27 | ejson@1.0.11 28 | es5-shim@4.5.10 29 | fastclick@1.0.11 30 | geojson-utils@1.0.8 31 | harrison:papa-parse@1.1.1 32 | hot-code-push@1.0.4 33 | html-tools@1.0.9 34 | htmljs@1.0.9 35 | http@1.1.5 36 | id-map@1.0.7 37 | insecure@1.0.7 38 | jquery@1.11.8 39 | launch-screen@1.0.11 40 | livedata@1.0.18 41 | logging@1.0.12 42 | marvin:arfcn-bands@0.0.1 43 | marvin:arfcns@0.0.1 44 | marvin:basestations@0.0.1 45 | marvin:bts-broadcasts@0.0.1 46 | marvin:countrycodes@0.0.1 47 | marvin:detectors@0.0.1 48 | marvin:gsm-readings@0.0.1 49 | marvin:gsm-scanners@0.0.1 50 | marvin:status@0.0.1 51 | marvin:threats@0.0.1 52 | matb33:collection-hooks@0.8.1 53 | mdg:validation-error@0.1.0 54 | meteor@1.1.14 55 | meteor-base@1.0.4 56 | minifier-css@1.1.11 57 | minifier-js@1.1.11 58 | minimongo@1.0.16 59 | mobile-experience@1.0.4 60 | mobile-status-bar@1.0.12 61 | modules@0.6.1 62 | modules-runtime@0.6.3 63 | momentjs:moment@2.10.6 64 | mongo@1.1.7 65 | mongo-id@1.0.4 66 | mrt:moment@2.8.1 67 | npm-mongo@1.4.43 68 | observe-sequence@1.0.11 69 | ordered-dict@1.0.7 70 | promise@0.6.7 71 | random@1.0.9 72 | reactive-var@1.0.9 73 | reload@1.1.8 74 | retry@1.0.7 75 | routepolicy@1.0.10 76 | spacebars@1.0.11 77 | spacebars-compiler@1.0.11 78 | standard-minifier-css@1.0.6 79 | standard-minifier-js@1.0.6 80 | templating@1.1.9 81 | templating-tools@1.0.4 82 | tracker@1.0.13 83 | twbs:bootstrap@3.3.6 84 | ui@1.0.11 85 | underscore@1.0.8 86 | url@1.0.9 87 | webapp@1.2.8 88 | webapp-hashing@1.0.9 89 | -------------------------------------------------------------------------------- /DISCLAIMER: -------------------------------------------------------------------------------- 1 | DISCLAIMER of SDR-Detector 2 | 3 | 1. This product is meant for educational purposes only. It is, by any means, a Proof-of-Concept (PoC). We can not guarantee that this software is able to shielded against surveillance of any form. 4 | 2. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of this software. BEFORE using our software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of software like ours, to see if this is permitted. 5 | 3. If you ever encounter a problem with our software or feel that your intellectual property has been hurt or not handled according to its copyrights, please contact us. 6 | 7 | * LICENSE: https://github.com/He3556/SDR-Detector/blob/master/license 8 | 9 | The above copyright notice and this permission notice shall be included in all copies 10 | or substantial portions of the Software. 11 | (GNU GENERAL PUBLIC LICENSE) 12 | 13 | 14 | This project contains the following code and data: 15 | GNU Radio Block 16 | - airprobe_rtlsdr.grc 17 | https://github.com/ptrkrysik/gr-gsm 18 | http://gnuradio.org/ 19 | 20 | - Mobile Country Code Table (http://mcc-mnc.com/) 21 | The MIT License (MIT) 22 | Copyright (c) 2016 Mustafa Al-Bassam 23 | https://github.com/musalbas/mcc-mnc-table 24 | 25 | 26 | ------------------------- 27 | developed by 28 | He3556 https://github.com/He3556 29 | Marvin https://github.com/marvinmarnold 30 | copyright 2016 31 | dm-development.de 32 | ------------------------- 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | This project is based on: Nodejs, npm and the Meteor platform. https://www.meteor.com/ 3 | Other components (like GNU Radio, Wireshark and GR-GSM) have to be installed in order to use SDR-Detector. It's also possible to expand the system with detectors for **WiFi, Bluetooth, UMTS and LTE frequency bands**. (using existing open source projects) 4 | 5 | **The software (stand alone detector) will be compatible to:** 6 | [Stingwatch](https://github.com/marvinmarnold/stingwatch) (Cordova based Stingray (IMSI-Catcher) detection) 7 | [Meteor ICC](https://github.com/marvinmarnold/meteor-imsi-catcher-catcher) Meteor-imsi-catcher-catcher (Meteor package for client + server side IMSI-catcher detection.) 8 | [API Client](https://github.com/marvinmarnold/StingrayAPIClient) (more details soon) 9 | 10 | *** 11 | 12 | ![Image of SDR-Detector] 13 | (http://smartphone-attack-vector.de/wp-content/uploads/2016/05/SDR-Detector_GSM-Scanner.jpg) 14 | 15 | ###[Usage](https://github.com/He3556/SDR-Detector/wiki/Directions-For-Use) 16 | 17 | ###[Detections](https://github.com/He3556/SDR-Detector/wiki/Thread-level-and-score-calculation) 18 | 19 | ###[WiKi](https://github.com/He3556/SDR-Detector/wiki) 20 | 21 | 22 | ``` 23 | //When all software tools and libraries are installed - 24 | download (or clone) the project to your harddisk 25 | and edit the settingsfile to your needs. 26 | Than generate a python script with: 27 | gnuradio-companion grcs/airprobe_rtlsdr.grc 28 | 29 | // Now you can run the detector as `root` in order to capture data 30 | sudo meteor --settings settings.json 31 | By default, Meteor runs the webserver on `http://localhost:3000` 32 | ``` 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /client/fonts/Coda-Heavy_gdi.eot: -------------------------------------------------------------------------------- 1 | Ëۊ� LP��K@�jzI� 2 | Coda HeavyFVersion 1.000; ttfautohint (v1.4.1)Coda-HeavyBSGP�t�o~rV}�����c�g 3 | i9��C�M� �)@w�zwx^4�?w��1�t5�/��c(� 4 | �Ũ�l�%>���Ԇ��}\*�+MN��!��I�[�����e�?&���8���a�]�k�kҭ����>l��$a,!�_����J?��d_Š6[�ӳR�Ļ[MT� �x= ��-=�ݧ6%��0�m"�iY|��}�� �@��%�~��ćX��.i���O��" k�^?u����� �ic����`����?��9=ܬ�N�^Պ&�Շ�0�~d�l�v��� �7�.���Rcѥē�!:���<��S��{� �d�/v�|�s��N� F���zS�.N� Z-��W�� 5 | ��0����@'h��M�Hr3���ܳ�^?���dz�OnLE[�bk7�ތʎ��A7��+�j.�d*!�Qq<̪�hC�q���s 6 | [�^j� �ږ.M�}Gh[�!P�J�9"������S��Y^X�{+y]��)�����GX3� `.��UN���qDr�1-uy���J�-��I��"A6)�&JIK �~KD�gj�.������P:vA~���!_l �Y���B��PuK�~�1&Ȯule`�2wd��r��G"�ER^��ru��f9��B��1���z@���X- ������9�� 1����B; ���!EeD^�!ӥ 7 | #4-C 1"+;!�������$�0& 8 | A,e�Ay��'�}�I��- ��f�8 ��*���ô�J�`��ހ��̗Խ4�X�?1���^�����r~�b�l�|Ft��b�e˗�f��BQ(J� ��Rg��eBf�����n3G3���8�侠����)�3�ٕ��׉�R�=V}T����.'��4�܋&��<��[�x�� D`%p­� 11 | ��2A���>n�$v�-#�C��&�sT�ev, �c���KM֙���#>G0���"�J�𱅭b�PV���|���\�W��ӕ��o]u�ߋj��/�K�ip)b��5� ��&>˿�E����/MwAg���lJ�f?�O�Wμ��'���<��� 12 | s����������J��#.����k��u ���R2����� S/�������f� 13 | �.�L�l���\s�F��9h�A���@��a�Bt����� 14 | N�ll��3bj�НGf a\����9B�Z�B�6�2����66�z��R~�̬m� ���S�f›�`�b��/3�6�KE��v#~.+{�|֪�fՀ3��e�mQ @c��A�f��F4�"���~#-&@̎,3圳ʚ���Jb������7��`�c�r��~e9U���Xc�01T ��r�������,0^G�M�p��~��nB9r��c��_��F��b_q 2l�:�BR�R@C�i�?� ^ܯ��z�踂C�A1 15 | ��#&�؃ z#����h����b>*f�]g�6NJ��{���8��q��%�J)��l�T� �(8���;�8)�ɪ$yaj&b/y �U�R� q� 16 | �1�*},�7�L�� 8�PH��dC�:� �̑��!�M �"� 17 | ��°�O�x�E~n�%��O-^h�k�~~�uy���W��_����h�AQX 18 | �e�M���Ǘ>'���"����|B�co�2����W�\)s����t|$*�)���2#����4�s􀜮h4��0���|��Ԉ�.�x���Rվ��#�g8�����Z�Ϸ��i�l�D�� /)p1t�$�� p6Û�a�'�aI�� 19 | \�S�n$�s�3x�=�Y�}.XgBax�"|��)X�(k�i���Y�+� 20 | Q���2�/�X���<@_o �B�k�\�PS�K݇)H�#< �?D��n�]r��)�c��9P�Cq���h2��e:J ��4�Q� �)2��x�2Dؓ�0�x6��<-A9Иc�T�C� Ե0�S�E����ލ�[��![�5�~#n9�C��7�Ļ��8ւH��<��g�y$�"��}�W �����~=�d���k��n������(��1XH"��?�P۷ ��m�B�!:�.���p6t�>���&���¾�b�`�]���)�T�fOm���!`�����o!�$�5��ZB�������nP`�? b_ڳ��{Ed�)؀��.�o�?�*m�S��:j`�;��-�x(�0U��(��!��(�X�G�lE���tz��wl� S��L.Kf�G;_E�?2�\�΁����ok9p� o���z��&�5�,ܡ��zǸ ���H/y1E�P�!(��(I#�̏7B>G�ᭉ��_y���Kg�����_ 21 | ��W��|�>@�������>5"��o� �g�$��� �����̖���Tl@��)[L[=/�r��l K�/@��j�A]H ��@ ��*����tM��8�9R)�ȶ�� K7�@R@�K�.q���rk]F�[<��<pe��mg�/3�bph���Y���Ձvͽ�leU��Zs1�_F�m.�Z]P]SyuJf����A�yss�mlR[�tnѤ�-F��� 22 | �aJ��M�c¶��Fd� '�B�6��]0��џ*�� ��wUS P���br���c�E�q@'��|���K�@��\Si6) E��1#�-9�p\�UI bу��X�b�B��a1���XtU� )D�.ui�3�v�c���&2P�R$���Ζ�%�)J�C�ƨ����:�z��4zI��nM�&�ݛ^�2E 23 | j'Ҷ@jO�Ȇ��?M��V �m]S���NaƟ�f΢?Q�7�h���>� ��v������ ��QJ/G�4s�� �]?�^�z��K�k��G��v�/�BO.��G75�ԏQ�h��'�P"�p���汕 �aQ�G�9�/�Xf9��nP�uQ�l=����*Ō!56W4RUO���Yz��Q��K�}(���6*9���T��ؤ�иc�>6�\~"��K�)Y�RqI�����T�S����?���P(��X�B�Ց��i�źWM"�L� �`q��4���T�㸄T���� 24 | L<�zQ� ��� R�T5�Ho�wRtq���s�ruk��%�����en,��I��~Q��g�vU3�a�]1zr 25 | j�S0���e��`��f��3���E�0���D�,l:x �\�M��Ib�9��ᗓJ�aF}��pOˁ�A)�֊S1�Q�PcM! p �ƛ�kif���q�=6�����QY���J�бe2��Jܘ�㦣��N.��қ�[[r=X�:`xa 26 | \���Ph��0m"U,R�b��!���#j���Cl �?�blV�| I]�.��WhVͲ��k�К̍.�W$]��� 27 | ¸ 28 | ���T];A�'� &ػ����B�0DH��?u�묿�h���q� ������J��<�g��%o��Ђ������ez�Σ�~���W�r�8�!���]�H8�;"�����\���AM�r��c)�����R=BJ����Q%)���Q1Gꈸ�-{��j@�D4P! ʴ!����_ȣ���{�?FtO=yi�ɥ�C�_0�p/�x��̑?�4~p�?����1x\�u�ɲz�cQmY�VSd��A���� Lz1b���10���:�oT2����$s�ꉖ9Q,p3=5ה��tNҐu�3)1#9��A��Hc蓡�c��'Nlq�Z �䛼�\md�-���I]�����}Ǵ���?�p�X����A��%"��wL�8s}��U"�[�ׂ�w�f����E�� M��^6��I'��A.I!�$�� Rհ� �l�Z���ᐵl���D�^/oY��{xu�b�!Oaf��J��B`�i��xR󠾐�ښ�5�(�<(��"Y���i�����tX+��X�N9%=��xGP�Ι.��%�'[���� Y��R���Z��Rz�@*`Ty� 6��x�o�k��bL2f$�T��z8��d�)�L׉��%)'ڃ�Ms�y�����z��ڟ�I�D �ٞ< :GNȻ �m���WK�\�7y�ͬ. �]�!Z�;D� I�chlg���� �ڎ������A 3��P���b#��ӛ�D��-�u�6�(� WTym] O��h�#��2P\�A��%I$$y"�x#AF�� ��kHd����0�{�BoXM�C G H���mلn{R�ω���=3ִ�����\`r��$J3�6i�<�i{��" 29 | ]�S��4��c� �k�fA�s:���n����`L#>QF���.�F[��צ�W��}�$� ���5U��3Uk�1Ҭr���6���qo'/�w���ě~k['����Q�b�>r��d:"vF�W�1շ.��:+� -Z��tC�"U��=6�ld[݀�`S�!����7Cz� e�dH�n쇜Ї��AP���J��b 1ʙJ�ҋ�h���9%C� �Κ:@�� ����7��H��0 ���9��2)��+3[`����R�'���ׁ�~�,N��̒��W�H��4D$�/p�ĥ�_��X?� L6�͗� ��� Mʆ�Վ �:�V���-k�A���w��Y^�6#8�z�k 4��Ҩ� 30 | r�"��� �z��0p�c�����n9S�4���}�m���Y���"���N���n��:7�m�M�G�Aw�C�2���+�b��� "!�.�P)��G�H�?f�4Zp���"���9�<`0P 4�.M���������'�1I�6��7����� >g&j-?0R�.BY�[z�� 31 | y#G��$�2@?A;5n�u9��$�� +��,͈��� ^��]��iP���؈��ΘgL�b�?M��<�M8�5�Y�Io`��&A'2�;'L5Y ��Q%A 2�K�@0� 35 | a��]n�.�C�C(�l2��W(��%�@�[�Px%�Z<������%����\s��K��U� U=ֳص��s��X#�:,֨��m�#�5���b�5Dx�#�@�D�k��PѢ$H�H�����$P��YbC 36 | �$@PH�X��BG� #("�|'���",!�����"rȄ�����"�H�"*Tb�� NXb`La�� �a���`��MX"x'��[�D�N0!4�!4Ph�'�$��?��H��C<���,?���E��O���R|Q�D��_D@�DDJp"W����D���L�"q����W�[gQm� m�Lϩ&Iԓ$h�%�L��&\�I�$˻I��$�8�g�_�Gv�#�w����r�Gxe��w��f>*g�*g�� �2�S0;)�K�M��r��_������i��M�E6J�MD�u�eQq�E� 37 | .�� 38 | �$h�`�FF�X(�F��m��c$h�`�JF��TnPQ�F�>4`�Q�F+�cFU�2�ёeFB����m[*3TfLQ�6�=� ��c`h��TU�IآH~>�x� [8�d�֦Lj`��6�#@Gj�jb� րȢ5�%iqh@}h �ր�Q h#Ah,A҃b7J � ��% A,�<1΂p�΂�΅bs�Xbu��Z�!��ԡ���!���Lga��c=�g�����$8��4��@�Iā����A����@�q��9��� Ё�)�@��� n�": �����#�@����n����#Bш �� D��R-{�L���([6/�HZ:G!hP��шZ 39 | A!h��!h ��P$���4R�@l�ʀ�H[9��[%�H[0�ق�@l�̠6d��P4���6���!m� ����6��@m�Ѐ�����>�^6������כ���<�����~O��������?����?��7���q�T�S����1���ّ�f�6i�����m^6��s��}��m'�a�lmA��x�'�k���o����~�����~^o���<����y�w��'w������7y����ww8��wK��wvn����tݗ�wv�ݧw�����.7���i���Ŧ���CM����o}-7�o}Zo���i���+M�os�nq��i�&�0�s�!����M̛�֛�ox�7�oxXo ��0�1���� ��ogx [�����C�Jgx��|7�gq�n9������V�f�X^ ��0�A��axo��&o���{ 百V+���vs�� �t����x8�.2��L��8ˣ�ts.���2��ˠ�t.�`؃-��� �%�bL�+�.��4r� 40 | Ƅ�FXы���D,h��c@.�+$�EX�K�z��.�V6R휱���J��.مE-z$�;E�l��>��u���w�P��� �`��� ���d��!�4��C����g1-m���Zm��;l�6�v�X�C��f��ߥ��.�o�]�(ߐ��(��]�(߀��(��]�)o���C�+���~�oл�Q��w��~B���w��~���!w��B����u(�B����t�n���Q�wAF� �%�v�nл�Q��w�F� ���|J7�.� 41 | 7�| 42 | 7�.�J/���/�-���c��/5;�ļ��'� �c���Ķy�K�N��/����/�;�ľ*w�ľ 43 | w�ȡ���Y/�|bK��rK�xI/;�%^,�Kaѥ^"��$�x���x��6%�Ӿ~%��|���N����N�8��'|�%��F*��^����~�����_�w�I~����u���N� /N�^;Ĥ�I;�J�hx��?&�N� /�N�_A;줾�;�I}��=$瓜�')9�I9��%$䓜�I����� 8A�l9ܓ�Nz�Sצ>,�d$𓞢I���I蓞�$�D砒z��I�s��Bq��8*p���J�H�s��q � 44 | I�'8���'8 '���N,��$�bs�I=�sڒ{I�q$� �e$�I�t�x�^��簒{��I=�9�$��9�N;"��)|#= ӆz�)��OD��H'�#=�#=Z ���Y���: �#8PN3��q�� �#8��#8S�F�<��Fq`)���b3�A=�3ڂ{H�q��e�H�t�{�3�A=�3ݠ����^.���A>�g� �F}�Ҍ|�!���3�A>�3��|ϭ��ϲ �#�G�1���|Aǔ��} ��1�x#h#��~z�ױ�����0JĿ�E����A*J���@WA)� 45 | |�K��)0Jb�������A)�7� )�K;��Ԡ,`�ͯQ�或@%���c?�E���A,��� ,�K]k�-(E������~(����?H�~��?�~���?h�_� ���^` �� re�(����r���)�tr�V8��T�ޔ)�Y�sD6�U�+��c�oO{x�M���;��K�����8s�2|iE�p�=��F�vI�Z�qHSM��[��A9�T b�(���AD�:E=ҧGI Aa���%m`°���x7�R��DX[�'Rt�G,���;#K6)@Y��^������b���/a>�g�[le��ٞO������UǢ��&�t{P |W�C�$��6qBI���Ddm���1Ak�Y'z"��Ҍ��"u�Ƨ�9n�S�<�y�%�1%���,�D1~S&y��ק1����K����E�.�5��Ӵ�B�V��i�hZK( t�5�K���I��P �u��V����M�ٱ��u��k�.u��ׇU�Uk� ��:NOɷZ_��@؀�ɘ�r���}c���Z\ś��q�~@ ����s}����2��mx3���d2Qeb�A��&,���M��휣k l���M���F��p���= x%�YG��"��D�T�c 53 | �ܲ�:o�gb �e���ZsLd 54 | �3��_p���5��=e��M�����՚ Ѱ���j�،ʂ���l��D���¨:8` 55 | Xgɰ�X>=��H�N�+�]�ҚP��G�1J���I���i�+,H.�-X�҄�Ѵ���(��o�-�rw岚+KU4�!4M&�f����iLF�IҢ:�*%B�RFw=��0��ǹb%�j/�;���YBX���;��B�j�YK��� 56 | ��F���SYv\r�'�t}�C��\�ZT�x�v$r���vn�� )�V� ��땝O����a�%[�����]ky};�i �����S{w4`:+�G��Q�D��Bi[rm�4eX�r�G�?:�61h����ҕT�.V z2���Ջ�,�T����R��� �;�z���-t+�i&���ga�"��e�1����.���H���x4kS*�������q �)#Mʩ*=D!�Ɣ�#B1(3a�B`�b��2�Zj�M�����!�9���&��ӑ���w ������<�X0XTwB(S��JB4�w� !�G检_�o���k���Q 57 | �;�s�T~$�h�/�33 �r1������*?�+#�l�v�Yr����U K0��z�ҎSƀDŽE��]]��V���$�"�Ӗݙe���$?���+aI�I� ]�4�.m�p>M���[��J�yw�w]�I��^l��:��zB�wX}�N����5�@Έ�̅nGۋ��9������%a����õ1��<�x��IN�Ç����^ ��'���}���])o�6���w����4vK%�����;��X}/J�ڨ�C�t�����4��F|mw�Iy 58 | p�s3��d �pLӞ�9R�{�T��p��$"�gv�vR�#ï����Z,->�a�iqlk̒�y��t��l��bR!bm��U��Zܞ+ �Eѩč��C�(֐F�z��H�T�"�d����BF�������-�� 59 | GX�kރ�}P�i�‡�(κ��Sd*0�i%@6�{"��*)����ڠ�o���G&�@����-Z���|:����!oĹ �۹�� x���l�Lo��YT�X�V�#Z�:��=9�T"㔚M��]�d3u>��e�� ���j�Fl���pk��Ls���+p����Z�0�5�t�����8���� qI)k�}��Y�.��}͂�|ɵ��nx,��Aq���L"A#&�H�y�+����"0�D�(����I'�M�T\�8!����5l�!j��wW��` �BTI�f�l�d �-v�t},�e@�8�ł� �~��4�˸�<�p`J�#e��f����H;�����\P����T�|�@𚵼1:��Bg�g�!�e�Z!�:Wsr�¬�)�O���@��� l/ �#��4`�?nj��� �.���)�MZ��R�1R�n҂0W��1�&�E v������*V� 60 | J<�62��F[�?2�Z�����GA[�!$�%�!�Q��֙Y2B׿�D$��[�YPF�Kv"�T�� ��D��-��E/f�Kb@�k�uC�6��q�c���z��jybp6"��!{Hқ�pQ.k����c�zc�Ѳ�0���Y�����H 1,� ؘC��KS:�n��n� �mV��Kp�oL��a1[�sv/�s 61 | �w �#�|@ � 핂0<�A0��{� 62 | `/ пN,I�\�?�0�(��y"8�?��xV�:@�x����L�*�0c�.`��r ��������5��>�\����(v�+G�h|�9�6�.��U��b�o��_}f� �i�K��S���K� �l��.N�������a���u�����28-�.U��-����o���1�Ρg���5�翛�?(h�a'h�n��h���.��8p�TEY$���V�~��բ*�<���u���������e�.�ddb:��?�$��WGOÇ%�� ^`jsO�1����5n�+5��W(ҫ�ШON���΢�t�.�:��%�cI���}-��.Ԯ_`�'d3C/�k�]e#����� ���`VL���f��[��_��i�|*�� ?�x������9!)b��$����~ ��߲�R%[PإQ��S��:8oX��f&oAd�]�?(���@����Ve�`�pyvB��\-Ƨ�Z�8��V�gے��>\�m��e*)i�j� /G��p\��Uԍ9U��R���KH�Ა�gP���| E?�L�;G6� ��|���6�uhje�C�U�m���u2��0f�2kC`t� EZ���;����]i$��r�C1u�!��$aB�d�(��o:3;b�C�� 66 | ��R3�K�|��j�rVl8Y������1�W�<�u��10���Z焖Z��gI���E�ɪt^4��E����5����H�a=�κ���M8V뤞?��"���@� �<�j�z1������p9��d@�J���!�Z������O���_XA`�x�OƫZ�CyA|7�,�B�teb��#���� .���[��JRWBt9�TB��B �6�����QG��� 67 | ��2Ds�|�P��+qfk�,�]6 ٱf�>5lT�<�SO����u��ݐڂ_h$.��5?�"��<��'�fղ������0mrX�MLn�ƿ ��6^l�G)�D���R\��D�J������x�*~}V�(���+��Kn͒��se*03�N挊�؉ǫc7�a�4���-��j��"'���:�U Lzo� Cf:�f������Ъ֏�i������#f�t~��n)�(�PC}Qg- iv֣V4=�[h�/P<�9"K�<7���0*��w$�<&)���&)�/\C� �}��;\_��Te� �zB�T�!= \�_��7����:ay��L/�� � ��y ���p 4.w �J��¢���\��y�"PF �R�>D�;K�|�~sq��Bj���o�����&(G�Ap�d�%�id1$�GLt�a�.ĚO�Y �1ҝSFt��bÆ��@�Y����Mn��Md=Ж�(�!y��>�����G>%1��O�*~ʃ�P�TQ�� W��9�r�6/��}l�6YG4���f4$�.f��"�� u�:I9���v���ebH�=9��p���U���C��:����7�����PI�)<>��?�k�^b�Θȑop�W:S^����%��t\��iS����iYlp���p"��d��Jd�I �v�e���� 68 | Ŵu:��"Z�j9섽Q�����^9׳�/"飖"`ž�����5�$�+v�<7v���Z§qyTR�F���38��+����z��4�*�j�rs��°T�8��q�W��E�� "�f��U�r$���W.�4]��/g��F����j&�}�S� �y]�}� ����֧}�䜍7�=x�xM�dtH�@�);go8��z9�k�`h���y ��Y �Q���Dۈ�w�W]w���6�-��y"Kھ�Ay��u��È�[(��9M�(�r5��PdL���F��G��\co.��o� 9��2�T����h�V������Ubu�H� �X~+��d[���%��Q�m�L�h�������޻b�����݇�T����LeG 4� ,{+�+��O̽vb�<� 69 | ԕ�l)I������'fmUQZ̨�AQ�6����� 'v�d�6�ce���л� ��(��������:ԑ�М��� 70 | ��h�Z�圔 KRcS� ����R�OA��E���wI�/� ���Y�dӊ��M�L�Z ��^4 ��G> �"Εzĸ����>��`�Q^7����x$T����#j��m;��=4 C��,�:o���m@�~�K�֛ ������'9:(�8�����b�O{k�����p���;�z�������r� 71 | ����Ҥ���g� �`�F�������k��)�6*���Q\�6�@����f�dm��!{F�e�6HBay�ݚ�ϐ=�H�����p�g0���'񰓨�3GI�[8��9s���jV]/Ʋj���:ǖ���1F�9�n���V�Q��! &�^�a@�d��ax 72 | ���q�� ��U�#b���EU�T���ƀ"�(KE�B`ݕ�q���Kʿ#@i���KGQ`]`������4z������E��P�Y��9{����&��4��)3�=Nrr9N0�xvё媝B�d��/k�㛚���� ��G2��:ɧ6Z�������A�R����,^G�.Ӄ�� 73 | ��X ,L��[hg�EN��9�g��E>�䜌� i��ᵞ�� ��V��������#/�h�����JJ���� H�8��Y�(���ْ�I!��2��b�t��J�-1n�q\>a'�W�}[�-�2�}^ �<�M\�k"�LZB��r��F>#s����#�gh������G�g�f��$��3,fr�����^QB��T!�����EH����dz�24I~S>]��[pՔ��o�l��Ye��������4�⸍�UHy�#�#zu�\��;;t��Tצ�nD�h 74 | !!����U,6܀����8g��٪�R�����#����R�7������p g�5��.ځ ���YG��� Lj��LGEH!o��E�2d����� 鸋���"��~��gR�\ܐF��_� ���Ħ��cSLI��`i�ZH���09+GG� H��e��<�:#�*��6􏄯�q=�t�h̻� ��D�o0Gȣ�=����>2�c�(a�tho8'�0E=BnjI�9���fY�uos�6[`�{��76�����oƮ�刉� BҪ�Hؐ9����߬& q� �����s?}>��il\d�P�\$�����z�9E[.gӈ�����y1 ]�P���O���UB�=�7g>U)<"�8R(q�Gu�q��������*�[Q�MʾWH Z 76 | u��f�{Q]��Xq"�{Q�*���6e�B��m� P8ѳ�*��U�@I�Kl��,��p-����{qafzqEWc���@fщ�>\@�1��{b�+)�~6{$Q\�;0��F�����<\\vGU^����"S!��r�����)������ ��5���0^~����@�k}A�!�b�F)��{�����x���3{{�h�,�}Nu���ƙ^T;�RE����:�a��2���;��Ě8w�����>��ٞ@��]��k�Ѐ&�r���Ž��"K�j��ν0���D3kF �iM46Jxd���a�L��� 78 | ,�����bˮ8��1AR ���1��W�2�{㟤��2v�z�>H"�j���uG����!�Z�l}�B���N�����I�x��kʸ�2Bc���x����ŸRp"�,�(e42���%F�q �3�\��i����GZ�fE��J���x�K|��%[�`Ѿ�K�W����aE��c�ɑ0�n<:-8�/�> ��zT݅�W7��W �ȡ\4����S8]��@ ��1��+��3���� !D�w�2S��5nt�����q�+���w�����UHd�[ 79 | �1��e�h�-�퉷���ߏPJ.P&��"ʌp�&��^��%)�ԥД'����6$���2"��Ե eL�v���U��)w����>�lKHc��d���I�MSL��-��Do� C������b��������k��KE���+T�KG%c��mW�V�� u� 80 | ����GyQu)�&>�{t��R)�0�u���Q8�� B q�%�'g�_ 81 | ��!�>����\�Ĥ�Di�R���RK���(H0͹���V� � b���;�ɵ��'���ƭ�f��`2 �UB�ZMm��9���O7��-A�� 82 | Վ�zܞ��e@�����aF�6�˱2���X�Pm���R�d��E��o�o��$ ��� 83 | ���״V�dIT���0��ZǼP �ٞT�BQ�~� �J���������(�J���1>�UÏn�����\5�Pح?�v�g%�ጆ��ʀ$|�@�+֮1d�*)�e=�!,:�.'9iT��g CY'�4��Ό�yD>o��e� �@?@9����jt�GO��A�p2Qq?2JZ#D�2+���4�,�%l��qB��)m��#ɽ��\s�A��\:�=ʋ:�i 84 | )t*���@�`e�f� 85 | �����-]gYL*D@�;�= �QהCkD�D��v��g6���� �.�${̧.�M�d K���54$N�"���K&���K�sjlm@� �"Hj����3��ʋ;.b��@CI�hŧ��H6�s���e���D���g�y9�'(7-�RwXh���B@ *%��S#xN��i��l������/{P�`��ql�^X�ldf/eZ����"Ƽj���D�܃e�>#sĊY�O(�cݨ �ΰ����Q���5��6#���gZ�ːk�~�'XG%%x=pZ��� �����X�5�[� m��uD$���J���sr���!п[�0L�[y�AC�ˀ+ F�c�3�xJ�.\�~���mR��Eaf��?&֋�:f�\a� ��S� ���_R�����M�!M5�`�z�j{R�\R���U��8I*z&�D(��m$� [*lmpup�[3�b"�� ��(�'��ۢ6�;˥�^�ٛl`!;g��д��a�4��v�,(��F���`�����W��"J��� _�q�B��}�$!���G��Bɴ�b 86 | ��od�}(�(d9c[z��"��^Nn���]0ӻ�}@Q2��^��6|�yP#V��5M͠C|�̰0x�;��w&tE�K\o7�4�a�9 ����,�����F�O"�/�`C��% v葱<0<%�t��(��8�`� 87 | �@ϙ�9�����T5�s����k�% �>BL(�C��ɦ�|��(Q�t� �e�^��g��C�Іǭ,���"~3�g�X &w ��۩� EϺ�AF$�F�p���َFAj����.�>��v�̱ dPEI8ؕd\+]�L�t$�b�j�������� ���ԊfM��QS-L_� 88 | �ҖM5K �y4E��S��O�����[6�f�zE� WU�[���=3�8�h�P���K}�b�bb�5�z���J/PVdE 89 | ��A�;3��C4b�0)E(��+��" ��D 0��� bi=��8��P) 90 | �C0�3�:�]�p�|MQ �V�,)m�U���H`3Bt���J����rcu�A�H5 ��j��-���� hicٟUx���v;��R�/�ԩ�o$���C�%�(���v�(���D-��s���i!���0Mb��sh�;� �� �ʰyA��:��i�~�5�-~ä��ݏ�Oנ�wB�ApZR�m��Y�+8`�� 6��� r����v����������4b�š��hg%T��~��{o����nIrXG0�&9ԙ���$�>��;�gȶ>�ë�A�*+G���r�o�2�@V���F�S��E3�Wa��D��H�Ͳ��7���L7<�jo�)���kȾ�k� 91 | x���H��T #Ei)]�����A��p����Ϸ���]^�S;�� 92 | �).�wBF>�L�����&����"�bD��2 Oj�T�u�|Ym�/��ݧ���Z�:��]���ת�. �������W����m?H'�-R�G���������mߑ��++�bA.��ˋ�� W͑-���k�福V��u�x�;��(�/Q�Ƴ�W "KH����_�3�}�S�����Cy�����ۮ{'� f#���V���.�*�)�k���[��V�qb�k�0k�;������PN�ǂU&�|`����U쌔n�p��{w�����!���'�"�Q쐔"�#S,���2I�y���b 93 | MI���: ��y����ø���E0#�i����Z��ϗ� ��b��]� ����{p� ����:+k�����M���p묐e� �U[���W3n�ћ����l ��dՒ��JM �'���Y���8�~z�\|o���s�� LS� >Vu�N�wp��@xu�O�d�D��|�5ǁ@�Uᐔ`K�V1� 94 | i����5 ��c �T��`���eX�E �N��L"�������(w/�x�N&�h�I�$)�������E>�;e�܃�צg� 5۩{���osPCs����ֲL��+V5U�eڀN֕<�hT�� ��EG��|�7���.�R���ݐ�d<�c Ý ���&��,k iiؓ���p�^����DA��Τb�������~0]$e�ǟP���#��~ +�k�ɍF�E'A㗬�m���W�@/��Ӆ¡��6�1YJ�̧�w�}O&҇d��VAuϋ'Oez5�!���HK�_�{C�� ��}^�L�'r�u������2���h|�-�2 95 | �Ğ� %�4��a�rq!_���� �Rpd[? �*�K��z�#�ԥR��w�,O��Fݖ���PT{I�(1���\�P{�p����@}I���?�H2E��U0�K~������"m����ڳ�g����4��7\#i|�b� 6[A�p���37�w���xe�F#��O�[�a�ȠOD�*"d-Io��$uޏi�FXEaA�����`���|�I�ԺR�ǭ2A�A\,�&���c��q�čA�&̮|�#X�}.��0]C�����4���=Hs��1�� ��x�U^(�ԥ��b�����o���0�c���0���3N����?�#^���sӬ��y%�2�s� � �d"��FV��(bY%5��pm�eC�%0�ѓ�*>���'���1y�����U��p/��}���28l��E���s� 96 | W�<���Ȱ�%�\&�E�e~m�O� s�8шd-����} �b�{5u�۬�����b�����3�Mn ;2��ݙe�aŧ������5( Zݼ����Ƥe��4���HA�~�#DJT��q��H�0%KW��E�����∁ j~s� 99 | ����10!�8਱#U\F�q���x㴜����c����p=�{[p%������KG�A� ����C:k@��Z� �G3%X��J��n$)��c8��ݾ��t����� 100 | ���6͗���l:��u��V��N�^�� "NJ���)�����S� #no��6�� �� �uIM�=��6�\#������ ����\���t>�9�$�$���2sjW�Iզ/L\/�O+��%^�!��mH80���U�.� �u������9����=N3�2�����P&�}آ�&�cT�Y�6"��sSzې�nS\|���o��`Ssc+z&X�]Y\!�Ȑn/t/G�A�Т\u���%����� z\`K.��:�[G� YE�O܆��#� ��_��r ��1���'�QM�����g��!�d�6S�^�)�హ�B�5 101 | H���B��Z�K(omCc�\�Fv�A�g���'m��ݫ�_���x��>��AO@�4 ���\b��_� +��E^�虾���<4��C�Z� ����>@ ��X|����pq� ЎN� �.˿d�t���i��9hX3���J��}�T{��Z�Ra�ΣoiAɝ���rz����P!�� 102 | H!�i����VMIY ��@y�aV��a�3G�s �z�w>Ж�P�U��*…r� t� n�"����q�SÊ�V�C:�-��)�����;:����|�h� H����?z�Q|ǃ�D X�&ͦ�T�� U��憷 103 | ����1������t]Y/�̉��� 104 | M|A�j_�f�� D�K$�|����~�� &�s�Am���ʶU9�U�IL���� 105 | xgVL�� }��luQx8������1���&r�F���{T������q��R������v��f��EKuྫྷ���.�×?��a3v�q ��f32Y4M�k�..����Ӽj��4�1צ�j�������`f���xOy`�s�|��4S!A�"���4 ��B�<\p���9�-y� �U�Ŝj�0:nd�;��ho` ���z�����\5_���&J�$ol��D9�� �x�3�B�1� KÃ�K�m�x�KF�4nn��|Z��=k����{�����f%ńs�i���"g��� ���@��l���L�DF���S��G-_��� ����~�c��vA��lP��ǏD�$=0�ފ�DU7u�lj�7E\+y�2�[�Eۼ���C�ޒ�@�Aa���§N����@��H�׬����h�&#WI�����>�L.����€ I�:�Bd�ǜ���WoSW�6�4�c��p;���p;, ���&�n��-Tm�{���d�<�L����Q���."�B� �%Z���Da���+�e��?��4T��Y-��A*�$���Qu �“$<��W���#� �i!]�)+���C��.+�@d��8⚁`N��7 W���h 106 | ��A�(O�n���kG.��̢��u��/�ğc{E�3�Ĥ0M��߿�������PC3Ȣq&?�1�����!m�4�2����bЕ�R�l��3@񑂸�(O+���C���t9!�����6���&�`j�p�*���D��ep����7jP����1�:�UX�����ps-��Jr��C��̑�j�*V|�3�S����>£�]�fC?��#v�)ɒ�0��*k�x�XRA3�6�r �"�A0,�$��q䣓���Z��� |�I�w�'��@A�-z������/�ȣ 107 | �<{Q�E�������.K�׳<:@���I���6���S��p-��+��c��/c/]La��j��Ջ:���$I�����x[�X��]�������M(~I4����+r �=�-G���+ӆ��oё-$�B)�\"l���x�W��v Y���-��{\Pi4b��LZ:�>�$��Őf&�l�^N�hX�P��T#�x�����.����B_�ܤ]4�7���:)3�7-yM�T��|MM:~)���)������r'���h*�,�a$\���Y$��Ha�w�c�a��; � |�� N�����\?S�$&%�x�m��ְ�������h�L��ΐ�Vve>j,�W/8B��������W�\�y5D◮�/��j5h�� ƒTJ~� 108 | ?�~�4X`Z�)r�G�@:@l�+�Q�����y��M��$���/��W�o�T�J�>��dF� *1n���P��bdȋ���; ��N�����^���*��]�l��y|�7����We緐בg%� �q�����M���<���}�A�8�� 109 | �=y��5�m�~u��=q!�;A>N�F�ғ����U��������$��F`#��!X��E\�.�����tA:�a��z���k&A��2p�aЙ��5d ��p�5��T����6��<�5���q�t/g%��W�ڰbk�ŚT�S�k�j9٠����� ���ŷl/l�Q- Y~�c��N#ь�s��*i�|�N��D�� �~�;A$jAC�y�B=�!k��}@�$�b�T�$"�f��N ���N�n �9�-a�$�.k�312��V��Z�% ����n�I&��~#?�f�d�g�$�!�΃1p=�U}�(Z�*��`$� ��+��� DP��S�� �@"r��0�v%7\� @Dz8폖j�# �`D�p�Q�Q���y�/����{٣��e_)�=�)I�L ��!ӭ| �"� 110 | b"�<$��y�'9��Cn���y�Y4B�q|��,�.E@�d#{C�I�� 111 | j� g��<��듈l.�� 112 | I'}�i����,��4p�#���p|"$��τF��RT �*=�Jw74�L�I����x57M��,��<��f��� I��m�Pa���iG�h� ��1tQ%� L�� {����ġx�f��@&�:i �c ]��|e�;8��"��F�v|`��t��� i��2��i�1J��$p}{�Ԕ�*1��ݙƣ˖$qK�&����Nd����Pfl\�^�d�� (uI�Db�zo�4[�D~�G.iɳH�P���@64S�m�s;�D(���7���� ��\>]�� 113 | ��ČŹ�1�#��6�eᲂ%�y��d�d��w��BSt�5�ip���]�H���i��L��e��qlVk��˛?�y�BN��(�a���{��t,���1K41'0ͱ�K�4D5O�*�Y2oǁ�Of�-��� T@.X����0��!��}b��b�� [� 116 | ��шI��Z�8x�4Lq��٩w��LŞ��o���Y��Bՠ�ƍDP<��� ��}´Qֿ@���o��J 117 | ���*��(M@rVz��/�=�E����i���Sߍ�bp����b+�����l��� NM�L�J�r+L�ٸ�^�#±g)�<�} `�$H�b� Ð �3s��i�8h�\b!N"�WT�z5u$I��@#M������@���.Ҷ��FB����j�h-�u�J� �a�$<~���X ��#5�E��4�a�r�q<;M8@/E�WK���z��QL�#j�#�� �nj��<},�$"�D���O=���S�� �G�� R�C�8���E��� �j�Iט)&50 ���{2�,Q�E�s�B������YU�5Xf�� @* �A>A���.��Vf8��c���O:�N�=����9֛�c�5D�B�b��R=�,���WIN3p���k�2�c+����A|6�L���)�b�v�#d� U���ʢ��a��� 118 | (��w��³���Q%`�Yq�6nW&O�� o�T��LbyQ2_%ap% � ���> �11"q��*��d‘�����o� 119 | ��}����/��IfN;P��f��R�YF �k��v�����D[䡹"����0߃p�n�0y�l9 0���$� ��6$��� 120 | ���b����'3vi~����st=�g�p�4g9��4�2| E�%�N��K��A,��?��k�Qƿ (祥,B��h��/��g�D�N���#,�2c��w퇒���6@1�(\��BE�.��R@�U�H�EA�j ��*y|GQn���Ȇ#{�����F�XE4B�uu�|w��n񼶦Dp ���� �?�\���Z���e\�}��8"��v=�_$�����1��0*(��8�1G0��i�)l��� ڎ�O�kA!m�� +���U|P�^Ep��B�’�ͤ���������=�J|`""F�&-*�x($E�1~�P�޼��)T���&����2���;��z�20�~���żG���$NGbhĚkµ�'x����i��/�F������hU��Q0m~�m"a�c�{>�.a��������z��'Z�� �A31|��#w m�=r��ۋ&�臾����>=|Gn͡����A���UB�8-�S5���6|�m����*�~�E�C �S�FN�=���xj��ELQ��S���������"�:v�KRl�'�0��W1�R�ъط-?��X`�Z���h+���!dAFm�B�帾H"y�I�n`<��˨ؼ������QY��P��PĞZ��ig 121 | 6)j}���;� U%@�g�;AA-�=���׿`+(���J[Ï5������"F�հ\�`e��pB��!0�1������ۀK1`���މZT�%��� �-q(�� r�j�h���x�}iSc9�,�F`e�.���oB���u�H�A����,{ [ ���+2�4�h�լ�4�S-$,�l���˫����JDX��!X�ϙ��Hc�� y&�Z��J�WAK!S�[�t��� f^x�=4NJ2��2��Ye���J�/�f����6Ԭ�[�_�r��i�������֬�vZ���;�����Pmۼ� ?PM���sJ�� �N�ۤ?�O�=����cz��U �+��S��G��N��潄H@��N5w��_�ONQ�v�Ӓx���/�GMR0����t�tz�Ѝ���d텛*�$lE���dz%j��L��Vħ�ʀ�91�2�v�f�M9b x 8�r�����w(} �,'D� �RIBV��\�T &\F�-��R�A���9ܼw!!���r���Z�h.7�� �[�٫e�_j�u��$JDrJ-<ͷ��Uǽ;t#5K���v��Չ��� ��R�̢ U����(�!@E3�/�/�8G�� jZ4N-x f J:�m@�ӣ(���1���`�59�U 122 | рֆ)��e��d�Lf�U�)J�y�X#���}��p�)�}u|��� ��������#�e�f����D���`f��lp��#�B>�D��Nڹ���S�@������YT�$T���8���+��qboͤ��d�|c ���!�%�]���K�go�L��E�d �m��0�g�%��6 A+,�[��� �^�1��2N�2Q�0Y�l��3����s�b�[&Wdȉn[��:sy��!�n �唓[ȹ)4b��xFI���]�}�qR<��m��FKG,˻0�P��KN���4�o ql�3�)E�I��4������[����K�谤D�ƚ}�S�ļ����Q8t|��q������[�f���Q�ī��5��1��(�c��c��i�iD�ĎLf����L��o:+`%}�?�2N��i�k���[�xGO/�p ;T/J���Ԇ�N�c���<�dm~o�S��� ^ ��S�6�����S��V:��GJ�"��dW:��1�` �F���+��FP p-��j ��"� _�� al�v�������~J�����qI�*�l�e�U �7,�_����F����G�4�ǀcz��)�p8iR��JV�s��晀$�2}U'@A�=��[J����}�a�wie��2xk�'Lmksb�"fǘ;�H ��6w�1��&���B>�؇�S'v�#�uY�d��$X�(7�+H��,�p ���� ��$Յ6&\���1�=y�$�F��`fP��+���&䔑ϩ��Q���cI�\c��FE6�>n���z��� ��nk ��n��C|Z��ؙɢ��P@M/��"6_���rH��O_b�-�S��nu�g����,8��Փ5��錍�K�z^Ě��̮= 124 | #>��c��#�X,5���\?Ɠ�+�=R�qLn�c��ee����s1�*ΩXTƙ���:�!V��[�G�IM,pcdY8М���a;�4+S-[ �D�;Ad^��k�ȋ��4�����E��Ӆ�W�Q� naN���ߊ5��\*ƱY�A�����:�۹�q0�zGL�4�C���n�> ѐ����9���q�xV�L ����5ꄲ+�!�m������ԱE1n&n��{A��~�\AGw�&߬(X����j4�|@V���x*���W��b?`eR6�q��W𻏉�!��I1k��l���ma�8I'�,\�6��;y�R%N��)O��������ؾ��� �Pdaw<)'�`%���7� Ǝ{���9�֒M,2��^z|��� L4��JM�<�,��uI�K�� 125 | tM��zE�� �9 �b�q��p��)�;n���uw�74 m'�'H�NFH�P9�� 0�L�� 8aT?���&?q��B@ᡜO��SN]�ؔ�7��A����pq5�W+���q�>Sv; ܁�#�*��<�r�J=56I���EV���(��AZ37J��dι���:��7�hW�o��)���&�鐏�ڎ[vQ�)���B3"�1��3m+bnm ,aAף�f�j�S���Gf����(,,��� 126 | �RDX�#ZU%��g���}:7q�Fh>�`(x���[� o+�$42���7h�3�e\ W�C!��L�Pm���zz9{<���m��ʾ��|O����R�twB��L�Q����/9V�`v#���a�<���k��$O�����'B��2$�%q�_��4(ܯ��$���@��(�Ӯ��f��@c��頝o`}�/�Cg���z�����V�������r�?��A���@ ɓ��J����}�r�**�$!�eG����Ȥ���e� MF��2B�A��T9'6��� �v B Vw��O0���������I>%\t� 127 | �i���R�#���ɧHلp ����9�"KBD����(\�8��"��л8#=����7��E|�� &Yb�>�e].��C|���B�L���I����]�)D0���(�����T��^� %�H �!H�0�B������=�� �| 128 | 0�ѓ���Y�LX 129 | ��L��Kɧ��+@.h�vPI��E�MwI���osm�H�C �M_Y���.�(=�Q�E���ق��N�I��x�Ͷ�I��#Oa+ ������ۧ��]��Ԗ i�� �y"��m;�1@s��H�si�jiA���� )�i�xz�g�z�{L^#��%�;m�=���Ed3˄>* T7">z#�����ZZp]RJ�h �L�k&y��,G����ЀR���4ڣ8MZ�HS�K�̅����ŃY�G�X���X��ڔ'�/���J"�Pj��v�_Pr��G��u�`��h�f�%�X��+>t$ҡN0b������Uc��a��Mj���s�4U�p u����T��� ���·���}A4UɃ4� O�CօF�H��l�$�.�dӂ.���ȆX�������{�b^H��� ̭�U-��G�,�P#��{]h�Yo��� �� q��X0[�'�!!I _��::q��N2���J{#�<��!%�?&)�F�s�"��!#�A����y��� 130 | �D6�4�o&������G�H<��~n��ۀ�����i�X~��6U�Q�����.a����.Lv29{\��y�F<$L��+�'��M:C̍�1�a��t�i�C�h�,�6����9#�-�.�e�&�1J��3�C��~���4H�!JA�K[�)\\�������(,�:G���N����-��O`�y�02��!J��S�[��S҄=Y#�Z����޿ �?nCJԠ�e�V���aV5W���gvV�I������S?���T�gkm]�G�p�/-<�+%��Y�(m��C ]�Oh�h����n�D�i򛙾˟#u9V��?��]�V�#���!{������V�2"{W8�⺥okA:ε|�/�N�!�Q��������x�o���&�k�P���y��� ]���ﱲ����N`|��s�����S��=� �9Xa�,��0i`��,N��F�O��v*3,���Hpm0� ��jJd�F�n�������u}�����T�SL[=���3�E'�x�_�4�����h��͔%ޅ��U�Kѧ���z�,p���Xg�V1"Q�7���x�~/!8�����Hg ��%��Seb���G%�o��D�L�N�s��t �Q3�q� 131 | ����V 132 | a��wT+� ����$��6�u��=� N�q4�,��|�\+�s5�����8��� 133 | �Rn��YKa7za@T�G�2��x� ub�J'<�v # 134 | g�0�Y�JP���E�~��> �<�wOs@g� 135 | �i�Gx2T�����˞傐(W&)(� V3�_��� ܗ� ;�9E0>@��j��R؍Q�ma���o��KF4[2bP a�O��'�g��b�"fB,'�dLY[��C�j�N�5GY>��*?���B����*p[�z��#A�*e�0d�6qxPZԨ���#�js��F��rɩTCf����c�^���R���Nu�\��K�M# yb�+(!`��t#�L�X���'�3xsZ� N|�����d���,����`�>u�����u� p��� ���"��X3V2E]=^[сil���4Y;��u���'#�M�m!_���e��垴�e� P������2���"����<� D1t��1v����yuC��&��ΰ4z�� _ 136 | ��Z/���Mu�����FF�e0��2�Ӹi�ͼ�Dـ�` 'FÄy���M�$�� �$�@4��ؙ�@̓��ٶ3J~7;�~ho��|Dm��i�|3C']�g�/DZ����k�*�������e�4��LmE�& 137 | O�A���#�YSIE�S|K�> 138 | C�/��8c�ƻ�r��l��^�{m}B��l�[�4�O�~5)|7qǔ��' ��"`r�k4LJ�e�^_V]�1]=b. ���r�*�N)����N���6��R�R �����&֨ճ���!��2t^c��G�v@ȁ5���X��8��3�ςp;�\��dƻlN�8T\!��]�}�F{TŁ� 4�9�^_ ������Q9�\n��xOm|�����[���;��Dߏf�F�)�L)� �ى1~����:�vg?ִ?�nI�� �U'!�ÕM���z}�ש�^����ipp=���!Wnz+�k��n��p1�6G�W�jh}F�XpR�(���W��1���r3����6=��K�K�i��a���"\+��;���{�e�*S�4|en�R�MF���$-��@)�?#UF�ܧދ42����D���e��أ�����v[�S�J��]1/�$DfY��1^��f�غh�lR  ��$��Q����ѹ;-�dc�A�&s�7Ȍ���%�1��1;t5�|� 143 | ��K#dU9ȅ����h+8h�f"z�1�5P�����E��xz��?d�ў"�X�ǽvL��$��.{� J3;݊,ël@bN`�ݬ�cD�D<�,:�Bm��7�7�%�WP@��%��9��caI 144 | {+pي��B@O�7�����5�g�k�:�K-��7J���+a,U��c�g{O��?���]'ʍ��E5D�\`�7��3 H�6�ɧ6�3$�zh�ыh�R��~!��{� 145 | ^��h`�&����u����2[��� iEi�������2$�r ̈o�V����xWE ?�#��%P�<����N]K9�ɘ�7�)>M �R U��@��G��;�Z���Y,6#�׫d(3+F�k 146 | �&�E�x�� r­��s���ƕi��$'�8tWb�nȿ��P�V:�D=���1�S��M��t4^]�'�P� �)D/F � S�r"`�D�Y3�Gp�f�' *,vf�! ��!(��� s�#d��8�1�Wv��tH��G9���ɜ����)f1��9�d�C_@��g�F��i� ֆ�m�l���ef����ѧc�Ö�F�jj`^@���wڅ' 5>=,xs��"�!���U��^�;���Q�`��WlFtl!�lX�2yv`�`�������>/Q�{W�C��2�Ey6K,���Mz��,΢�b�y���D����'���vv���7t|P�n����qi�E��ŦvɄ��2r_�)[�,đ��w���.4#��jؙ�V�RDݚ"�u��!е陮�M��2�Ab�딎 �W[�d���OP��<>G���Qݚ8��#1�,�?��5K�QDz�Ղ 148 | ?�j��a0�ڃP�} %� %Wr+:�I=)���8�� 149 | "��]5��~�ʤ�Qˢ���� N�#��3��!����|�ӂ`>�}��߻�G�3f���%Es%�ɖo $̉��{H�/�e��^�mʸ�\��k9W��]��D�b���x� `m�̏�����x�+��ʍ$�d����3�0�p��)����Cd X"���]A��� ����L�����Bi*�A�o��[{�h����9�}c�h�9�*��9jؓ�ua�\�ɪ�$5���m��b~�ȥ=M �(���S��2wj�𷂷2���`hk>u�@�t���o�e�S���>��i����?W-zɾ-�W �N�EQI�JS6���8R���5,'�U�@�d��o Q�������qD�B�Y��0 150 | F��=����x)�I�L�>��� 6�;"'� 151 | 4����Ym���}��)+�D�:���ʂ�w>k�, X{���ɩp����4���p7f����G��8����ʽ�����)Vыc�����p�l �(��Dle�>[#�X 152 | ұn��uq9!�����0�O��W���N>� � ��E��I(=�n�p�N��r�-���,1>**�d�Agu�- @y�|}3 �@��΂�eb ��umt��r. �E# A�ʭ����e�دZS��.�*)�b�Z2<�4,��ң#��A3��� �„���Y�{ٓr�Vl�:T��ww��k�}A��/h�A%CW�D��N���zu���uc��1�U< 153 | !�7������&�QY�Ч�lMກ�/�E�6�@ʱ���l@ 154 | ����&��!����8��������p���B!(p4��p�șTAK�:(�H2�R��@@ s5]��.�� _�*���Fu����_���Q�ɽ�)?��h7��(���?�����.P ��Jz�����t暝 155 | �?-�I`h�'�JO�B�t7��y; 8����Ʉ��]��E ϙ�uͣB-cD0���~"���%�nd��X�������RV��AN6Y1�mB@b��1�7��,JB���`�2���r��g�i�5`X 156 | (05��1 �w�Ke��=AB�r@�j�B�� �@?8Mo� 157 | �,c4�*�m.�sѐȏ��� �4r[>���Zʪ��Ҋ�ma���:�Q�_Š���� ��M�`�)fH���%� s��3|�����-��V���٤AG�v�����̀pڐ2�෿ iu ��k��ʪ��gÄ2��&+�~����Q��1!�2Z���|�`�ӗE�eP�no��S��sd��<�a���5pDqO�q���5���V�"C���1:+%\@��|� �� ��lzms%8D����G� 158 | ��cԫ~�&���TCS!Pb�A2͓c 3�c/g�\�2(�]0��L�M��Z2"�\ H��N�5|�{�4 �ğ��K��sh�����c�}��2S������ͩ�U%{���k&�:�^�#v�v�ͯd?�YR�i/p]���ᥣbrk��׈�����޼�`����ދ��p�\�zE�Z(Q���"Ǝ���W%�8�YA��L{ ��f����v�sԐ=m���-�� m��I��;��?� �{ 159 | PK7W��L71�i4���R�x�&���8�MAdZ��@�z.�s�̺҄P��m���.�.l5����@�X�ԻCr�2G[�������1Ǻ���J����?#��:����YL����1f�08g��P ��kPrX��+ٞ�Z�,���Z� J�o�n�� 160 | ^� ���*]vjʳ��₶^7�� 161 | ���m"�\֋݈D�������"T}wP �>-@T]x!7�.פm;"���J�!qA�G�뛂����{#� �MM�i�p<��@���^Zڠ���cS�3%�B��WWC�b5կ����hEz��MB�m@�]�(�[��T���T}5T� ��p��[��l-U/��%d(5}����sb�ע�A�Qj�� � �uZ�kK���غ����|����8����g��tNʜ�9������s'�]1�˹��u���w�K�w��7��i~�� ��@�~`�~0����)�^�x�ڂ�� �������И�y����?��ò��������G9?��� ̂� �,8���?~�������Ï��I�������d�O��𚅋�/��O�����e?�󣙏N~��G��X�c#[��W�g<>����X�DٓIONxr��瞺��yO?u詿-�}��E�����O����O�}��3�<�ճ����l��n�ٓ?+����V<���YϏ{���/�����>�y��'���/Nq鋿�/M~��Kg[�b��/'�<���/�|����^2uI��ݯ�{e�++^��WS^}��7_�|i�һ��]����e㖽�쏅� W-�U4����xqZ���W�_�VrM��Ӗ���G������ym�k|=����}��xc������W�X���+�nbW���� 6)�JlRRRrk�Ԕ�)�RV�|��9uOjy��ԣiiiW��O;��uڟ�dw�eA�U]vv�[��]�w��uDח���O�+=)}p����ҷ��dd\Vxّ��n ���#���=��8��|Vf֨��Y���Y��~�g�{���sQ�w{������a����������kDN��9w�<��R�Ҝ9k���xP�q9�������[rg����������3��Qp����9(KN������\��YG����L����V�΢~o���t+���������a���g����j��YcV���-������ߗ"�2P�@1(˝�f�lD�M`3���m��6ס��½d.wN�^�Z��r��N�����:u��9��3סd�u����~���}0�|an����fp >�����m�>�9fF�+���i�e��Uf�1fZ�F�����m<��>~���і����.�}2���L��i�n����#ܽ(� Ĺ��D~�o6��q��\�{?b�7�f)�] 4 | A(%`��g^C���"o��+��J3ݼi�0�0/�U�,�V,ͼ����"�{`5x�k1��u[~փ ���k��F�o� ��;�'�'�'�oP�8�v��V��?`/�S��oc��^j�G����|(��G�{��1�p|��>EU����a���P����L9��z�X-l��v�v��d&���� 3 �5��ᖁBP�A X�속�D��În3��s�'�^�O��fR12@/�昳�Ԁ������K䜇G�9�����6���W�xE6�H�gzq����"����,���8z� �(Eޥ�c5�X�<����1�h�{�= 1�����l��P��@�\�� �� {k��ў�`Kͨyj^��W��U�yj^�2�����1����~m@�6�_0�Я ���k�� �ڄ~mB�6�_�Я ѯyh�J�\��}ڄ>mB�6�������l2Q�|�X�V����_�Oj�I->�l��~�m5��8����ns����9����h���ym�m��������A>���A���,�'�F����*0y A�y� 5 | ;ۍ��|����G+`�/E�e��bP6 �F��6�-`+�J�|~�uA�0_��1���ҳ���R����60.��BK4�ҏ3�P�l�� ��A�y�J�Iʛ;ډ��GO��z��1�J�h�##��κ-ȡ��j~mgЅ这��_'%m۸!�!�q�<�P/"���%��!�h�_�u�hO���A��q:~s�� 6 | K���'e(�Y�p��Bo�EO�@���$�:��GZ���Y��X��u�ƪG��~9�O�'Sm�~��m֪�2��'���k��n9��k�e���������A��e�@������������([>r��� �]`�9��� �c(�p����ݜ_�_��_��saK�蛝�91o4/���1�T��C��!�����Ý�&x�xڶx�j� 7 | �ūQ��^m�@��6�a�� �i=B�]�2��@�y��^��^�.�)�����j��ǖ�BW[;�m�ƧcQ�&��1�/�њ����H�����,B��3a ݐN�-6��ע��f�m���X�~����8�<��'Q��0�,A{����v}��I�z�8bz�_�8�F�����<����=���Z;��O�?� 3���c���a�o�z��囁a�� 8 | ̳��g+0 �a���ހ5�sn���g�o���LX�,���R��Q���cv^^�e%f���QVbFY�e%j4kx��6��mX�۰6�amn3k��Q��(�&�l[�6P 9 | ~�2��v�|va��6ga�)h���#�m/���������'��� ��g��{%�T�2_��'Ю����,��p�x=Z�Z�J�tZ�J�`6Zpj'j�jьR�E���om(UR�R*�[:R���L�Kݐ?�/)����A���j�!��j�O����e(r��~jD?U{�W5��k� �O���f�_a��X}O��7��7����h�Z�-ڿ�_���E�ע����h�F�}#ھm߈�oD�W������h�j�}5j�jـ�ϰ��^�m(�A98>F���p��� ��g����@ڽe�Gm���Z�Jl+�p�h�8Z�F�{��S����5�EJw���Ѣ����������ZԤ59��?�7��]Q�Pʵ(��q,f�t�2�9f9�sܺc�����s���ai��s9h�\�uZ�~������E����{jv ~���L6��a��Bʮ�=%����G��A��ѻ��jx��1+1+��q�ij��=����8z;����ۥX�{��k1"O›΀7�o:Y�� x�����%�=s�x���E��kj��� 9 9 9 9 9 9 �`&�d'�d'�����h۳��������8�$+��J�h�����Ђ������v<��4k��Z���8��8�%k��ZⰖ8��0���r�������{�4����� �����H̹y�G��F���u�k��?�F�4�u�Ur�*�h�\�J.Z%���V 10 | ����`Y鰆+��G-����qd���S=F)W�� }@��|�"=��'�����]�\�8B��>�H/��!C�@�/l������U��v���ԇ���_�����_�t#� 11 | ��k������v��~v�h������h���G�u���6u���[�:�3���bv V/����tl��(�?�;�^=Z.կ�N�l7�d�U����9��t��L��ٰ�|���ԑ-���;���g�w�R�=���o��5�ݤ�z���[����5 12 | ��ʷ���!ҭG��zzhY��/E�Q>�s��Nx��D����5�3K�E��H�4�l�va�L�oԡU��U��� �Y7��Xi�6�z���b���0>{������k|�!��aHs4�͒�������a?�}�O�����Š�+�^��{���Ŋ�+�^��1��\p��i��1��\psA+��V��� Z펙^�^|���~���[1�[� 13 | ��*p�o V_����փ�$g\�g8����Z�R� ������6��Q�+��S�{=���q�}�o�����^X� ����g���3��f>>_��/�����wψr�(� 15 | ��+ś�V��fU,׼_�m|��>#�j~����������0�nD;&ΈƇΈ���U��*�|z��ى��.����V���Q6�� 16 | �Pk�2�თ#��S����|�v��QV�XJV@�C��R���a�^�Y�䔁�6b�X��a 17 | ��0ނ������d��W�O�����l��O��ӝ&���v��k�0r1&�������y�B�`i��ǰ�r�_iO��S�����n�˙�G�P^Up�,�^T��tj����P�v���B�l�c� B,qw�h��sO�;B��!D�x0�P��a�)�_�ݫ�F�,��s� F�bX�bX�b��bX�bX�b/�g�b/�v�Mu'~�t���� 18 | �z�k��7�a���=D*�CȌq2���R}�^�8׵+�W�_����uf�CX�ы��4eB�v�@O~aƂ|g�]E����r��X��4ch�:Ќu��Go�������G콰���2��ww�Y�T�����o�٩�4�{L�:����n� -<�+ӱδ�����'����W� ��J���wZ>�T��'��'�g 19 | � 20 | ��1j �?�33��� O�S�\�2 �C3�Cq����9�E?߉v�|���>� L6 ���z��9lEߏG��9Į�s�3E9f�O�q��M|�p����FN�S5z�;�j��Zs*���|������kF����:����C�u�}��e&�Qdz�I�Y����s���:��s���|�1�5��c JQ�R��l]�c�Q� (E)J�B�E)Ρ �q�"�R���8L����ȯ 6�+E%����$�JGI��$�( |(�qZD������j��������AN�0Gw��`��šD����������J/�ڈ�X�1���1���Bo�3���]�Q>d]����O��[kG�*�o�gv���S��v}�Dop��� a����3�_ ����sQ�!�=�e�m ��<����`]�>v)�� �"P J�r�E���{��5��8)�@�ǰ:v�׬o��尩Tc`U�aE�������0W����� 21 | a5���L6����yu[�g���.�s��̷k�h:��^CM8�ja?�0�����{k�3>��]�v���L���k2�v�龢�_z �1��͹�����e�[�~��aׯ���� :x�w���Ы��9}˻Ga�����"�<�~��M 22 | ��xW)��R�ݥ�;L�w��F�ߞ���w��:�e�BE��XQ��ݨ�;Ra�����;Sa�b�b�b��T�;V�w��\=b�ػW�g��b���f�� 23 | ��xg+��V��@�\���p�b�����;_a��>���`a��f}~�w�����! �Kx�,�nY����0�w����)�E ��x6*��Z�͚�;k�w��d���i ����Q��[�ݷ�3Z�w���ލ ��s�zR���ޝ �Cx�.,S*�Ŋ������ ��x7/��^�]��;{�w����� �(�o�S�V��X��@�@X���b��Z�����a�b�����Ga��TA��WlW�P|蝶�FA�VA�fA�vA��T��A��Aا(SP� �{';�}>����>W�DAm�@��P��~X�vB��B��B��B��j+�!�����Z<��(�������������) 24 | E�bE�����7�5�5 B�U� 25 | �N�!B��Ja�Մ 26 | ��"B�=9���#5$O�YԖԘԚԜGԠ5 27 | jR����A��ԪԬԮ԰ԲԴԶԸԺԼԾ������K��m��� 28 | E�bE��Z�����@��@���I�Y�E�U�MQ��FG�+�+v(>T��'c }M�@m�@��@��@͏�OQ�د(WPVP+$ �;F�� �AB�=�8�k�j��;�^��� �㡦H��H��H�`��ݾ�Hȶ�#�$!_Q`�7-�6I8��o�����j�S�vIȶ��}-�p���&�'�Z'��'��'�(�Z(��(�gP5R�2E��HQ�(QPS%P[%lRlVlQlUlS�*�����ͪJj]�1vƭ��[B�U���Z.��"�j�j��|O�&��DJ->GɈՈ�6���X�r2T�T�TQT�T�T�T�T�T�T�T��S)����x�$ܪ�� 29 | G�G�*G�:G�JG�ZG�h��m�zG��G�GAU�@u�@��@��0�����?U@�<UA�AUB�B�2E��HQ�(QP]$��NxW��b��}��Z�IUJ�&�f��V�6E���&!�خء�P�ˮn }5�@U�@u�@��@���OQ�د(WPP]%T|�8����w��XUY�G���� 30 | �������?�V�%P�%P�%P�%P�$�Y�.�Z.�*%��.a�b�=����| �*��x &P&P#&�C�}��!�%�y�LEp�7�̱y��=�A�E�I�M�U��X�x_�F�VAM��I�Y�E�U�MQ��N�+�+v(>TP3'P;'PC'�=85u�>E�b��\q@A �pP��␂=�3����5{�{B����Im�@��G�*��z�4��� �� �� �� �� �� �� ��� �� �� �*���|�~F�S�q��/q���Z?��?��?�@�Z@a�]�+|m�@��p�]�+}��=J;(PC(PK(�g��J_[(�V�QPs(��F�Q�o}���&QX�(T)�% 31 | jjjj�7� �t_�(P�(P�(P)��X�x_�FA��@ͤ@�IA-�@M�@m�@��P���R�+�+v(����vYū�VS�fS�vS�q�}�2�~��5��NJ��OԄ 32 | Ԇ 33 | Ԉ 34 | Ԋ 35 | Ԍ 36 | Ԏ 37 | Ԑ 38 | ��T�P�T��T��T��T��T�U�U�&U�6U�c���}��@ͪ�Ԟ����eJ�Z�4�EJ�Z�4�%J�*l�ꍸ�}����j 39 | j j j jjjjjj���]�V_�(P�(P�(P�(P�(L�{�6_�(P�(�^\��QX�(T)�% 40 | j$�M�͊-���m�R��B\�]�C�o�X��1_�)�S�)�+��� +���*�����ZV[�@%�@E�@e���n7(*�7��>����F�������F���}�ռ��m���~��>T� 42 | T� 43 | T ����>T T T T ��\�꫈�� 44 | �����i 45 | ����������������Cղ@���LQ�(R+JT; �[�\�����������������w�w��]}���)~�X��:[�J[�Z[�j[��V~�����W���;T� T� T� T� T� �[�l�ȅ}�2��U������ʷ�P�.T(���U>T� T� T� }��\ȳo����5悾��\��NԠ G�X��@ݶ����[��[��[��[�K1Y1E1U1M1��K�|]�@}�@���k{&���ƅ] 46 | *�*��=�l���B��zs��s��sa�ա ԣ  47 | �����Յ`�_���]��]е��ZR�.P�.P/�i��u��H����u���u����X��u���u��R{?�����BE��XQ��ZW�N_8��n_�m��u�����3������r��� �� S��^��^Ч?�� �� �� �� �� �� w)�3E1U1MA;@��@ �@e�@M�@��@Ź@��P���_�;���w� d(����w�; ��.�)�(���0M�w�{ �!�B��|:j!�� Pe-����0T_ |�A�� ��;�u�΃�w�!�]��D|7B�;ߕ�΄�w'���SS�|�B�;߽����w1�������^��T(���P��;���������!����~|D�� � �n��wD�+"�����|�D�K1Y1E1U1M�wN�{"�!��>|7E�Lܧ㞉�T(:��j��"���j����wG� #��A��!�*�������w���K#��o�|3D�"�����) 48 | E�bE��o�|�D�ߺ��M�o�|3E�*�Tb�Z�����0=���b����v0y����}7���k����������b��7�ԝ���Fȿ�;�u������w���������LF ����1�l�s�c���y�`�����L���xb�7�C|� 8f����xbXӿB >������1�^XG��qv���0���_�qn �9�ʔl��'�s�ΰu�p�u*�Y�:8���5���3����P;>�uC��o�&�{��בx.&�s��g���W������!̛�ةX��� xb��#�z O����^c��m���>�qv;gaA�S�D<1�@���\<��cs(2�p�Z��O�����^Dܶ��~n��)v'��m����≙���0�? 49 | �j˓��H�ڒuN 50 | �h"���%��l�]|i��'��.|���4�Y�Ӌ�kT(�;C�z�i ��jO#�1B8F��I���[��O�JiR'��r �F"śB�~�i��;j��IR�&�X�`'I6����w�\�'f��S=���9���]�~ a�P�`Y��K��!w����)�1��Ԣ�3�Be�xI����$�Z�� �@���J1ݶOrD���qO&��FmV&3*�+ɛWie�� �Ŷ�X�'f��I2�O2��ai��୽nހ��|1R����c�g�s��+ٛ��p1y���gu�󥮃Qi������r�t�3�^-/m�(����$k�����9弃��#��yw�}&{V kB:"݀���F��cf�I̙#���s(b��oEw���e� ����k�9��qJ�����i���!x���p��� ��N�IQ���D`�e��~"Nxl� �#%¾�ē�.׋_���ڍ�����q�Q��~݊�A#�10B�{]ov����p�g����Zwǥ�jnD�܌��/�)�;�3����"���\{�$*� �������gzw|F��Qiw6cJ�FŻ�������HwW��Z�Y��%l]��'j��J%lk� ݛ��ۚ;�u6�D�W�G��Z��2#’tm�z#"z��Y?�f%�)[�ڍ}wx%�|%k��7y�KZDo�%s�&)SR�)�O���t��'�O:��js�k͛"�����*n����\���xτֳCN�s��T�����.B “�|�ӭ֫rw������w����k���Z<)X�?�W�K�œ�a�ut�B����O���婁]�;S�;[�/�t\z��s����!~c���� �좋]Ǔ��&۱����h��l�bǂ��߄�����j���$fC���5��im��S�������JR���@�G�`C#ڿ�����G�Ј�[�a��r�����-5���-J*��Z�zJv�n�y�����q�y��>9b���ٍ#=ٞ����(K؟k �I�w�����y�x��=<� ��ѵ_�j?1P�p�o",�>ڰ;i Ԣ���a�X�33�;�0�� �@zv�#ٝ�ݑ����������+�(�n�����Oo�%n?*ܱ߹O}�>��Tjط�\:�Y�5I̩�Y���= �Vj����<1����:��L̆M��I���aS���5����}���۴������Lv�w����=U���{��{�=aK���s��S��T��,|�iu8~�>��!��� �?���ʹ?�h���C��h�[n���S����>���#���?�n �J�lO�3Pg�ZC~�k��VNĻ�gǹ�S�sf�W�N,xd�����9~dDO��} q��8�`aO��'���,g#|�2x��U 51 | ���jG��J�4��<%��8ʺX/��7f9�DXGb��J��D��ĺ�҆��f�L�0���� [TKUZ��/�8�l��`�ųU���΄�n{z-�h�d�&�.%Ͷ�{����|�e\�'ͶO�m�0ծei���_�<����f���� �g������ׅҹX3�Υ���^�V��S���9����Nؑ�>K����U'��SZ�85�ͥz�3*��Oes�"V�z<�sr�F��ۅ�(��2�2���"�TǹоaT�,�=@�Y01GK����\`.r�*f[�;n�%�턕c֊�v���H����y(Pw�����bi;�J�����(�����V����v��ݓ��Ɍ��� 52 | sq����Y��+rj�R��������I�kx��ێ9)��q}D���*A-���9ЃtW+T�=K��cbcܫ$ٽ�������$�oI���${>��֠�P9�@–>�>kuNf�TorO��V��ql�� �+�S�����.�'dz�3�V��;�n�6׭�Z�;�ε'}�[ޝA��O�0��h�Dݵ<6bĶ�hS���Z�;Vg=cM�����P�$���;ی�{h?�`W�6!7��f |�mU ���y���#��Ρ} 53 | ��!fu�k8��*����g�� �r�W~�|�h���]�G�����F� �o� c�ZQb��^�_@K3c�bO�ܰ3Bg-k����.�����R����huGh����G�R�7���<Q���ru�o��Y� 54 | Ӿ����-��Y�1w5ugjw5u��������l���b%fO7tint�Q���98�[7�[g�o!k��j �17Iؐ��+�!�qW�Q�~Ӎ�����\���Ĉ����> \l�6r/���;uV������V,bSf�#����<Q�.����]p��O������7v����$��mԖMǧ��A��軞{#�E�W�>i�ބ�$[�v�������YmZ`����/�$�n�'�\�d�� ׯ�@�ȹw��󘩱��l�7�s@�jZ�*g��Ш��<��W�.*GY+�ݕ�b��ؿ���{���=�k]g�d���B!'B�o֛��ז����lr(��T���s�OR�� c�5H�����%����V1Ֆ�=Kf�2z6}1�9_8oy9�:���쌃�Ko�>/v7;��y~�{�D+�����=Wr��]9^G�-y��p�_D�OE�?fC�{�� �>}���4�bF]ǡ��7�]��7}(t.������~�s�9���s�G��q�w��1���I�����]�*W��m��j�V��: +�l�d�O��R����94����3��U:�<���q���2����R�Y%a��c��������׏�8;Wǡ��?:|O�M��R�\�=}����={<� 55 | latn��x�c`d�¬�����:�՘��QB3_dHcb```b`eeQ, ��� ��G���8'�����9��(<$�x "x�c```f�`Fx�1��, '������P���ѐ1�1����8�-�;�{D$�d� �J�(*) ��ԧ���>�1��T�0X���X�%B��������������}�g���>��`����>X�`΃�:���;u' ������ H0�+ +;'7/����������������������������������������������������������������@`PpHhXxDdTtLl\|B"C[{g���/Z�l����W�Y�~݆���nٶc���{�],JI�d�XX�}�,��cC1Cz9�u95 +v5&��ع����Z�:|��� gv20c�q�å�s WZz�{��'L��:�aʜ��?U��p���� f��x�c`���P�Q�͚��m�ÿ �I>�%���v������[A|��tx��Vis�V���I�R�Т.O�8M�'�RL�e���Z J+�N�/�2�oЯ�2� ��O빒m I�����y���_�4%H��\_��3mv�M��{]6h��D��Q��]ԊZ�+ �$�'͑���kN`[�+��E%z��w(�z���O9nץ�뙔-�;�=S�F� �t����j�j�/���h [���u>_g���'`M 56 | ��xv�M1�`�����ߗ�u�C߷(��ɕBX�w:��Mi��������K����-��-6R � �ۥl�ġ#"AA��/��m/����I��m�z83ص�~���&�J_ˤ�*`)m��K;����](�|٢ %��i��,�����)A#1���Ӛ��es�I�r.��[� 57 | Lp�w �H���$^��1%a�ȡ�Ȏ ���S^���/\��J�OOe�lC�~ٴhFř�K��aѬQ:������� �v���ʢ9\3��D ]��Y'Q hA�h^���8�k�+4s(�X��jo{��t�0�����U�6��{���4W��E%��~��A�22�-u���o��e�eS��!6�s~��;>W}=��$;��qv��`�9 ���`�9��`�/���c�9�=�0��_1���4�f�o�� `��96�st�à������|� ��6A���K� �-,�Wtc��� ��1��1�gE��^$�_����ԇ�n���x�POS�HS��������]��!eW:O�����cN��x��} |Tչ�9�n3�,3�L&{2�$�uHB ��$@d#a a �A�*(�Tp�uk�>�>��g]QDm���և���m�U�-��=�����νw�$�}�����@��9�9�,�~��]�H'!�Gn$QȞ��mݾD˜�C!��B{DJimMm9X8I���CmtĈ����|��bJI�QI����_,!�,�#�$�QTns����Ʉ2�� �u3�a��-d~}�ڈb��xX,����'(�������9l�٬��TD]� ����u�w���_�k��GB�gx�� ��32��r0���S�E�:a��4**�%��(5��X��lk�� � � ���X��LW�o.�0`�`,p.��6M���y58�΄͑Ќ���[ �m���df����$'9 �v���jUҋhy���W<���o���.��n���>?Ⱦ{W���Y��]]�վ�r_�s��������;�G��o���E�/i��~z��������b�2f�2�rRC��le%L2 �b�F)4����Q��8H(�(��$���OpB8���� R} ����W3czu��o��JBMȐ ���pUz���e+�Ux�ʊ:VU�rdH��U�f���_{õ�5r�w��]*���Y����jVnݾue�����T��a^A���|Hl��X�9����8놫6tϊ����r_sk��!X_�#)��a� ];^۹���O�Jk�45�����H�ؿ��˻/��ԓ��喃I� ^(͂�O�D���3"��( k 1��^�(�b��R2�*2 ���<��6���U���V��p��"d��_���w�++ޮӹ��L]�d{�J[E�85Ct�b�m+ϙ�P^�>�V�n��O�]=-Y��������T�Ǫ5�[��̵�ܷZ������߷�˻O��O�>�VV7�)��&�iN}YZZY���,՝�4��LX�_��VB�5wN��͙���84���>���� 59 | ��Vk�Xj�f&2� & 60 | ��d`:=���0;�r������l�`���o^,3��9Z�8K���a����A����������&V �4�N�[�?e�wS����}��b��꾳w�8�N��1�Ƈ�DG����:ѥb��@lD~�CY͍�l�'7�JJ� ��K��Ϩ_P�[�sh�|�&��?�>��.�I�Ϡ?�}FUߢ3�3��E3�c;i��7 Q�d'r�&�e d��+�*t#j����rI�J�;�W��N`. 62 | %���vb����+�J}G���#\K���W��Az7���Y��M��+�>đ�Y��۬��ΐ�Ǖ�zQ2�r؜��,rN����\�%�{UE�ݕj��������Ǣ�Qɮ�$������m)�kZC� �� (C|iLS�X�c�ԁH R��P��nP[�s|> ,�ѠM�2~����'#�� 63 | �{��N��B�P����\F߄�� 64 | U=EA}3@�\ʹ���D�.t W�:����\s�(���c'$E���$�jOuV�^�%�� �$��'�L L<(���V������ы�^#;lKO�*�����,]�����B���{�z��< ��G���m�3��Z��y##��ڶΞ|�������99� �z�Gw��t���D2�#� �gyP���"�:;����� 65 | ��� ̣_�2� 66 | '�����R��� 67 | �~Gd�@4M3�HP�H��殜 68 | ̟�Q�N�Y���=��\�6���x��+A�yDq�DE� 69 | ��A߸��|r=0�8f��\Pvf�"-c�?�皠�1��D��싁J7q���sd��xM�1n�xH5y&u�|���zn��͵��n��_ˮ9{|�>�)���o��ӿ�/�/���+�uϟ�����7?�hU���y�ԏ�ZL�H� � e�/�ZxMī�HC��Ȅ!P�D&&Ȳ1k��>�3@NߤUJ���Ƿ"DVБ�:��o��S 70 | ��5(j`d�]Y������ ߓ���c�o���gH���*%��]��E�i��?y����)���)��f��T۔��#;_�|{1=���k���H���{��6�ؼ�iF����hJ�S�g��\t��������T�C?�}\}6 �~4e!wV`*��Г����*� �~z�(h�� %�ڰM��z_�V%��� rbz�f�۬� 71 | +\�k!�\��7S =�~�=u�>,<>�9�.�y��������,�JT����uD�bBsIDs��,����`�����~�p�)��|���B6Nִ�i�"�6��s��鑑����M{ZY�����/)���__����3w�{���V6d�x6E��ǀ6dr�!tP$n���C!#*�D'P]���D�� �Т[���4*2�h6��ʊ�,���Q5���䚿��i�I��%�}(�Q^��_��ӇK@���0�� �V��I�1I���N) ��"d"�xv? N#�sMP �١i!�Q�v�M�"*����-B�&���H�@��J�3C����-�iUynR������p�; �3=�ޫ�%�D{�������2{lR�5������Y|��ۦn��Pk���_5m��ԚS�0cц��ݛz��z7�jiܰhFBiL��Zڰ����/�i�_�b��қ �J�)Puz�E��� /:(vs h�d"����ۂs�������.�U�B&� 0H�����^�����玷�7������_��퍯GF����_ 73 | �`�1� 74 | ����=����I�J'8N� C(��3��,��zp5��y�|����ԙ`H0r���-���$�x}E�٥Q��Q����f[ 75 | �g�-�O��������J�-=���ly&���;8����F��$��Y����f�����ת�~��=�Ts[ $�n�W��=�!�l�ĕ\?M -�53��L}v���=0�n�q��˾�ǂ�w���������. ��Y���l�S�����L?S��k�K=������ǀ~�O��ᰉ�!�J�I ` �n��� Z~�q�"������'�g)�bEQ��=�T�U�E\���^ X���h����$���rk[�Zks���������o�+��Xk{����C��.{���)��N;-T?z:��s~�;iZ�u�|�w-����p5�[��E�$�u��.�<��)@������(�М(�� 76 | zZF�t�’$�@%�(�)I�%ԩ��< �23a�L��Q+��d�%K� W1:��`��7�GZ�=Ȣ4��t��=Gk¢6��,�L�'h�{�EȦ����;[�͜1u�']�I�Q&�����m��Ku#�b����p�y*�mKe�P^�WY�� �3�K"��0�ˀ���XN�T1Cp&0,��� /,IM�N��6�1�&��������*�u�V���+n^Q�n���iU�/ݻ���l�Z�g|�ݝ���4���Uַ��xN���'7UW\����G���v-ʦ�vݱ���Bj��)�Ɯ�$���eIO����i�+�����������JZ�� Ԙ��}�SW�r�\s6�����~��^o� +��2V�Θ��y�)�������XYX���k�n�B}�4��� 77 | ��t 6��C �{��,)5KF;��2H�ܑ�)aT�����U�� 6H��R#�nV �%D�l�V4�i���+�%�������X.����j�h�z��0��Jx�������I��ۡ��@S_���N��'9�y��b�#���+LZ�P�H�B�"�!,9G�JwyP�)hk�TvS����4^d(Pl�J#�7Rf���n��b������t))ojrmkwcƴ�$�����0��{���Kj]rMvm��~oò��X�,�Z�,��Z0vBJ>�K^��,G?��a#n�Η�l�@D;����M �d�#aAPI"���y&]�C����%�^�()E�Pb�����z��Y0|���:� ]l{r۝��[�1�q҂���գ�}�K|�oIo_�Н߻tq}^^�⥽��.� ��G����"pa�`�J���À�?���=����<��`b��E,x�f?{��W�6fJ~7Ly8��{���FwN��1<�!��~�G ;VZ��K= :��V'�sA�" %/���x��5A5��:� N��G���#�VB�����я䚓��3j�I�Mp^fr�6����=�c���B����w'2&BŰ��c� <��R=���L�8 �ʌI����.9����_0�kN�z�!yE�f�x`�QP��k!s6�����:���Y��\��m���YIߤI����O)<{D\�� ����M#����t� �oX���H�� t7�� �u��jn��� B8� �Bw����/Kpg�����K.*��x��ɭ��3��(V���neO���3S�s�ں{��5E˗.��cp���>�ԠEi%��[�h�E��A�b[ e)�yJx-��ѽ�X ��~���?穗�R�S?Q��)��–���5���m~�q��R�C4�0E�M�e��F'Q�(��ϧ�P�8�b��nN�4�;!��O d���e�Jr;�>q}J���Ж�fi����3��7{ cUA<�D �TLAۮ7�������*�Ԉ\s�:T��Y�]ǃ�6�̖(4��/*F[:! B 78 | � �-�Q��[Q��Kz]�8�n�00C�e���nu:=�T=� ��>��WF_a��Aٱ �)�+ G�~Hǖ��+xQ<}�a?��*���Lk([`x�$"�l�)C�r��qV�5g�9��q"O�1E���G+e�\���) ]������R�(�%�C�h�;��x�]�� 8�T�����y\����s[����D�-��>*����\>m:,��"0Lm/�[� 3�4d��^J} a{��:�Dk���n.N���M�^]Mߢw���Vί��xD 79 | l]0dP:q��G 80 | ?���զ�����u\�F�B�vNꋣĕ��a�%�4]�FC�����{��!��=?�������f�߶�}U�P�}L#<� ���sLg��="��MP��>��$�8O58]�IG�fs��oGX�����v��s�u3E��ʾǾw���fgIR��m���$�co�Ԕ;u? �Q���H ��Y^i���/H�<��݇!� ɸ�G��:Zd��$! 81 | �!Q �@Db����(D��!z�/*-�e���6k�� � ��M�O~��c7�͒��s�Z��8�r��̿܆��\S_s�R_���pI�������O!.���}� $dt-(�#h��8ݏ��]x��$�VnEN�tA�Aº�~��� �1yё}��jQt��)eŅ��R����H�C��� FF6ԯ�9�s]�)�u�_��uloe3�襉��nz���_��I��KO+����� o�vuS^juז���~�����QX۳�����kZR����o���-_Vȷ4Ե�r#���~�Ț�SŠa 82 | z����@��n�rhҡ�-�"�� 83 | Q�����*ŗ�G@!z t�3�h�4�,�+�z�e�\ }����:�r1�"%(1��n 84 | ��%e��!�p����dxn���b ��E6���=fz�$X�a*���t�+� �����C(<��O� � ) .�Vt�&�wt���D�]�<:�y���U���G��H��N�������uḒ��riq��-�n�����fk��q ;W���I����b-%���Vu�����P��[��u�:_��VI<��s�:b��� 86 | {G?�Z�#��R �ݵ�2����\���h��1�������K���R�TpeH$���QN0 %4�S�/ 87 | |�Ѫ QҊ� �Lp�&��=�ʽD��[� 88 | 3����j-�L� 89 | %ʄM��ĵdbFɸf2H}������/���h䫌��j�����`[X�.��N�_9�g�ebYN6^�65{Wޱ������MM)�?�Q�3������\#�� W� ��ְV��D(�9!����֪f,?h>'p҅kz�7��88��@�Uԗ?9�8C V ��� �%���h���c~�V^)Y����_�{��{[s%�u��w�~|,q��G������3s��e֝�ݹ�6'�z^�ϵ�G����zس����R�^T���l؏`E!|Ln z��=��*" ����v�=L��Dq�Ml ����1��X���5Ã1th���fpV! ���5ތ���j�����[��iÏ}b��;�����:��K� $Ŀ�o�x��!�no�z��7wHR�ɣȠ ؂�'ĪT,�j�q��)>�@�r�D �GV���x��9�$�:�8\���]�Ƭ-�����nu���T�~,%l��`ij�{O�{�����F��CR�4��G[t� ["r�N�_D��.C 0�Iа[@얙�2)DR��(F9�iF���,��@�$Q��:{'h�dPw&m���a0� �h�!n bxc~�� ��7k�qx�y�� 91 | "�u/E���(R��qm�Ŗ�q�":�q�w�Q|sV�Ӫ$G9���E�֖BvƦu�^z���+���Ҹ����&3���'�uOˍG��f��ټ+|�چ���¾��4g���>��$�Qv��C�������I��¥�$2��L�zB ����*����+� d%���o'v�#����<�QAL;�k����}W��d6�0�)����8�{���s�~�:" y�W 92 | ١�T#��w+ 93 | �B�)E�F��MT˧��ز`�6�������Ds�5� ��O�GO��Z��C�T�ȐÏ��0���)l6��+t8 94 | ��Z㢣����P�����Hf5�=��gV��.nHv`�t �9�3�s݇��ef���(cy�����9���������6xW��?�+�����t���~�O;��󳻾���ԻZ����%pԬ����Ǵ30� Ti��:{�zH:��/J� H���8�]�YE�ق�4�E��&Mf^��͙�d��xK�����w/�u�|�)Z�`$�t�ז ��I������Ѹ�!xb��ܸ�s�7�s����_�f٢���L��À������inU���zXWM�K�&]�� �_�~���߹"�;P��� �\��&3����qh~�ԑ6ڷ|#t��Ar�+��%�KxLB�Kwe��K�(� �Y�t�KW��T���k���:��Z��@�:�D�]B�j ���h \�+F�ZA�7�M���3�`�~�I?W� V���#1�����@(�bH���p�IU6 8�m�Q�U>_�S�\J8>��*�a�ꖶ��p�c�'a����|j*�8���p�4nQ'G�Ⱥ}�0��d@�� 8�f>g��*��aD9���^�����{~��Y_r+ݎ �ݘ�f6j�w񍻯[U�P���7w�z���E-1_t񍻶��)����o�������}���W۷�۷y�ª �R�k���̸�ust\bތ�k�/b� !��жr��-D��&d�s���Ep�K��z1�EA�0�B�C�C]-�`܍� *����!w��;�c\�q�۬% ��[�bܕ�>�;�l�w�������oS�zB;�]0� ) R;9���B��L?d�KC[���.��j , Ct�Ah����W,���c�2Ba�yM�i���R8Գ���R��&(��)�gF�Xx�� у�m_�y�C&�1="g�Qv����"��`�Z;nP�Q�Q�!�݊Y#?q51q` ��w�ߓFo�Ό���A��c�&�$=.  95 | 3׾i(�i��솖j�,���� 96 | T�� f�3�%h�����m�=�ѿ̿�~�a��{���tm��h�9�x'��B���W�k��]����I��Z�W0!l�\R ?^u�}\�H��"�GL\���Gt��������=���� 97 | �T����}.��P2��X]�D�%譒� �_`P+e������]�!� ��&-8hȥ�� ��> "���H�@2��':�D�]/��H�h���4w�-@(�~���ҵe�;!N�ݙ��+��:)z"\ ^g����R�1�m.���5�]�(�7��5�es���&*���s�0� b���ؑ,�}��#\���x�Ч�B����sԟI �}��>�"�E���W�]d*��+�PjF=C�����]H��(��R]E� ~�!VP2��EH�x%��#V�I�_���1� ��OBtLlqCG��رv^�9���*ɂ[�9a��������Xj�������k�~B�6y%�&��ؙ�.#&�l6�ܤ�7Ee`�L&��l����3Z1,���� zz#��M��?�}h+_��4�z, 6c��C��jZ��csV��. ��t ͍W$|L�Q�Jge�@og����7�1��sW�������u[x�'+���kI��Wv �rm�K������S�h�~Q7=�ĚU�nU(5K1��<���[�@Dw������i�ڭ����%� s3����toILt�swV�S�H�R c��]�I���fo\�Ȥ����E��=�g��1�Ʃ��y��ʩ�� Lq*�J���S�Uy�،5k�?��`���������Qʜ5s�E;k���w��yjgeHbJu��yl:�ۉ`#�/(\�� 98 | ���.)�n���S��_��蒍���MC� �,�p1p� 99 | �I���x~��s��T��Ze�b��*&Y��Sd�Lq&��h7�u��iBp�W���b��(r���h#+�o �I"I�DЯ P{4�V���k����R��.���\�W�أO2�~��)�e����TE_�]w@��/��=�W9��M�F.��~���?��$:�}����~�V�?"�p�r���?������w�#Т�^��g�0�*fÏU��*�"�l����F��M&e�Bi5+�<`ܵJ¥Iʓ 100 | ]�Ժ����aVLC��� 8��:1�c�t�E��� �����[˲�C�äg�I#���yM��g5TOKO����Q�T�����2�>k���O��.fѨ�sg��-t�Rvm�*E�7_{��k����E՘Y�K��j���e0��)�K�οh�s �j���Z5#q����ɹŞT&L�Qƾ��o�ͧ��mh[V�R���i�,Jv��峫��&FQKra}g�x0�)�p�����3V�D1A�J��ğ�'C2�*i֊xz$̎t�>z-(� "&�?�W��4�n`k}��9-���`e�A�������2ج"� � o��>��A�(1�?m2qӌc 1-l�������y�:�z�x����L�maW]ҩ~�%�%��Bv�h�����k5�L+��H�,�`�w 105 | ύ�s΋\�����Q�� %�)��Ԯ %���;��G=��.^b���Bcr�~�GWk\��ڊ���m�B2I5��DB��fjaJ�.=x� 106 | c�)q:�3c2�X�EXE����S�UV�y�Lb̈2�k*�O��VUU=oKGiiǖyMW��+���{KJ���x���y�’��Ϭ]�� ��; ^�3n�g�&�T�'ǀ�1��d`e�㞳܉!p�L���z4m��s9JG���K��3����k��H«�9l�aCޔR�:�g�2�52.����#���>*�fխ��<�*��i鲥ME���G 107 | gw�v�.\,,�s񚶪�ڍ�,���յ����ʿ~����.*����~�<�j��]) 4h��t�Y�\ղ\�1�#��ݶ*Kvݚ�z��:�%Ӟ���hϴ���ޱv�k��-��U=�J�h�si����i�}W��MͲ�e�,�h���%����W��������c�? �b=�[��GT�Pi��[s�S4���Y��{��Gŗ�b�8�sr������lf�5LT�Rc����ՙ���g?KΌ�&�'������Jl�}�F6�� \�j�"İK��5A�ي��;1�}N~G5${p/����6l6[�v�Ј�F1 �iߋC�%�~�,����Iv���^����M�_c��7�s(v����`m��gG����vm.�������ѯD��I����4%�'��ˀD;���!I��PYn�%�h� y��ˡsH�Fu.��djj��Z� I������m_�M�hw���iSRAŌ���Sj�W�'%�WN�I�P�����(�$�Y�sW��6�Q�Gϊ��7�����e��ed3!rj@FƘeư�� K�%��gbn2�g�o���@h���o��������d&���a[,3�Ѩ�j�<�� ��VPu�<%2h���l�fP[��1ɫ�j�!9f:?�7�U$hlG�� E��V� �<�����sGh+��o/���� 1�������_j�4��] 108 | F�*��-f���g�2�B�ǢE%��[��j�-1���#[AJ:,� M��h 109 | �7��P K y�Z�@,H��� 110 | ����N�:5�^ЖN�霜��`iq5��R�W:��z�T^�*��6q�8���Ng1��x!��D�̦h�PD&�Z"ʱ� ��%�B�������Y��RTlP�`���@cYEbc�zHTln�wYޯ��ޱ_�:t��a�L��»���E_}�̚�{+� 111 | ���e#����Q���zn)J#�qm�G��k����.ӏ"�H������a���`����<�G�ş��_#� OG+`lL��e>`�#9���0{�Yy-�0���v�-c�&�� 112 | ������X�!̻�#/�a��c2����i���gU`ޡs�a�0�{��;��f�4�����YUf��d�*>pGA�i#�i�o���d���l������4��h�G1k�˻ i ���E���%-�R� ho��r���Y��}���+��C���?���__���ӂ�;�sF�%+�E�oV����ԇ�F?8C�Oi��6�ڜ�<�LjU&��i� ��9��]óx�g4���@ܬ��x�5&`b9��?��L��8'�$q���8D�������$�Y������������5�{�x&�����|S�������?�5��Y�_=�~��B�Փt3��x�I���a{�-�Ҕd��bRF���&1�u�k��t�,1���.�K�����@�i>��=��"ϣljYN~�ғ��!�=u�ڹ�V_�i���h�]h�`Y�����I�!�]FF<4!&�ٕ�EY 115 | C�ě-�[�\BJaX�����>�ӯ���,����4�s-E�l�= �#sl�~�9��sj�Ioc@@}Ο���������`?c��՛��}>��8����a�����S 119 | 0e|<�giz4�`�͈k�#�ne�}\��c-�(�Y�g�� ��qS`\U��(5��z1�c��%E������^ 120 | h�~�@����C��V`r�u��O�楹�~RZUzլ�|w�]�U[�����ma���X�{�e]~}B�7���� �3�}�Sl V���LYT}��+����o��o/����nX����E߾~�E��3fT�<��RX���.Z�뮜��~�}�|��u\�5��l�3 �#sl�~V����h�Y;��#���o{�����I�Ol��a!Ӏ�9i0:O����N�����T��GĆ���N�.q��_��/�ྴ��m�'���y%�)_�+'a����}���L&̋�f���\iG���J4�R��Ш"+�'�Lѯ��1T9PMW���&B�RL%��� �Ŷ6��/ߌ� 121 | ����v��N=zt�O��^���,I2���z���O��\�\X攔��v�M�M�Gk�2�{��_8�,ؒ9��%۸��L�پJZ���X��<���Nw~���8�W��q`�M�O#ǝ�| w2�y�`<6V������։G�Y��.����%��w�'|�򬀦�Hw�Q~��FG�3R� �. K�R0�!��g�Q�{R���)�D��$/,݊P������%��)ɕ���ݰ��!���7,k�*O��G?�2��'.;#����ח�?�Y��1˓�ᶪ�ʯw�Y��Vj��R~w7���?� 122 | ɰ˲�~�;�py&��ҩK�> w�$����*��(<��v�vn��)[f-��>z�H��6L3Rh�0㤢��)d0��M��0~��7P���d�(��@�ee���t#�$��m�%NfX=���@@�+�Q��^�γ���1�h��o��Qߡ.����3qÙ��/Z� 򵧣���uJ���Z�7���}=סRO��Ey)嚲A)�eb�(~S�EL���P�U���j�f�h0l$s��� ���� �Fs���K0�L���������|���C�+�\Q � 123 | ��C��d�!�A�x��Q�eG�J�T�5���,2G�w^ߵ���Y题>k����7v����v�Zy��k�S��o=*Լ뷱��y/j���|�=ޛ����e��}�>OzaI��� ������W�Q��:�o�\�� �[�8�z{��q�~��T0�:��8����?�V��h��5O��}��7^� �|��8��;W��Ħ�}���>;F��Z�uZᅲ���žg?#��L�={ui9�IˮPi�H�$3=���X�$��E�ցm�e��X�H��� 124 | Z��21?�f��Otz��'y򖮩S��̞uEWyy��ޖ�� ����m��Gp�>$s�����w �_�{�����K�?{/1��K/��\z&:�v��^y�XI�!���&��S�����Lt�%ŋ��xA��`~ȥX� ��f*+��ԏ_f���+��s5�w�S��񗉩�t���f��R�bz�� 125 | �E�϶��}�lk F�,.�cv�Շm8c���&�������[̻�c�08�d���0*�r��-� Ўqؒ`܋6 )��بM���w͓��{������N�+��|>-8� 126 | _�>�6kQ/8+�vvmܛEm��E� l�a/t� ��n 127 | {�������soy;6ѻ�At��;�?�߾���V6�@���-�� ��e2���C0�:�ꏡJ45E)��k��fK�X���L�ڃ��tP��w�b1ȷ*���Ȅ�4��~����Coo�h�O++ʧd��29c�$��@��W�P��i��7�ę��߿߸��o��ZI��_��J'���2���]�%��������m��M}�`���>�w}��+�����\v����eO �C��`d�+P���c�Ğ��A:\WI�O����N�G���h������B�vj����QH�����>��:]Hl�� �}����x�c`d``b���j��m�2�s0���֙0����O���@u L QZ� �x�c`d`����#������F0EP�m��x�mSMHTQ=�{�;��(B� �$b&2��`"C ��D�a�…���!�D��� -$$h�BD���YD� w!SD��v�+S������;�9��>��(�Mx�(�L=�%d�"��2�2�X�Wpü ���#�0w�y�}���_בS?��S�r�&����r�`d����"���29$�;Y����.J�.�2RC��E\UP���U�vG�IF��5��@��dz)�I�Y�����*� �8 zar��b������y�|8 g��R���s̻�)�o߇�H��1zq��=��9���p9�zr�^��a�'�m��;��!�̠[J�s�������m(�����q�c�'V��%�"+H�� M1�X�H��|�� N��D~�T���� � � 130 | � B h � � & � 131 | n 132 | �n��*h� j����v\�R� \��>l���pJ"�24�� H � �!x""v#"#�$:$�%<%�& 133 | &`&�'f'�( (>(�)))x)�*�++�+�--@-�..4.b.�/6/X/�00F0�0�0�1141~1�1�1�2B2�3�4 134 | 44.4@4R4d4v525>5P5b5t5�5�5�5�5�6P6b6t6�6�6�6�6�7�7�7�7�7�7�8R99*9<9N9`9r9�:�:�:�;;;*;<;N;`;r;�<<<,<>.>f?&?�?�@@@@@l@�ADAbA�A�A�BBB�C 135 | C�C�D�D�E 136 | E(FFLF^�vY 0w��x��[�n#�u.��ڻ���8 0��0A#���ǻ�xa�#Q#b)RKR3��V�(��즻���'�M����e��<@���$�9u�Hj$oY�����������(��z/Q{���K��xO}��U�����e�P���'2�H齯e��������0��P����d����A*�O1�k�ˇ!��ԟ?\��G�������~����'J���}�)�,�����d�T�M�� �2�H}����?V�{�d������F{�)�O�_>����'c��QƟ�/�d��G��������D}��OձJ!�(��ZMU��z�"���g�)~~��V�R��Hp�1~�*W��j�~L�N�W������R��u_O �8�7Ϟ>���Z��͒41�8�燦5�^����6[�1�cb c�=�t��p���|�t/ ��jj�^�4T�?�a��a�/X���A�+-;֯�u�8GP�)��W~&ض�g 137 | pb�q@�x�s�r��fy ��>}�kS�pY��8Z�����G��f��Ls���1>����\ 138 | ?daU�b�܄��±���;�N��S��Q�u�^���E���5N��p^��d#���a��)֋�: ��a�.�ݭ�~���t�ku����[�P�7� ����M�:e��C���9�^�F��yc퐽���a�>5�g�넹�%�c�hX˻;�Ԩ>��,E�۠@Z� 139 | �#������K1��Hr��fn�j�rU�0�gʣ��bnj�U��,kK}���C�} 140 | �y���'W�ۂ������3��5����˸j����G���Pm؝t��s��w`���m��!_���Z ���� �2��j�����Թz ���-d�kh���s��˻��"�Yp�=��~����3F-�w 141 | ��� �F>Dq���J|q��89Y�gg�})g ��bj�R����w���ς�K�]�ޢ�[�7b�]�Ӹ���L��$�J_�*ff\���)�#�|��'/1��z�9 ��+���Ԅe��Z��8[L�9��TjT�wn8�#�L��O�o-O�L�R�F��KI��$,]�8$��S��zMӌ�[�<�y��&.��5 l�ƺ�'�RV�UK��rf��9���M(]q��y9�g�L(Y3e;�k'���� kk87�$��˕s�s������5�A6�:����0%Wbκ��{K�����\IM����$W|5.�>��C� s�kW��N�|��5�w,h���ߵ�Jh�C�<�������{aR!��!�ۻ��� L�+�t�^�p�V���U��XŁ�>ǖ����u� ڿ�̑��|���-�c"+;�Dm����;�^���"y����ػ��ReH�}^��f>u���nbﶀ�'�F���=�����Z�գ�W����������jȄ{[6�M[�#���55� 142 | .��c�ۿ7��#��I��� }�T*���;�Z�֍�a%�� �^�@2B,����1ѴrU��Uݫ7��ޗ�Q�sDD���9]O��*��ҵ6��\$�"����ޗ�1K@G�f�v��������s����������<�s��;���f��y=Ϸj����p��K��e ��T����%� 143 | �p���=��*XS�����fg��E�är�p���:ٰ�6��5�N/J #�X���.���q�r�[�����!���k9 �z���vy?��m1nS�t+��iR�1[jc�9W����������Ԉ���.c��XL�oU�"KZ���J��Ǜ�u;N�rVi#��� �l��3�4>٨,˜��:������b9�{"r��i���뎯���"<�M�SP-�;_�ܽ���鳷�]qZ��|�鮥/q4]�kkV�^�^sD�����!�k���{iW���VO�O�^[�S[�#��m�C&�S�@]�9��Y*������+r���zvwT&���K������K�!�2O ���9��]�:�5Oj�S���l5�Hyƚ����M�6O�W��*o�>aX>�Ok��,�"�~�tOVe٬��;�����������υbu.����,k�?}�S����*����I���M�g�ҕ�kO��2����=��`!�.Xw�lf.H���������+�9E��8fnޚ����Iq%�鞉�{��Oߩ ���<��^>��z�+ov�VK�g]��\�Fz�H�>q������i]���ӅC'g ��Y-���`�]u.��^�� 144 | جy��Dlw>_��ղ�z��I��p��욓�)�B��;zn�����x��)�wҋ�yB�U����?c>gT�3�d��m�ׁ7O��ѯ��{{=���\ n>{���ԟ�y�-;�1�ͥ�q�e,O5 145 | ���g����@|�2ݢV�)?�c�n$�_7<|��s�v�q�w�]�,|;�Y��ԟ=�=zg�8��E�o>ܿmwGN�]�� 146 | x�)�*뜽���۪���X�q����R�;��S� o?�����z�����K����^ۧ�?��;�{�u��g�<ןZ>���w=��|)��[i���r��'���f���;�箺���w�T�N_Ǫ �;��� ���-X�~l�O�G� � ��3��������{'��3�P�?b�{���3��dZ����[�� ���o��������2�>Ɔ%��wvm^gxiq���+̽~=�����Y'��צT��$ӂ�1tpw[��az$�HѸW�y*��#�<�7�����g���ֹ7�-��I�cNq���f �����-�[^� 147 | r�X� �=�2` I��O\��Y'Y_�L��ʡ`��0��Z����]~����r�t��� 148 | m��%�$��8�+�#�N��e�����S��@����k�f<��e����R��!�9��Qͯ�w;�wk˳���\��9-~������_i�pn���2k���{b��Ң}��mT�pĵyU��14{R_��Q�x�H�/엒5������'C8Z��nX���RwE�a���tw|=��(�/���__���y�~�蛸���|9Ŝ�Iaz���כ�M����Iqf���3�#��X�L�63�Ԛa�k� ���]� 0�w��1�Kd�t�ȕ��7� �1ih��&\A��jf ���ַ&,^�iQ,^�_`= ���P� �����$��$,� DhW��@O��O���ٙ �'�lS���CPw`d���8��~���%������������l���L�%Sa�$�9a[����n⬮���}��uF�~o�5��#�0���4D&�!�xf�JKg`SfM};��q��Kv0�� ~C�n$���hJp����y��,��aG�a`�_��2�Ty����:J����F�Ԏ� '�K�:*��l�B�����p�s�&>�:�'(��f�G�/�IJ����?�y��oύ��#��p�AU���l��@S|,����%M���,�/8ä��f��B��nFoB�h&�D�;��a�Ҥ��&$�N썱�*�҄0&e[�b�f�*��uB1f������qN��F�$�™��b�"ػ�[�JʪA��D\�Ր �/ڃ��p�@0_��~�D���f�8�b�] ��=)(qҦ��t|m/��N� 153 | 1P�!���o޴��OIw�J$^�9�I^���jR묑�w�%7 154 | y�����#��y@&)U2����� ���M��.�m�2I3�11,c��R9O��6��`æS#�}Q��Zv6�Q��x�N@=5_:��zl ";�A4���*'Z��5U�@:F�s5���8�#��Z�����} 155 | �9Q@�}���t9���17sS (&Ҍ�d t�����!$"�Z>�Fz����9�I��H\Otb.�7�w�[�G�/�|�d���M������s�F�e��m�m|���5�iW. Nn��Ћ C�\�Z4P"�K����Q��E��`�/�9�����B<0���J%�� I-a��T1��sG��آ��.%׸�V[ۈC����=p���yJ!n�q�7 156 | �1Z�"�2�֛�c���.8���]���󯭠$��*9���[΅Bg.L\��i]����a����tRUr 157 | v��@ϗ9#QY�M ��R�$ț����ك�����{���^���KW��f�Q�鵨�4OuW��/� ��*���O��g���w�$ ��Z�����ꜷzt�v��a�t��5h���\ ��;'���5����������#��Vo����5�tz'�n�'�����s~���O��w/O:�W�%���t�;��@t���B��ƾS Y��p�z��vFos����)���Ek0�_v[sq9�����Ꝁl��;�K���iHuܿx;��:�4�d`F��I��5�& �Py`x�!� �~�&�Zݮ�]]�0g�� V�lC�N�NH����u�z�Vti��@WІW�^{��fx�>���u��c��|�%DO1l{� ���68k3 �������5�AC�3�F�(o:�v`Z��"��A�� ���~ �^=���Bs��U�[;Oڭ.I�����{�� u�3������������x�m�Gl����ߡ����ۧC�n���{+5ZJ��b�H܈uA�� A���HHl�R|G���sE�ۯL��׋�"D�����THaEULq%�TJie�UNyTTIeUTUMu5�TKmu�UO} 4�HcM4�LHs�$J�B��R��Zm��^��t�.C'�u�U7���S/���W?� 0� � 1�0Í0�(��16���*����;kl���� "���Jۂ� ڦ���.y������i�C����q��b��&�憻n������=p�}GL��V�=��d}��Y��n�l���i�\���m��>�g��Zl�3v[j�e����'�:捷�z굗�9���.;��.:���.�A�����eg�B�����L��6)l��CɉQy�9���p!x�c���p"(b##c_�Ɲ  �؝6I22h��y89 ,160��i�#'��鴋�ffp٨�����#b#s��F5oG#�CGrHHI$l��`������uK�F&�ͬ)l ..�%` -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | SDR Detector 3 | 4 | 5 | 6 |
7 |
8 |
{{> titleBar}}
9 |
{{> titleBarRight}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{> Basestations}} 18 |
19 |
20 |
21 |
22 | {{> gsmReadings}} 23 |
24 |
25 |
26 | 27 | 28 | 37 | 38 | 45 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | Template.titleBarRight.helpers({ 2 | currentStatus: function() { 3 | return Status.get() 4 | } 5 | }); 6 | 7 | Template.titleBar.helpers({ 8 | threatLevel: function() { 9 | return Status.threatLevel() || "UNDEFINED" 10 | }, 11 | threatScore: function() { 12 | return Status.threatScore() 13 | }, 14 | numPages: function() { 15 | return Status.numPages() || 0 // not necessary on clean app install 16 | }, 17 | location: function() { 18 | var bts = Basestations.Basestations.findOne({mcc: {$gt: 1}}) 19 | if(bts) { 20 | var cc = CountryCodes.CountryCodes.findOne({MCC: bts.mcc}) 21 | 22 | if(cc) 23 | return cc.Country + " (" + bts.mcc + ")" 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /client/templates/arfcn-bands/arfcnBands.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /client/templates/arfcn-bands/arfcnBands.js: -------------------------------------------------------------------------------- 1 | Template.arfcnBands.helpers({ 2 | count: function(){ 3 | return ARFCNBands.ARFCNBands.find().count() 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /client/templates/arfcns/arfcns.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /client/templates/arfcns/arfcns.js: -------------------------------------------------------------------------------- 1 | Template.arfcns.helpers({ 2 | count: function() { 3 | return ARFCNs.ARFCNs.find().count() 4 | }, 5 | used: function() { 6 | return ARFCNs.ARFCNs.find({currentlyBroadcasting: true}).count() 7 | } 8 | }); 9 | 10 | /* Template.charts.onRendered(function() { 11 | // Get the context of the canvas element we want to select 12 | var ctx = document.getElementById("myChart").getContext("2d"); 13 | // var ctx2 = document.getElementById("myChart2").getContext("2d"); 14 | // var ctx2 = document.getElementById("myBarChart").getContext("2d"); 15 | 16 | // Set the options 17 | var options = { 18 | 19 | ///Boolean - Whether grid lines are shown across the chart 20 | scaleShowGridLines: true, 21 | 22 | //String - Colour of the grid lines 23 | scaleGridLineColor: "rgba(0,0,0,.05)", 24 | 25 | //Number - Width of the grid lines 26 | scaleGridLineWidth: 2, 27 | 28 | //Boolean - Whether to show horizontal lines (except X axis) 29 | scaleShowHorizontalLines: true, 30 | 31 | //Boolean - Whether to show vertical lines (except Y axis) 32 | scaleShowVerticalLines: true, 33 | 34 | //Boolean - Whether the line is curved between points 35 | bezierCurve: true, 36 | 37 | //Number - Tension of the bezier curve between points 38 | bezierCurveTension: 0.1, 39 | 40 | //Boolean - Whether to show a dot for each point 41 | pointDot: true, 42 | 43 | //Number - Radius of each point dot in pixels 44 | pointDotRadius: 1, 45 | 46 | //Number - Pixel width of point dot stroke 47 | pointDotStrokeWidth: 1, 48 | 49 | //Number - amount extra to add to the radius to cater for hit detection outside the drawn point 50 | pointHitDetectionRadius: 20, 51 | 52 | //Boolean - Whether to show a stroke for datasets 53 | datasetStroke: true, 54 | 55 | //Number - Pixel width of dataset stroke 56 | datasetStrokeWidth: 2, 57 | 58 | //Boolean - Whether to fill the dataset with a colour 59 | datasetFill: true, 60 | 61 | //String - A legend template 62 | legendTemplate: "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" 63 | 64 | }; 65 | 66 | var options2 = { 67 | //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value 68 | scaleBeginAtZero : true, 69 | 70 | //Boolean - Whether grid lines are shown across the chart 71 | scaleShowGridLines : true, 72 | 73 | //String - Colour of the grid lines 74 | scaleGridLineColor : "rgba(0,0,0,.05)", 75 | 76 | //Number - Width of the grid lines 77 | scaleGridLineWidth : 1, 78 | 79 | //Boolean - Whether to show horizontal lines (except X axis) 80 | scaleShowHorizontalLines: true, 81 | 82 | //Boolean - Whether to show vertical lines (except Y axis) 83 | scaleShowVerticalLines: true, 84 | 85 | //Boolean - If there is a stroke on each bar 86 | barShowStroke : true, 87 | 88 | //Number - Pixel width of the bar stroke 89 | barStrokeWidth : 2, 90 | 91 | //Number - Spacing between each of the X value sets 92 | barValueSpacing : 5, 93 | 94 | //Number - Spacing between data sets within X values 95 | barDatasetSpacing : 1, 96 | 97 | //String - A legend template 98 | legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" 99 | 100 | } 101 | 102 | 103 | // Set the data 104 | var data = { 105 | labels: ["2","5","4"], 106 | datasets: [{ 107 | label: "", 108 | fillColor: "rgba(220,220,220,0.2)", 109 | strokeColor: "rgba(220,220,220,1)", 110 | pointColor: "rgba(220,220,220,1)", 111 | pointStrokeColor: "#fff", 112 | pointHighlightFill: "#fff", 113 | pointHighlightStroke: "rgba(220,220,220,1)", 114 | data: [random(), random(), random(), random(), random(), random(), random()] 115 | /* }, 116 | 117 | { 118 | label: "My Second dataset", 119 | fillColor: "rgba(151,187,205,0.2)", 120 | strokeColor: "rgba(151,187,205,1)", 121 | pointColor: "rgba(151,187,205,1)", 122 | pointStrokeColor: "#fff", 123 | pointHighlightFill: "#fff", 124 | pointHighlightStroke: "rgba(151,187,205,1)", 125 | data: [random(), random(), random(), random(), random(), random(), random()] 126 | */ 127 | 128 | /* 129 | 130 | }] 131 | }; 132 | 133 | var data2 = { 134 | labels: ["January", "February", "March", "April", "May", "June", "July"], 135 | datasets: [ 136 | { 137 | label: "My First dataset", 138 | fillColor: "rgba(220,220,220,0.5)", 139 | strokeColor: "rgba(220,220,220,0.8)", 140 | highlightFill: "rgba(220,220,220,0.75)", 141 | highlightStroke: "rgba(220,220,220,1)", 142 | data: [65, 59, 80, 81, 56, 55, 40] 143 | }, 144 | { 145 | label: "My Second dataset", 146 | fillColor: "rgba(151,187,205,0.5)", 147 | strokeColor: "rgba(151,187,205,0.8)", 148 | highlightFill: "rgba(151,187,205,0.75)", 149 | highlightStroke: "rgba(151,187,205,1)", 150 | data: [28, 48, 40, 19, 86, 27, 90] 151 | } 152 | ] 153 | }; 154 | 155 | // draw the charts 156 | //myLineChart.data.labels = ["1","2","3"]; 157 | //var myLineChart = new Chart(ctx).Line(data, options); 158 | var myLineChart = new Chart(ctx).Line(data, options); 159 | // var myBarChart = new Chart(ctx2).Line(data2, options2); 160 | 161 | }); 162 | 163 | function random() { 164 | return Math.floor((Math.random() * 100) + 1); 165 | } 166 | */ 167 | -------------------------------------------------------------------------------- /client/templates/basestations/basestations.html: -------------------------------------------------------------------------------- 1 | 28 | 29 | 39 | 40 | 47 | -------------------------------------------------------------------------------- /client/templates/basestations/basestations.js: -------------------------------------------------------------------------------- 1 | Template.Basestations.helpers({ 2 | basestations: function() { 3 | return Basestations.Basestations.find() 4 | }, 5 | count: function() { 6 | return Basestations.Basestations.find().count() 7 | }, 8 | }); 9 | 10 | Template.Basestation.onCreated(function() { 11 | var template = this 12 | this.autorun(function() { 13 | var arfcnId = Template.currentData().arfcnId 14 | template.subscribe('gsm-readings/signal-strength', arfcnId) 15 | }) 16 | 17 | }) 18 | 19 | Template.Basestation.helpers({ 20 | pages: function() { 21 | var a = ARFCNs.ARFCNs.findOne(this.arfcnId) 22 | 23 | if(a) 24 | return a.numPages 25 | }, 26 | arfcn: function() { 27 | var a = ARFCNs.ARFCNs.findOne(this.arfcnId) 28 | 29 | if(a) 30 | return a.channelNumber 31 | }, 32 | signalStrength: function() { 33 | var r = GSMReadings.GSMReadings.findOne({ 34 | arfcnId: this.arfcnId, 35 | signalStrength: {$gt: -100} 36 | }, {sort: {timestamp: -1}}) 37 | 38 | if(r) 39 | return r.signalStrength 40 | }, 41 | 42 | provider: function() { 43 | //console.log('provider'); 44 | //console.log(this.mcc + " " + this.mnc); 45 | //this is only a test, find all documents with the same MCC. Only 2 will be found for MCC=262 46 | var pra = CountryCodes.CountryCodes.find({MCC: this.mcc}).fetch() 47 | //console.log(pra); 48 | // (test2) 49 | var prb = CountryCodes.CountryCodes.find({$and: [{MNC: this.mnc},{MCC: this.mcc}]}).fetch() 50 | //console.log(prb); 51 | //this should actually do it (both conditions are linked with &) 52 | var pr = CountryCodes.CountryCodes.findOne({MCC: this.mcc, MNC: this.mnc}) 53 | //console.log(pr); 54 | if(pr) 55 | { return pr.Network } 56 | else { return 'nothing found' } 57 | //but nothing found all the time... 58 | 59 | } 60 | }); 61 | 62 | /** transform: function(doc) { 63 | doc.provider = CountryCodes.CountryCodes.findOne({ 64 | mnc: { $in: [ doc.mnc ] } 65 | }); 66 | return doc ; 67 | } 68 | }); 69 | **/ 70 | 71 | 72 | /*Template.Basestations.helpers({ 73 | count: function(){ 74 | return UniqueBTSs.find().count() 75 | }, 76 | readings: function() { 77 | return Basestations.find({}, { 78 | sort: { MNC:1 } 79 | }); 80 | } 81 | }); 82 | 83 | 84 | Template.Basestations.helpers({ 85 | recordedAt: function(){ 86 | return new Date(this.timestamp) 87 | }, 88 | }); 89 | 90 | Template.country.helpers({ 91 | getlocation: function() { 92 | return CountryCodes.findOne({ mcc: this.mcc }); 93 | } 94 | }); 95 | */ 96 | //var countryCodes = CountryCodes.get({_id: this.MCC}); 97 | //return countryCodes.County; 98 | //return UniqueBTSs.find().count() 99 | //return UniqueBTSs.findOne(MCC) 100 | -------------------------------------------------------------------------------- /client/templates/country-codes/countrycodes.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /client/templates/country-codes/countrycodes.js: -------------------------------------------------------------------------------- 1 | Template.countryCodes.helpers({ 2 | getlocation: function() { 3 | return CountryCodes.CountryCodes.findOne() || "UNDEFINED" 4 | } 5 | }); 6 | 7 | //var countryCodes = CountryCodes.get({_id: this.MCC}); 8 | //return countryCodes.County; 9 | //return UniqueBTSs.find().count() 10 | //return UniqueBTSs.findOne(MCC) 11 | -------------------------------------------------------------------------------- /client/templates/games/games.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 20 | -------------------------------------------------------------------------------- /client/templates/games/games.js: -------------------------------------------------------------------------------- 1 | Template.games.helpers({ 2 | availableGames: function() { 3 | return [ 4 | { 5 | methodName: 'games/hide-and-seek/play', 6 | friendlyName: "Start (p1 = rtl-scanner)", 7 | }, 8 | { 9 | methodName: 'games/trick-or-treat/play', 10 | friendlyName: "Start (p1 = kali)", 11 | }, 12 | ] 13 | } 14 | }); 15 | 16 | Template.game.events({ 17 | "click": function(event, template){ 18 | Meteor.call(template.data.methodName, true, function(error, result) { 19 | if(error) 20 | Status.set(error) 21 | 22 | Status.set(template.data.friendlyName + " started.") 23 | }) 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /client/templates/gsm-readings/gsm-readings.html: -------------------------------------------------------------------------------- 1 | 38 | 39 | 53 | -------------------------------------------------------------------------------- /client/templates/gsm-readings/gsm-readings.js: -------------------------------------------------------------------------------- 1 | Template.gsmReadings.helpers({ 2 | readings: function() { 3 | return GSMReadings.GSMReadings.find({}, { 4 | sort: { timestamp: -1 }, 5 | limit: 300 6 | }); 7 | }, 8 | // count: function() { 9 | // return GSMReadings.GSMReadings.find().count() 10 | // } 11 | }); 12 | -------------------------------------------------------------------------------- /client/templates/paging/paging.html: -------------------------------------------------------------------------------- 1 | 26 | 27 | 35 | 36 | 43 | -------------------------------------------------------------------------------- /client/templates/paging/paging.js: -------------------------------------------------------------------------------- 1 | Template.Paging.helpers({ 2 | basestations: function() { 3 | return Paging.Paging.find() 4 | }, 5 | count: function() { 6 | return Paging.Paging.find().count() 7 | } 8 | }) 9 | 10 | 11 | /*Template.Basestations.helpers({ 12 | count: function(){ 13 | return UniqueBTSs.find().count() 14 | }, 15 | readings: function() { 16 | return Basestations.find({}, { 17 | sort: { MNC:1 } 18 | }); 19 | } 20 | }); 21 | 22 | 23 | Template.Basestations.helpers({ 24 | recordedAt: function(){ 25 | return new Date(this.timestamp) 26 | }, 27 | }); 28 | 29 | Template.country.helpers({ 30 | getlocation: function() { 31 | return CountryCodes.findOne({ mcc: this.mcc }); 32 | } 33 | }); 34 | */ 35 | //var countryCodes = CountryCodes.get({_id: this.MCC}); 36 | //return countryCodes.County; 37 | //return UniqueBTSs.find().count() 38 | //return UniqueBTSs.findOne(MCC) 39 | -------------------------------------------------------------------------------- /client/templates/scanners/scanners.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 18 | -------------------------------------------------------------------------------- /client/templates/scanners/scanners.js: -------------------------------------------------------------------------------- 1 | Template.scanners.helpers({ 2 | availableScanners: function() { 3 | return [ 4 | { 5 | methodName: 'scanners/p1-rtlsdr-scanner/run', 6 | friendlyName: "Signal Strength (rtlsdr-sc.)", 7 | }, 8 | { 9 | methodName: 'scanners/p2-airprobe/run', 10 | friendlyName: "Process 2 (airprobe)", 11 | }, 12 | { 13 | methodName: 'scanners/p3-airprobe/run', 14 | friendlyName: "Process 3 (airprobe)", 15 | }, 16 | { 17 | methodName: 'scanners/p1-kal-rtl/run', 18 | friendlyName: "Process 1 (kal-rtl)", 19 | /* }, 20 | { 21 | methodName: 'scanners/p1-kal-hackrf/run', 22 | friendlyName: "Process 1 (kal-hackrf) [unimpl.]", 23 | */ } 24 | ] 25 | } 26 | }); 27 | 28 | Template.scanner.events({ 29 | "click": function(event, template) { 30 | Status.set("Client started " + template.data.friendlyName) 31 | Meteor.call(template.data.methodName, function(error, result) { 32 | if(error) 33 | return Status.set(error.message) 34 | 35 | Status.set(template.data.friendlyName + " done running.") 36 | }) 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /example_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "pythonLocation": "/usr/bin/python2", 3 | "airprobeLocation": "/home/user/workspace/SDR-Detector/desec/grcs/airprobe_rtlsdr.py", 4 | "rtlsdrscanLocation": "/home/user/workspace/RTLSDR-Scanner/src/rtlsdr_scan.py", 5 | "quickScanLogLocation": "/home/user/workspace/quickScan.csv", 6 | "tSharkLocation": "/usr/local/bin/tshark", 7 | "kalRTLLocation": "/home/user/workspace/kalibrate-rtl/src/kal", 8 | "kalHackRFLocation": "/home/user/workspace/kalibrate-hackrf/src/kal", 9 | "quickScanTolerance": 43, 10 | "quickScanNumSweeps": 1, 11 | "quickScanDwell": 0.262, 12 | "quickScanFFT": 1024, 13 | "deepScanPeriod": 15, 14 | "testing": { 15 | "testRTLSDRScannerCSV": "/home/user/workspace/SDR-Detector/desec/packages/gsm-scanners/scanners/p1-rtlsdr-scanner-tests.csv", 16 | "testPCAPFile": "/home/user/workspace/SDR-Detector/desec/packages/gsm-scanners/scanners/p2-bts-test-file.pcapng" 17 | }, 18 | "public": { 19 | "PRTG_URL": "http://192.168.5.109", 20 | "PRTG_PORT": "5050", 21 | "PRTG_TOKEN": "D1BDFAC3-F654-4200-A433-4ABE67FBCA49" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /grcs/DISCLAIMER: -------------------------------------------------------------------------------- 1 | DISCLAIMER of SDR-Detector 2 | 3 | 1. This product is meant for educational purposes only. It is, by any means, a Proof-of-Concept (PoC). We can not guarantee that this software is able to shielded against surveillance of any form. 4 | 2. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of this software. BEFORE using our software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of software like ours, to see if this is permitted. 5 | 3. If you ever encounter a problem with our software or feel that your intellectual property has been hurt or not handled according to its copyrights, please contact us. 6 | 7 | * LICENSE: https://github.com/He3556/SDR-Detector/blob/master/license 8 | 9 | The above copyright notice and this permission notice shall be included in all copies 10 | or substantial portions of the Software. 11 | (GNU GENERAL PUBLIC LICENSE) 12 | 13 | 14 | This project contains the following code and data: 15 | GNU Radio Block 16 | - airprobe_rtlsdr.grc 17 | https://github.com/ptrkrysik/gr-gsm 18 | http://gnuradio.org/ 19 | 20 | - Mobile Country Code Table (http://mcc-mnc.com/) 21 | The MIT License (MIT) 22 | Copyright (c) 2016 Mustafa Al-Bassam 23 | https://github.com/musalbas/mcc-mnc-table 24 | 25 | 26 | ------------------------- 27 | developed by 28 | He3556 https://github.com/He3556 29 | Marvin https://github.com/marvinmarnold 30 | copyright 2016 31 | dm-development.de 32 | ------------------------- 33 | -------------------------------------------------------------------------------- /grcs/Readme: -------------------------------------------------------------------------------- 1 | This project is based on: Nodejs, npm and the Meteor platform. https://www.meteor.com/ 2 | Other components (like GNU Radio, Wireshark and GR-GSM) have to be installed in order to use SDR-Detector. It's also possible to expand the system with detectors for **WiFi, Bluetooth, UMTS and LTE frequency bands**. (using existing open source projects) 3 | 4 | **The software is compatible with:** 5 | [Stingwatch](https://github.com/marvinmarnold/stingwatch) (Cordova based Stingray (IMSI-Catcher) detection) 6 | [Meteor ICC](https://github.com/marvinmarnold/meteor-imsi-catcher-catcher) Meteor-imsi-catcher-catcher (Meteor package for client + server side IMSI-catcher detection.) 7 | [API Client](https://github.com/marvinmarnold/StingrayAPIClient) (more details soon) 8 | 9 | *** 10 | 11 | Usage (https://github.com/He3556/SDR-Detector/wiki/Directions-For-Use) 12 | 13 | Detections (https://github.com/He3556/SDR-Detector/wiki/Thread-level-and-score-calculation) 14 | 15 | WiKi (https://github.com/He3556/SDR-Detector/wiki) 16 | 17 | 18 | ``` 19 | // Must run as `root` in order to capture data 20 | sudo meteor --settings settings.json 21 | By default, Meteor runs the webserver on `http://localhost:3000` 22 | ``` 23 | 24 | *** 25 | 26 | ------------------------- 27 | developed by 28 | He3556 https://github.com/He3556 29 | Marvin https://github.com/marvinmarnold 30 | copyright 2016 31 | dm-development.de 32 | ------------------------- 33 | 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SDR-Detector", 3 | "private": true, 4 | "scripts": { 5 | "start": "meteor run" 6 | }, 7 | "dependencies": { 8 | "meteor-node-stubs": "~0.2.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/arfcn-bands/arfcn-bands-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.addAsync('ARFCNBands - correct GSM 850 calculations', function(test, done) { 2 | var gsm850 = { 3 | "name": "GSM-850 Downlink", 4 | "startARFCN": 240, 5 | "endARFCN": 251, 6 | "isActive": true, 7 | "m": 0.2, 8 | "b": 843.6 9 | } 10 | 11 | ARFCNBands.ARFCNBands.insert(gsm850, test850ARFCNs) 12 | 13 | function test850ARFCNs(error, result) { 14 | if(error) 15 | test.fail("Could not create ARFCN Band for GSM 850 " + error) 16 | 17 | // give arfcn N and get center freq, upper freq, lower freq 18 | var arfcnBandId = result 19 | 20 | // 128 - start band 21 | var arfcn128 = { 22 | channelNumber: 128, 23 | arfcnBandId: arfcnBandId, 24 | startFreq: 869.1, 25 | centerFreq: 869.2, 26 | endFreq: 869.3, 27 | } 28 | test.equal(ARFCNBands.getARFCN(arfcnBandId, 128), arfcn128) 29 | 30 | // 251 - end band 31 | var arfcn251 = { 32 | channelNumber: 251, 33 | arfcnBandId: arfcnBandId, 34 | startFreq: 893.7, 35 | centerFreq: 893.8, 36 | endFreq: 893.9, 37 | } 38 | test.equal(ARFCNBands.getARFCN(arfcnBandId, 251), arfcn251) 39 | 40 | // 244 - mid band 41 | var arfcn244 = { 42 | channelNumber: 244, 43 | arfcnBandId: arfcnBandId, 44 | startFreq: 892.3, 45 | centerFreq: 892.4, 46 | endFreq: 892.5, 47 | } 48 | test.equal(ARFCNBands.getARFCN(arfcnBandId, 244), arfcn244) 49 | 50 | done() 51 | } 52 | }) 53 | 54 | Tinytest.addAsync('ARFCNBands - create ARFCNs', function(test, done) { 55 | var gsm850 = { 56 | "name": "GSM-850 Downlink", 57 | "startARFCN": 240, 58 | "endARFCN": 251, 59 | "isActive": true, 60 | "m": 0.2, 61 | "b": 843.6 62 | } 63 | var arfcnBandId 64 | 65 | ARFCNBands.ARFCNBands.insert(gsm850, testCreateARFCNs) 66 | 67 | function testCreateARFCNs(error, result) { 68 | if(error) 69 | test.fail("Could not create ARFCN Band for GSM 850 " + error) 70 | 71 | // give arfcn N and get center freq, upper freq, lower freq 72 | arfcnBandId = result 73 | ARFCNBands.createARFCNs(arfcnBandId, onARFCNsCreated) 74 | } 75 | 76 | function onARFCNsCreated(error, result) { 77 | // test that right number of ARFCNs exist 78 | test.equal(ARFCNs.ARFCNs.find({arfcnBandId: arfcnBandId}).count(), 12) 79 | ARFCNBands.createARFCNs(arfcnBandId, onARFCNsRecreated) 80 | } 81 | 82 | function onARFCNsRecreated(error, result) { 83 | // test that same number of ARFCNs exist 84 | test.equal(ARFCNs.ARFCNs.find({arfcnBandId: arfcnBandId}).count(), 12) 85 | done() 86 | } 87 | }) 88 | -------------------------------------------------------------------------------- /packages/arfcn-bands/arfcn-bands.js: -------------------------------------------------------------------------------- 1 | ARFCNBands = { 2 | ARFCNBands: new Mongo.Collection("ARFCNBands"), 3 | arfcnBand: function(arfcnBandId) { 4 | return this.ARFCNBands.findOne(arfcnBandId) 5 | }, 6 | centerFreq: function(arfcnBandId) { 7 | var band = this.arfcnBand(arfcnBandId) 8 | var centerFreq = math.chain(band.m) 9 | .multiply((band.startARFCN + band.endARFCN)/2) 10 | .add(band.b) 11 | return prettyNumber(centerFreq.done()) 12 | }, 13 | // Get ARFCN arguments for an arfcnBandId and specific channel 14 | getARFCN: function(arfcnBandId, channelNumber) { 15 | var band = this.arfcnBand(arfcnBandId) 16 | var centerFrequency = math.chain(band.m) 17 | .multiply(channelNumber) 18 | .add(band.b) 19 | 20 | var radius = math.chain(band.m).divide(2).done() 21 | 22 | return { 23 | channelNumber: channelNumber, 24 | arfcnBandId: arfcnBandId, 25 | startFreq: prettyNumber(centerFrequency.subtract(radius).done()), 26 | centerFreq: prettyNumber(centerFrequency.done()), 27 | endFreq: prettyNumber(centerFrequency.add(radius).done()), 28 | } 29 | }, 30 | createARFCNs: function(arfcnId, callback) { 31 | var arfcnBand = this.arfcnBand(arfcnId) 32 | 33 | var counter = 0 34 | 35 | for(var i = arfcnBand.startARFCN; i <= arfcnBand.endARFCN; i++) { 36 | newARFCN = this.getARFCN(arfcnId, i) 37 | var oldARFCN = ARFCNs.ARFCNs.findOne(newARFCN); 38 | 39 | if(oldARFCN) { 40 | // skip already exists 41 | } else { 42 | ARFCNs.ARFCNs.insert(_.extend(newARFCN, {lastSeen: 0}), function(error, result) { 43 | if(error) 44 | callback(error) 45 | 46 | counter++; 47 | }) 48 | } 49 | if(i === arfcnBand.endARFCN && !!callback) 50 | callback(undefined, counter) 51 | } 52 | } 53 | } 54 | 55 | var prettyNumber = function(number) { 56 | return parseFloat(math.format(number, {precision: 14})) 57 | } 58 | 59 | var numWDecimals = function(number, numDecimals) { 60 | // return (number * 10^numDecimals) / 10^numDecimals 61 | return number 62 | } 63 | 64 | ARFCNBands.ARFCNBands.attachSchema(new SimpleSchema({ 65 | name: { 66 | type: String, 67 | }, 68 | startARFCN: { 69 | type: Number, 70 | }, 71 | endARFCN: { 72 | type: Number, 73 | }, 74 | isActive: { 75 | type: Boolean, 76 | }, 77 | // Individual ARFCNs will be generated following the formula 78 | // ARFCN_Center_Freq = m * ARFCN + b 79 | m: { 80 | type: Number, 81 | decimal: true 82 | }, 83 | b: { 84 | type: Number, 85 | decimal: true 86 | } 87 | })) 88 | 89 | // Equivalent of autopublish and insecure plugins 90 | // TODO REMOVE FOR SECURITY 91 | 92 | if(Meteor.isServer) { 93 | Meteor.publish("arfcn-bands", function() { 94 | return ARFCNBands.ARFCNBands.find() 95 | }); 96 | } 97 | 98 | if(Meteor.isClient){ 99 | Meteor.startup(function(){ 100 | Meteor.subscribe('arfcn-bands') 101 | }); 102 | } 103 | 104 | ARFCNBands.ARFCNBands.allow({ 105 | insert: function(){ 106 | return true; 107 | }, 108 | update: function(){ 109 | return true; 110 | }, 111 | remove: function(){ 112 | return true; 113 | } 114 | }); 115 | -------------------------------------------------------------------------------- /packages/arfcn-bands/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'marvin:arfcn-bands', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: '', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.2.1'); 15 | 16 | // Core packages 17 | api.use([ 18 | 'ecmascript', 19 | 'mongo', 20 | 'underscore', 21 | ]); 22 | 23 | // Community 24 | api.use([ 25 | 'aldeed:collection2@2.5.0', 26 | 'aldeed:simple-schema@1.4.0', 27 | 'ecwyne:mathjs@2.4.0', 28 | 'marvin:arfcns' 29 | ]); 30 | 31 | // Common 32 | api.addFiles([ 33 | 'arfcn-bands.js' 34 | ]); 35 | 36 | api.export("ARFCNBands"); 37 | }); 38 | 39 | Package.onTest(function(api) { 40 | api.use([ 41 | 'ecmascript', 42 | 'tinytest', 43 | ]); 44 | 45 | api.use([ 46 | 'marvin:arfcn-bands', 47 | 'marvin:arfcns', 48 | ]); 49 | 50 | api.addFiles([ 51 | 'arfcn-bands-tests.js', 52 | ]); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/arfcns/arfcns-tests.js: -------------------------------------------------------------------------------- 1 | if(Meteor.isServer) { 2 | Tinytest.add('ARFCNs - findOneByFreq', function (test) { 3 | ARFCNs.ARFCNs.remove({}) 4 | var arfcn1 = { 5 | channelNumber: 123, 6 | arfcnBandId: "test", 7 | startFreq: 10, 8 | centerFreq: 11, 9 | endFreq: 13 10 | } 11 | var arfcn2 = { 12 | channelNumber: 456, 13 | arfcnBandId: "test", 14 | startFreq: 100, 15 | centerFreq: 110, 16 | endFreq: 130 17 | } 18 | var arfcn3 = { 19 | channelNumber: 789, 20 | arfcnBandId: "test", 21 | startFreq: 1000, 22 | centerFreq: 1100, 23 | endFreq: 1300 24 | } 25 | var arfcn1Id = ARFCNs.ARFCNs.insert(arfcn1) 26 | var arfcn2Id = ARFCNs.ARFCNs.insert(arfcn2) 27 | var arfcn3Id = ARFCNs.ARFCNs.insert(arfcn3) 28 | 29 | test.equal(ARFCNs.findOneByFreq(12)._id, arfcn1Id) 30 | test.equal(ARFCNs.findOneByFreq(130)._id, arfcn2Id) 31 | test.equal(ARFCNs.findOneByFreq(1000)._id, arfcn3Id) 32 | test.equal(ARFCNs.findOneByFreq(12.01)._id, arfcn1Id) 33 | test.equal(ARFCNs.findOneByFreq(130.0000)._id, arfcn2Id) 34 | test.equal(ARFCNs.findOneByFreq(1000.000001)._id, arfcn3Id) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /packages/arfcns/arfcns.js: -------------------------------------------------------------------------------- 1 | // Write your package code here! 2 | ARFCNs = { 3 | ARFCNs: new Mongo.Collection("ARFCNs"), 4 | arfcn: function(arfcnId) { 5 | return this.ARFCNs.findOne(arfcnId) 6 | }, 7 | // Find ARFCN that freq falls into 8 | // TODO ARFCNs should not overlap frequencies 9 | findOneByFreq: function(frequency) { 10 | return this.ARFCNs.findOne({ $and: 11 | [ 12 | {startFreq: {$lte: frequency}}, 13 | {endFreq: {$gte: frequency}} 14 | ] 15 | }) 16 | }, 17 | } 18 | 19 | ARFCNs.ARFCNs.attachSchema(new SimpleSchema({ 20 | channelNumber: { 21 | type: Number, 22 | denyUpdate: true, 23 | }, 24 | arfcnBandId: { 25 | type: String, 26 | denyUpdate: true, 27 | }, 28 | startFreq: { 29 | type: Number, 30 | decimal: true, 31 | denyUpdate: true, 32 | }, 33 | centerFreq: { 34 | type: Number, 35 | decimal: true, 36 | denyUpdate: true, 37 | }, 38 | endFreq: { 39 | type: Number, 40 | decimal: true, 41 | denyUpdate: true 42 | }, 43 | lastSeen: { 44 | type: Date, 45 | defaultValue: new Date(0) 46 | }, 47 | lastSignalStrength: { 48 | type: Number, 49 | decimal: true, 50 | defaultValue: -999999 51 | }, 52 | lastRecorded: { 53 | type: Date, 54 | defaultValue: new Date(0) 55 | }, 56 | firstSeen: { 57 | type: Date, 58 | defaultValue: new Date(0) 59 | }, 60 | numPages: { 61 | type: Number, 62 | defaultValue: 0 63 | } 64 | })) 65 | 66 | // Equivalent of autopublish and insecure plugins 67 | // TODO REMOVE FOR SECURITY 68 | 69 | if(Meteor.isServer) { 70 | Meteor.publish("arfcns", function() { 71 | return ARFCNs.ARFCNs.find() 72 | }); 73 | } 74 | 75 | if(Meteor.isClient){ 76 | Meteor.startup(function(){ 77 | Meteor.subscribe('arfcns') 78 | }); 79 | } 80 | 81 | ARFCNs.ARFCNs.allow({ 82 | insert: function(){ 83 | return true; 84 | }, 85 | update: function(){ 86 | return true; 87 | }, 88 | remove: function(){ 89 | return true; 90 | } 91 | }); 92 | -------------------------------------------------------------------------------- /packages/arfcns/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'marvin:arfcns', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: '', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.2.1'); 15 | 16 | // Core packages 17 | api.use([ 18 | 'ecmascript', 19 | 'mongo' 20 | ]); 21 | 22 | // Community 23 | api.use([ 24 | 'aldeed:collection2@2.5.0', 25 | 'aldeed:simple-schema@1.4.0' 26 | ]); 27 | 28 | // Common 29 | api.addFiles([ 30 | 'arfcns.js' 31 | ]); 32 | 33 | 34 | api.export("ARFCNs"); 35 | }); 36 | 37 | Package.onTest(function(api) { 38 | api.use([ 39 | 'ecmascript', 40 | 'tinytest' 41 | ]); 42 | 43 | api.use('marvin:arfcns'); 44 | 45 | api.addFiles([ 46 | 'arfcns-tests.js' 47 | ]); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/basestations/basestations-tests.js: -------------------------------------------------------------------------------- 1 | // if(Meteor.isServer) { 2 | // Tinytest.add('new BTS', function (test) { 3 | // Basestations.Basestations.remove({}) 4 | // var arfcn1 = { 5 | // channelNumber: 123, 6 | // arfcnBandId: "test", 7 | // startFreq: 10, 8 | // centerFreq: 11, 9 | // endFreq: 13 10 | // } 11 | // var arfcn2 = { 12 | // channelNumber: 456, 13 | // arfcnBandId: "test", 14 | // startFreq: 100, 15 | // centerFreq: 110, 16 | // endFreq: 130 17 | // } 18 | // var arfcn3 = { 19 | // channelNumber: 789, 20 | // arfcnBandId: "test", 21 | // startFreq: 1000, 22 | // centerFreq: 1100, 23 | // endFreq: 1300 24 | // } 25 | // var arfcn1Id = ARFCNs.ARFCNs.insert(arfcn1) 26 | // var arfcn2Id = ARFCNs.ARFCNs.insert(arfcn2) 27 | // var arfcn3Id = ARFCNs.ARFCNs.insert(arfcn3) 28 | // 29 | // test.equal(ARFCNs.findOneByFreq(12)._id, arfcn1Id) 30 | // test.equal(ARFCNs.findOneByFreq(130)._id, arfcn2Id) 31 | // test.equal(ARFCNs.findOneByFreq(1000)._id, arfcn3Id) 32 | // test.equal(ARFCNs.findOneByFreq(12.01)._id, arfcn1Id) 33 | // test.equal(ARFCNs.findOneByFreq(130.0000)._id, arfcn2Id) 34 | // test.equal(ARFCNs.findOneByFreq(1000.000001)._id, arfcn3Id) 35 | // }) 36 | // } 37 | -------------------------------------------------------------------------------- /packages/basestations/basestations.js: -------------------------------------------------------------------------------- 1 | Basestations = { 2 | Basestations: new Mongo.Collection("Basestations"), 3 | IMMUTABLE_FIELDS: ['cid', 'arfcnId', 'lac', 'mnc', 'mcc'] 4 | } 5 | 6 | Basestations.Basestations.attachSchema(new SimpleSchema({ 7 | firstSeen: { 8 | type: Date, 9 | denyUpdate: true 10 | }, 11 | lastSeen: { 12 | type: Date 13 | }, 14 | arfcnId: { 15 | type: String, 16 | denyUpdate: true 17 | }, 18 | lastARFCNId: { 19 | type: String, 20 | optional: true, 21 | }, 22 | cid: { 23 | type: Number, 24 | denyUpdate: true 25 | }, 26 | mcc: { 27 | type: Number, 28 | denyUpdate: true 29 | }, 30 | mnc: { 31 | type: Number, 32 | denyUpdate: true 33 | }, 34 | lac: { 35 | type: Number, 36 | denyUpdate: true 37 | }, 38 | })) 39 | 40 | // Equivalent of autopublish and insecure plugins 41 | // TODO REMOVE FOR SECURITY 42 | 43 | if(Meteor.isServer) { 44 | Meteor.publish("basestations", function() { 45 | return Basestations.Basestations.find() 46 | }); 47 | } 48 | 49 | if(Meteor.isClient){ 50 | Meteor.startup(function(){ 51 | Meteor.subscribe('basestations') 52 | }); 53 | } 54 | 55 | Basestations.Basestations.allow({ 56 | insert: function(){ 57 | return true; 58 | }, 59 | update: function(){ 60 | return true; 61 | }, 62 | remove: function(){ 63 | return true; 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /packages/basestations/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'marvin:basestations', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: '', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.2.1'); 15 | api.use([ 16 | 'ecmascript', 17 | 'mongo', 18 | ]); 19 | 20 | // Community 21 | api.use([ 22 | 'aldeed:collection2@2.5.0', 23 | 'aldeed:simple-schema@1.4.0', 24 | 'marvin:arfcns' 25 | ]); 26 | 27 | api.addFiles('basestations.js'); 28 | 29 | api.export("Basestations"); 30 | }); 31 | 32 | Package.onTest(function(api) { 33 | api.use('ecmascript'); 34 | api.use('tinytest'); 35 | api.use([ 36 | 'marvin:basestations', 37 | 'marvin:arfcns' 38 | ]); 39 | api.addFiles('basestations-tests.js'); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/bts-broadcasts/bts-broadcasts-tests.js: -------------------------------------------------------------------------------- 1 | // Write your tests here! 2 | // Here is an example. 3 | // Tinytest.add('example', function (test) { 4 | // test.equal(true, true); 5 | // }); 6 | -------------------------------------------------------------------------------- /packages/bts-broadcasts/bts-broadcasts.js: -------------------------------------------------------------------------------- 1 | // Write your package code here! 2 | BTSBroadcasts = { 3 | BTSBroadcasts: new Mongo.Collection("BTSBroadcast"), 4 | } 5 | 6 | BTSBroadcasts.BTSBroadcasts.attachSchema(new SimpleSchema({ 7 | arfcnId: { 8 | type: String, 9 | optional: true 10 | }, 11 | basestationId: { 12 | type: String, 13 | optional: true 14 | }, 15 | firstSeen: { 16 | type: Date 17 | }, 18 | lastSeen: { 19 | type: Date 20 | } 21 | })) 22 | 23 | // Equivalent of autopublish and insecure plugins 24 | // TODO REMOVE FOR SECURITY 25 | 26 | if(Meteor.isServer) { 27 | Meteor.publish("bts-broadcasts", function() { 28 | return BTSBroadcasts.BTSBroadcasts.find() 29 | }); 30 | } 31 | 32 | if(Meteor.isClient){ 33 | Meteor.startup(function(){ 34 | Meteor.subscribe('bts-broadcasts') 35 | }); 36 | } 37 | 38 | BTSBroadcasts.BTSBroadcasts.allow({ 39 | insert: function(){ 40 | return true; 41 | }, 42 | update: function(){ 43 | return true; 44 | }, 45 | remove: function(){ 46 | return true; 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /packages/bts-broadcasts/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'marvin:bts-broadcasts', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: '', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.2.1'); 15 | api.use('ecmascript'); 16 | 17 | // Community 18 | api.use([ 19 | 'aldeed:collection2@2.5.0', 20 | 'aldeed:simple-schema@1.4.0' 21 | ]); 22 | 23 | api.addFiles('bts-broadcasts.js'); 24 | 25 | api.export("BTSBroadcasts"); 26 | }); 27 | 28 | Package.onTest(function(api) { 29 | api.use('ecmascript'); 30 | api.use('tinytest'); 31 | api.use('marvin:bts-broadcasts'); 32 | api.addFiles('bts-broadcasts-tests.js'); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/countrycodes/countrycodes-test.js: -------------------------------------------------------------------------------- 1 | Tinytest.addAsync('CountryCodes - seed from JSON', function(test, done) { 2 | CountryCodes.CountryCodes.remove({}) 3 | CountryCodes.seed() 4 | 5 | test.isTrue(CountryCodes.CountryCodes.find().count() > 0) 6 | done() 7 | }) 8 | -------------------------------------------------------------------------------- /packages/countrycodes/countrycodes.js: -------------------------------------------------------------------------------- 1 | CountryCodes = {} 2 | CountryCodes.CountryCodes = new Mongo.Collection("countryCodes"); 3 | // All readings should point to an ARFCN 4 | /*GSMReadings.GSMReadings.before.insert(function(userId, doc) { 5 | ARFCNs.ARFCNs.update(doc.arfcnId, {$set: {lastSeen: doc.timestamp}}) 6 | 7 | if(doc.signalStrength) 8 | ARFCNs.ARFCNs.update(doc.arfcnId, {$set: {lastSignalStrength: doc.signalStrength}}) 9 | }) 10 | */ 11 | 12 | // Some readings should create a new BTs and some should update lastRecorded of ARFCN 13 | /*GSMReadings.GSMReadings.before.insert(function(userId, doc) { 14 | var containsValidBTS = Basestations.Basestations.simpleSchema().namedContext() 15 | .validateOne(doc, "cid", {modifier: false}); 16 | 17 | if(containsValidBTS) { 18 | ARFCNs.ARFCNs.update(doc.arfcnId, {$set: {lastRecorded: new Date()}}) 19 | Basestations.Basestations.upsert({cid: doc.cid}, 20 | {$set: { 21 | cid: doc.cid, 22 | lastSeen: new Date(), 23 | }, 24 | $setOnInsert: { 25 | firstSeen: new Date() 26 | }}) 27 | } 28 | }) 29 | */ 30 | 31 | 32 | CountryCodes.CountryCodes.attachSchema(new SimpleSchema({ 33 | MCC: { 34 | type: Number, 35 | denyUpdate: true 36 | }, 37 | MCCint: { 38 | type: Number, 39 | denyUpdate: true, 40 | optional: true 41 | }, 42 | MNC: { 43 | type: Number, 44 | denyUpdate: true 45 | }, 46 | MNCint: { 47 | type: Number, 48 | denyUpdate: true, 49 | optional: true 50 | }, 51 | ISO: { 52 | type: String, 53 | denyUpdate: true 54 | }, 55 | Country: { 56 | type: String, 57 | denyUpdate: true 58 | }, 59 | CountryCode: { 60 | type: String, 61 | denyUpdate: true 62 | }, 63 | Network: { 64 | type: String, 65 | denyUpdate: true, 66 | optional: true 67 | } 68 | })) 69 | 70 | // Equivalent of autopublish and insecure plugins 71 | // TODO REMOVE FOR SECURITY 72 | 73 | if(Meteor.isServer) { 74 | Meteor.publish("countryCodes", function() { 75 | return CountryCodes.CountryCodes.find() 76 | }); 77 | } 78 | 79 | if(Meteor.isClient){ 80 | Meteor.startup(function(){ 81 | Meteor.subscribe('countryCodes') 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /packages/countrycodes/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'marvin:countrycodes', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: 'Holds information about the Country and Network', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.2.1'); 15 | 16 | // Core packages 17 | api.use([ 18 | 'ecmascript', 19 | 'mongo', 20 | 'underscore' 21 | ]); 22 | 23 | // Community 24 | api.use([ 25 | 'aldeed:collection2@2.5.0', 26 | 'aldeed:simple-schema@1.4.0', 27 | 'matb33:collection-hooks@0.8.1', 28 | 'marvin:arfcns', 29 | 'marvin:basestations', 30 | 'marvin:status' 31 | ]); 32 | 33 | 34 | // Common 35 | api.addFiles([ 36 | 'countrycodes.js' 37 | ]); 38 | 39 | api.addFiles("private/mcc-mnc-table.json", 'server', { isAsset: true }); 40 | 41 | api.addFiles([ 42 | 'seed.js' 43 | ], 'server') 44 | 45 | api.export("CountryCodes"); 46 | }); 47 | 48 | 49 | Package.onTest(function(api) { 50 | api.use([ 51 | 'ecmascript', 52 | 'tinytest' 53 | ]); 54 | 55 | api.use('marvin:countrycodes') 56 | 57 | api.addFiles([ 58 | 'countrycodes-test.js' 59 | ], 'server'); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/countrycodes/seed.js: -------------------------------------------------------------------------------- 1 | CountryCodes.seed = function() { 2 | if(CountryCodes.CountryCodes.find().count() === 0) { 3 | var json = {}; 4 | 5 | json = JSON.parse(Assets.getText("private/mcc-mnc-table.json")); 6 | _.each(json, function(o) { 7 | mcc = parseInt(o.mcc) 8 | mnc = parseInt(o.mnc) 9 | 10 | if(mcc && mnc) { 11 | CountryCodes.CountryCodes.insert({ 12 | MCC: mcc, 13 | MNC: mnc, 14 | ISO: o.iso, 15 | Country: o.country, 16 | CountryCode: o.country_code, 17 | Network: o.network 18 | }) 19 | } 20 | 21 | }) 22 | 23 | Status.set(CountryCodes.CountryCodes.find().count() + " country codes created"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/detectors/detectors-server.js: -------------------------------------------------------------------------------- 1 | Detectors = { 2 | _detectors: [], 3 | setDetectors: function(detectors) { 4 | this._detectors = detectors 5 | }, 6 | preRun: function(gsmReading) { 7 | _.each(this._detectors, function(detector) { 8 | detector.preRun(gsmReading) 9 | }) 10 | }, 11 | postRun: function(gsmReading) { 12 | _.each(this._detectors, function(detector) { 13 | detector.postRun(gsmReading) 14 | }) 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /packages/detectors/detectors/changing-basestation-tests.js: -------------------------------------------------------------------------------- 1 | var setup = function() { 2 | Detectors.setDetectors([Detectors.ChangingBasestation]) 3 | Basestations.Basestations.remove({}) 4 | ARFCNs.ARFCNs.remove({}) 5 | Threats.Threats.remove({}) 6 | GSMReadings.GSMReadings.remove({}) 7 | } 8 | 9 | var requiredFields = [ 10 | {field: "cid", threatScore: 50}, 11 | {field: "mnc", threatScore: 50}, 12 | {field: "mcc", threatScore: 50}, 13 | {field: "lac", threatScore: 30} 14 | ] 15 | _.each(requiredFields, function(row) { 16 | var field = row.field 17 | var title = 'Detectors - Changing Basestation - Changing the ' + field + " raises an alarm" 18 | Tinytest.add(title , function (test) { 19 | setup() 20 | 21 | var reading = { 22 | arfcnId: "a1", 23 | scanner: "test", 24 | frequency: 123, 25 | timestamp: new Date() 26 | } 27 | // Add the required fields to the reading. 28 | // They will not change during testing. 29 | var staticFields = _.difference(_.map(requiredFields, function(f) {return f.field}), [field]) 30 | _.each(staticFields, function(staticField) { 31 | reading[staticField] = 923874 32 | }) 33 | reading[field] = 243 34 | GSMReadings.GSMReadings.insert(reading) 35 | 36 | reading[field] = 4349 37 | GSMReadings.GSMReadings.insert(reading) 38 | 39 | test.equal(Status.threatScore(), row.threatScore) 40 | test.equal(Threats.Threats.find().count(), 1) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /packages/detectors/detectors/changing-basestation.js: -------------------------------------------------------------------------------- 1 | Detectors.ChangingBasestation = { 2 | name: "ChangingBasestation", 3 | preRun: function(gsmReading) { 4 | var bts = Basestations.Basestations.findOne({arfcnId: gsmReading.arfcnId}) 5 | if(!!bts) { 6 | var requiredFields = [ 7 | {field: "cid", threatScore: 50}, 8 | {field: "mnc", threatScore: 50}, 9 | {field: "mcc", threatScore: 50}, 10 | {field: "lac", threatScore: 30} 11 | ] 12 | _.each(requiredFields, function(row) { 13 | var field = row.field 14 | var originalValue = bts[field], 15 | newValue = gsmReading[field] 16 | if(newValue !== originalValue) { 17 | Threats.Threats.insert({ 18 | timestamp: new Date(), 19 | gsmReadingId: gsmReading._id, 20 | score: row.threatScore, 21 | detector: "ChangingBasestation", 22 | message: field + " changed from " + originalValue + " to " + newValue 23 | }) 24 | } 25 | }) 26 | } 27 | }, 28 | postRun: function(gsmReading) { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/detectors/detectors/missing-channel-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('Detectors - Missing Channel - Test', function(test) { 2 | ARFCNs.ARFCNs.remove({}) 3 | Threats.Threats.remove({}) 4 | Detectors.MissingChannel.reset() 5 | 6 | Detectors.MissingChannel.preRun({ 7 | arfcnId: "11", 8 | signalStrength: 123 9 | }) 10 | Detectors.MissingChannel.preRun({ 11 | arfcnId: "22", 12 | signalStrength: 123 13 | }) 14 | Detectors.MissingChannel.nextIteration() 15 | Detectors.MissingChannel.preRun({ 16 | arfcnId: "22", 17 | signalStrength: 123 18 | }) 19 | Detectors.MissingChannel.nextIteration() 20 | 21 | test.equal(Status.threatScore(), 30) 22 | test.equal(Threats.Threats.find().count(), 1) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/detectors/detectors/missing-channel.js: -------------------------------------------------------------------------------- 1 | var _knownChannels = [] 2 | var _detectedChannels = [] 3 | Detectors.MissingChannel = { 4 | name: "MissingChannel", 5 | preRun: function(gsmReading) { 6 | // if valid reading and channel not already detected 7 | if(gsmReading.signalStrength > Meteor.settings.quickScanTolerance && 8 | !alreadyDetected(gsmReading)) { 9 | 10 | _detectedChannels.push(gsmReading.arfcnId) 11 | if(!alreadyKnown(gsmReading)) { 12 | _knownChannels.push(gsmReading.arfcnId) 13 | } 14 | } 15 | }, 16 | postRun: function(gsmReading) { 17 | }, 18 | nextIteration: function() { 19 | var diff = _.difference(_knownChannels, _detectedChannels) 20 | if(diff.length > 0) { 21 | _.each(diff, function(missingChan) { 22 | Threats.Threats.insert({ 23 | timestamp: new Date(), 24 | score: 30, 25 | detector: "MissingChannel", 26 | message: "Missing channel (" + missingChan + ") not detected [+30 score]" 27 | }) 28 | }) 29 | } 30 | 31 | _detectedChannels = [] 32 | }, 33 | reset: function() { 34 | _detectedChannels = [] 35 | _knownChannels = [] 36 | } 37 | } 38 | 39 | var alreadyDetected = function(gsmReading) { 40 | _.find(_detectedChannels, function(arfcnId) { 41 | arfcnId === gsmReading.arfcnId 42 | }) 43 | } 44 | 45 | var alreadyKnown = function(gsmReading) { 46 | _.find(_knownChannels, function(arfcnId) { 47 | arfcnId === gsmReading.arfcnId 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /packages/detectors/detectors/new-channel-tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('Detectors - New Channel - Test', function(test) { 2 | ARFCNs.ARFCNs.remove({}) 3 | Threats.Threats.remove({}) 4 | 5 | Detectors.NewChannel.preRun({ 6 | arfcnId: "adfasdf", 7 | signalStrength: 123 8 | }) 9 | 10 | test.equal(Status.threatScore(), 60) 11 | test.equal(Threats.Threats.find().count(), 1) 12 | }) 13 | -------------------------------------------------------------------------------- /packages/detectors/detectors/new-channel.js: -------------------------------------------------------------------------------- 1 | Detectors.NewChannel = { 2 | name: "NewChannel", 3 | preRun: function(gsmReading) { 4 | var arfcn = ARFCNs.ARFCNs.findOne(gsmReading.arfcnId), 5 | signalStrength = gsmReading.signalStrength 6 | 7 | if(arfcn === undefined && signalStrength > Meteor.settings.quickScanTolerance) { 8 | var channel = arfcn ? arfcn.channelNumber : "unknown" 9 | Threats.Threats.insert({ 10 | timestamp: new Date(), 11 | gsmReadingId: gsmReading._id, 12 | score: 60, 13 | detector: "NewChannel", 14 | message: "New channel (" + channel + ") detected with signal strength " + signalStrength + " [+60 score]" 15 | }) 16 | } 17 | }, 18 | postRun: function(gsmReading) { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/detectors/detectors/paging-tests.js: -------------------------------------------------------------------------------- 1 | var setup = function() { 2 | Threats.Threats.remove({}) 3 | Detectors.setDetectors([Detectors.Paging]) 4 | GSMReadings.GSMReadings.remove({}) 5 | Status.ensureDefault() 6 | ARFCNs.ARFCNs.remove({}) 7 | Detectors.Paging.reset() 8 | } 9 | _.each(['tmsi', 'imsi'], function(page) { 10 | Tinytest.add("Detectors - Paging - Test " + page , function (test) { 11 | setup() 12 | 13 | var arfcnId = ARFCNs.ARFCNs.insert({ 14 | channelNumber: 123, 15 | arfcnBandId: "test", 16 | startFreq: 10, 17 | centerFreq: 11, 18 | endFreq: 13 19 | }) 20 | 21 | 22 | // First reading has diff value 23 | var r = { 24 | arfcnId: arfcnId, 25 | scanner: "test", 26 | frequency: 123, 27 | timestamp: new Date() 28 | } 29 | r[page] = 2134 30 | GSMReadings.GSMReadings.insert(r) 31 | 32 | // Enough on the same to trigger one warning 33 | for(let i=1; i<40; i++) { 34 | let reading = { 35 | arfcnId: arfcnId, 36 | scanner: "test", 37 | frequency: 123, 38 | timestamp: new Date() 39 | } 40 | GSMReadings.GSMReadings.insert(reading) 41 | reading[page] = 123 42 | GSMReadings.GSMReadings.insert(reading) 43 | } 44 | 45 | test.equal(ARFCNs.ARFCNs.findOne(arfcnId).numPages, 3) 46 | test.equal(Status.threatScore(), 30) 47 | test.equal(Threats.Threats.find().count(), 1) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /packages/detectors/detectors/paging.js: -------------------------------------------------------------------------------- 1 | // Stores an array of counters for each tuple 2 | // Does not need to be persisted to DB because reset frequently 3 | // Sample values: 4 | // [ 5 | // { 6 | // page: "imsi", 7 | // pageId: 1234, 8 | // arfcnId: 1, 9 | // count: 4 10 | // }, 11 | // { 12 | // page: "tmsi", 13 | // pageId: 5678, 14 | // arfcnId: 4, 15 | // count: 5 16 | // }, 17 | // { 18 | // page: "imsi", 19 | // pageId: 1234, 20 | // arfcnId: 6, 21 | // count: 88 22 | // } 23 | // ] 24 | var _readingCounters = [] 25 | 26 | Detectors.Paging = { 27 | name: "Paging", 28 | preRun: function(gsmReading) { 29 | // Count both IMSI and TMSI pages 30 | _.each(['tmsi', 'imsi'], function(page) { 31 | // Does this Reading contain an IMSI or TMSI 32 | if((gsmReading[page] !== undefined) && (gsmReading[page] > 0)) { 33 | // Find existing counter for each page (imsi or tmsi) 34 | var counterSelector = { 35 | page: page, 36 | arfcnId: gsmReading["arfcnId"], 37 | pageId: gsmReading[page] 38 | } 39 | 40 | var counter = _.findWhere(_readingCounters, counterSelector) 41 | if(counter) { 42 | // If counter already exists, increment count 43 | counter.count += 1 44 | } else { 45 | // If counter does not exist, create and set count to 1 46 | counter = _.extend(counterSelector, {count: 1}) 47 | _readingCounters.push(counter) 48 | } 49 | 50 | var count = counter.count 51 | 52 | // Find ARFCN this page was broadcasted on 53 | var arfcn = ARFCNs.ARFCNs.findOne(counter.arfcnId) 54 | 55 | // If the count for this page is higher than 56 | // any other paging count on the same ARFCN, 57 | // set the count of the ARFCN = count of page 58 | if(arfcn && (arfcn.numPages < count)) { 59 | ARFCNs.ARFCNs.update(counter.arfcnId, {$set: {numPages: count}}) 60 | 61 | // If the count for this page is also > 9, 62 | // add a new Threat w/ score 30. 63 | if(count >= 9) { 64 | Threats.Threats.insert({ 65 | timestamp: new Date(), 66 | gsmReadingId: gsmReading._id, 67 | score: 30, 68 | detector: "Paging", 69 | message: page + " (" + counter.pageId + ") paging detected " + count + " times. [+30 score]" 70 | }) 71 | } 72 | } 73 | 74 | } 75 | }) 76 | }, 77 | postRun: function(gsmReading) { 78 | }, 79 | reset: function() { 80 | _readings = [] 81 | ARFCNs.ARFCNs.update({}, {$set: {numPages: 0}}) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/detectors/detectors/signal-strength-tests.js: -------------------------------------------------------------------------------- 1 | var setup = function() { 2 | ARFCNs.ARFCNs.remove({}) 3 | Threats.Threats.remove({}) 4 | Detectors.SignalStrength.reset() 5 | 6 | // prepolutate with 10 readings 7 | Detectors.SignalStrength.reset() 8 | for(let i=0; i<10; i++) { 9 | Detectors.SignalStrength.preRun({ 10 | arfcnId: "452sfg", 11 | signalStrength: 100, 12 | }) 13 | } 14 | } 15 | Tinytest.add('Detectors - Signal Strength - 15% & 20% tests', function(test) { 16 | setup() 17 | 18 | // First X readings should not cause alarm 19 | test.equal(Status.threatScore(), 0) 20 | 21 | // 15% alarm for 30 22 | Detectors.SignalStrength.preRun({ 23 | arfcnId: "452sfg", 24 | signalStrength: 115, 25 | }) 26 | test.equal(Status.threatScore(), 30) 27 | test.equal(Threats.Threats.find().count(), 1) 28 | 29 | // reset test 30 | setup() 31 | 32 | // 20% alarm for 60 33 | Detectors.SignalStrength.preRun({ 34 | arfcnId: "452sfg", 35 | signalStrength: 120, 36 | }) 37 | test.equal(Status.threatScore(), 60) 38 | test.equal(Threats.Threats.find().count(), 1) 39 | }) 40 | -------------------------------------------------------------------------------- /packages/detectors/detectors/signal-strength.js: -------------------------------------------------------------------------------- 1 | var _previous = [] 2 | var _maxLength = 10 3 | Detectors.SignalStrength = { 4 | name: "SignalStrength", 5 | preRun: function(gsmReading) { 6 | var ss = gsmReading.signalStrength 7 | 8 | if(!!ss){ 9 | if(_previous.length === _maxLength) { 10 | let avg = _.reduce(_previous, function(sum, sss) { 11 | return sum + sss 12 | }, 0) / _previous.length 13 | if(ss >= (avg*20)) { 14 | Threats.Threats.insert({ 15 | timestamp: new Date(), 16 | gsmReadingId: gsmReading._id, 17 | score: 60, 18 | detector: "SignalStrength", 19 | message: "Signal Strength " + ss + " exceeded average " + avg + " by " + (ss-avg)/avg*100 + "% [+60 score]" 20 | }) 21 | } else if(ss >= (avg*15)) { 22 | Threats.Threats.insert({ 23 | timestamp: new Date(), 24 | gsmReadingId: gsmReading._id, 25 | score: 30, 26 | detector: "SignalStrength", 27 | message: "Signal Strength " + ss + " exceeded average " + avg + " by " + (ss-avg)/avg*100 + "% [+30 score]" 28 | }) 29 | } 30 | 31 | _previous.pop() 32 | } 33 | _previous.push(ss) 34 | } 35 | }, 36 | postRun: function(gsmReading) { 37 | }, 38 | reset: function() { 39 | _previous = [] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/detectors/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'marvin:detectors', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: '', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.2.1'); 15 | 16 | api.use([ 17 | 'ecmascript', 18 | 'underscore' 19 | ]); 20 | 21 | api.use([ 22 | 'marvin:arfcns', 23 | 'marvin:status', 24 | 'marvin:threats', 25 | 'marvin:basestations', 26 | ]); 27 | 28 | api.addFiles([ 29 | 'detectors-server.js', 30 | 'detectors/new-channel.js', 31 | 'detectors/signal-strength.js', 32 | 'detectors/changing-basestation.js', 33 | 'detectors/missing-channel.js', 34 | 'detectors/paging.js', 35 | ], "server"); 36 | 37 | api.export("Detectors"); 38 | }); 39 | 40 | Package.onTest(function(api) { 41 | api.use([ 42 | 'ecmascript', 43 | 'tinytest', 44 | 'underscore' 45 | ]); 46 | 47 | api.use([ 48 | 'marvin:detectors', 49 | 'marvin:status', 50 | 'marvin:arfcns', 51 | 'marvin:gsm-readings', 52 | 'marvin:threats', 53 | 'marvin:basestations' 54 | ]); 55 | 56 | api.addFiles([ 57 | 'detectors/changing-basestation-tests.js', 58 | 'detectors/paging-tests.js', 59 | 'detectors/new-channel-tests.js', 60 | 'detectors/missing-channel-tests.js', 61 | 'detectors/signal-strength-tests.js', 62 | ], 'server'); 63 | }); 64 | -------------------------------------------------------------------------------- /packages/gsm-readings/gsm-readings-tests.js: -------------------------------------------------------------------------------- 1 | // cid1,arfcn1 -> cid1,arfcn1 (one bts: ): no detection 2 | // cid1,arfcn1 -> cid1,arfcn2 (two bts: , ): detection until startup details resolved 3 | // cid1,arfcn1 -> cid2,arfcn1 (one bts: ): yes detection - detection tested seperately 4 | // mcc+mnc same behavior as CID 5 | 6 | var setup = function() { 7 | Basestations.Basestations.remove({}) 8 | } 9 | 10 | // cid1,arfcn1 -> cid1,arfcn1 (one bts: ): 11 | // no detection - detection tested seperately 12 | Tinytest.add('GSMReadings - Normal operation', function (test) { 13 | setup() 14 | 15 | GSMReadings.GSMReadings.insert({ 16 | cid: 1, 17 | mcc: 34512, 18 | mnc: 234, 19 | lac: 2343, 20 | arfcnId: "a1", 21 | scanner: "test", 22 | frequency: 123, 23 | timestamp: new Date() 24 | }) 25 | 26 | GSMReadings.GSMReadings.insert({ 27 | cid: 1, 28 | arfcnId: "a1", 29 | scanner: "test", 30 | frequency: 123, 31 | mcc: 34512, 32 | mnc: 234, 33 | lac: 2343, 34 | timestamp: new Date() 35 | }) 36 | 37 | var bts = Basestations.Basestations.findOne() 38 | 39 | test.equal(Basestations.Basestations.find().count(), 1) 40 | test.equal(bts.cid, 1) 41 | test.equal(bts.arfcnId, "a1") 42 | }) 43 | 44 | // A new ARFCN creates a new BTS 45 | // cid1,arfcn1 -> cid1,arfcn2 (two bts: , ): 46 | // detection until startup details resolved - detection tested seperately 47 | Tinytest.add('GSMReadings - New ARFCN', function (test) { 48 | setup() 49 | 50 | GSMReadings.GSMReadings.insert({ 51 | cid: 1, 52 | arfcnId: "a1", 53 | scanner: "test", 54 | frequency: 123, 55 | mcc: 34512, 56 | mnc: 234, 57 | lac: 2343, 58 | timestamp: new Date() 59 | }) 60 | 61 | GSMReadings.GSMReadings.insert({ 62 | cid: 1, 63 | arfcnId: "a2", 64 | scanner: "test", 65 | frequency: 123, 66 | mcc: 34512, 67 | mnc: 234, 68 | lac: 2343, 69 | timestamp: new Date() 70 | }) 71 | 72 | var bts1 = Basestations.Basestations.findOne({arfcnId: "a1"}) 73 | var bts2 = Basestations.Basestations.findOne({arfcnId: "a2"}) 74 | 75 | test.equal(Basestations.Basestations.find().count(), 2) 76 | test.equal(bts1.cid, 1) 77 | test.equal(bts1.arfcnId, "a1") 78 | test.equal(bts1.lastARFCNId, undefined) 79 | test.equal(bts2.cid, 1) 80 | test.equal(bts2.arfcnId, "a2") 81 | 82 | GSMReadings.GSMReadings.insert({ 83 | cid: 1, 84 | arfcnId: "a1", 85 | scanner: "test", 86 | frequency: 123, 87 | mcc: 34512, 88 | mnc: 234, 89 | lac: 2343, 90 | timestamp: new Date() 91 | }) 92 | 93 | var bts1 = Basestations.Basestations.findOne({arfcnId: "a1"}) 94 | 95 | test.equal(Basestations.Basestations.find().count(), 2) 96 | test.equal(bts1.cid, 1) 97 | test.equal(bts1.arfcnId, "a1") 98 | }) 99 | 100 | // example: cid1,arfcn1 -> cid2,arfcn1 (one bts: ): 101 | // mcc+mnc same behavior as CID 102 | // yes detection - detection tested seperately 103 | var requiredFields = ["cid", "mnc", "mcc", "lac"] 104 | _.each(requiredFields, function(field) { 105 | var title = 'GSMReadings - Changing ' + field + " does not create new BTS" 106 | Tinytest.add(title , function (test) { 107 | setup() 108 | 109 | var reading = { 110 | arfcnId: "a1", 111 | scanner: "test", 112 | frequency: 123, 113 | timestamp: new Date() 114 | } 115 | // Add the required fields to the reading. 116 | // They will not change during testing. 117 | var staticFields = _.difference(requiredFields, [field]) 118 | _.each(staticFields, function(staticField) { 119 | reading[staticField] = 923874 120 | }) 121 | reading[field] = 243 122 | GSMReadings.GSMReadings.insert(reading) 123 | 124 | reading[field] = 4349 125 | GSMReadings.GSMReadings.insert(reading) 126 | 127 | var bts1 = Basestations.Basestations.findOne({arfcnId: "a1"}) 128 | test.equal(bts1.arfcnId, "a1") 129 | test.equal(bts1[field], 243) // should keep the original value 130 | }) 131 | }) 132 | -------------------------------------------------------------------------------- /packages/gsm-readings/gsm-readings.js: -------------------------------------------------------------------------------- 1 | GSMReadings = { 2 | ssThreshold: 100 3 | } 4 | 5 | GSMReadings.GSMReadings = new Mongo.Collection("GSMReadings"); 6 | 7 | // All readings should point to an ARFCN 8 | GSMReadings.GSMReadings.before.insert(function(userId, doc) { 9 | // Run detections before any other action 10 | Detectors.preRun(doc) 11 | 12 | var now = new Date() 13 | 14 | // update last time any data came in over ARFCN 15 | ARFCNs.ARFCNs.update(doc.arfcnId, {$set: {lastSeen: doc.timestamp}}) 16 | 17 | // Update last signal strengtj 18 | if(doc.signalStrength) 19 | ARFCNs.ARFCNs.update(doc.arfcnId, {$set: {lastSignalStrength: doc.signalStrength}}) 20 | 21 | var containsValidBTS = _.reduce(Basestations.IMMUTABLE_FIELDS, function(passes, field) { 22 | return passes && Basestations.Basestations.simpleSchema().namedContext().validateOne(doc, field, {modifier: false}) 23 | }, true) 24 | 25 | // console.log("contains valid data: " + containsValidBTS); 26 | if(containsValidBTS) { 27 | // Update last time ARFCN recorded a reading 28 | ARFCNs.ARFCNs.update(doc.arfcnId, {$set: {lastRecorded: now}}) 29 | 30 | //////////////// 31 | // Some readings should create a new BTs and some should update lastRecorded of ARFCN 32 | 33 | // Put values required for a BTS in an array 34 | var btsVals = _.map(Basestations.IMMUTABLE_FIELDS, function(field) { 35 | return doc[field] 36 | }) 37 | 38 | // Build obj of required BTS fields 39 | var bts = _.object(Basestations.IMMUTABLE_FIELDS, btsVals) 40 | 41 | // Check if BTS already exists for this. 42 | // ARFCN is the only field that will always be added to a BTS when discovered. 43 | var existingBTS = Basestations.Basestations.findOne({arfcnId: bts.arfcnId}) 44 | if(!!existingBTS) { 45 | // set last ARFCN, update timestamps 46 | Basestations.Basestations.update(existingBTS._id, {$set: { 47 | lastSeen: now 48 | }}) 49 | } else { // Create a new BTS 50 | _.extend(bts, { 51 | firstSeen: now, 52 | lastSeen: now 53 | }) 54 | 55 | Basestations.Basestations.insert(bts) 56 | } 57 | } 58 | 59 | // Run detections after all action 60 | Detectors.postRun(doc) 61 | }) 62 | 63 | GSMReadings.GSMReadings.attachSchema(new SimpleSchema({ 64 | timestamp: { 65 | type: Date, 66 | denyUpdate: true 67 | }, 68 | frequency: { 69 | type: Number, 70 | decimal: true, 71 | denyUpdate: true 72 | }, 73 | arfcnId: { 74 | type: String, 75 | denyUpdate: true 76 | }, 77 | scanner: { 78 | type: String, 79 | denyUpdate: true 80 | }, 81 | cid: { 82 | type: Number, 83 | optional: true, 84 | denyUpdate: true 85 | }, 86 | mcc: { 87 | type: Number, 88 | optional: true, 89 | denyUpdate: true 90 | }, 91 | mnc: { 92 | type: Number, 93 | optional: true, 94 | denyUpdate: true 95 | }, 96 | lac: { 97 | type: Number, 98 | optional: true, 99 | denyUpdate: true 100 | }, 101 | signalStrength: { 102 | type: Number, 103 | decimal: true, 104 | optional: true, 105 | denyUpdate: true 106 | }, 107 | signalNoise: { 108 | type: Number, 109 | decimal: true, 110 | optional: true, 111 | denyUpdate: true 112 | }, 113 | locUpTimer: { 114 | type: Number, 115 | decimal: true, 116 | optional: true, 117 | denyUpdate: true 118 | }, 119 | /** 120 | pagingChannel: { 121 | type: Number, 122 | decimal: true, 123 | optional: true, 124 | denyUpdate: true 125 | }, 126 | mobileIdentity: { 127 | type: Number, 128 | decimal: true, 129 | optional: true, 130 | denyUpdate: true 131 | }, 132 | **/ 133 | imsi: { 134 | type: Number, 135 | decimal: true, 136 | optional: true, 137 | denyUpdate: true 138 | }, 139 | tmsi: { 140 | type: Number, 141 | decimal: true, 142 | optional: true, 143 | denyUpdate: true 144 | } 145 | /** 146 | statusPagingChannel: { 147 | type: Number, 148 | decimal: true, 149 | optional: true, 150 | denyUpdate: true 151 | }, 152 | **/ 153 | })) 154 | 155 | // Equivalent of autopublish and insecure plugins 156 | // TODO REMOVE FOR SECURITY 157 | 158 | if(Meteor.isServer) { 159 | Meteor.publish("gsm-readings", function() { 160 | return GSMReadings.GSMReadings.find({}, { 161 | sort: { timestamp: -1 }, 162 | limit: 100 163 | }); 164 | }); 165 | 166 | Meteor.publish("gsm-readings/signal-strength", function(arfcnId) { 167 | return GSMReadings.GSMReadings.find({ 168 | arfcnId: arfcnId, 169 | signalStrength: {$gt: -100} 170 | }, {sort: {timestamp: -1}}) 171 | }); 172 | } 173 | 174 | if(Meteor.isClient){ 175 | Meteor.startup(function(){ 176 | Meteor.subscribe('gsm-readings') 177 | }); 178 | } 179 | 180 | GSMReadings.GSMReadings.allow({ 181 | insert: function(){ 182 | return true; 183 | }, 184 | update: function(){ 185 | return true; 186 | }, 187 | remove: function(){ 188 | return true; 189 | } 190 | }); 191 | -------------------------------------------------------------------------------- /packages/gsm-readings/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'marvin:gsm-readings', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: 'Store GSM readings', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.2.1'); 15 | 16 | // Core packages 17 | api.use([ 18 | 'ecmascript', 19 | 'mongo', 20 | 'underscore' 21 | ]); 22 | 23 | // Community 24 | api.use([ 25 | 'aldeed:collection2@2.5.0', 26 | 'aldeed:simple-schema@1.4.0', 27 | 'matb33:collection-hooks@0.8.1', 28 | 'marvin:arfcns', 29 | 'marvin:basestations', 30 | 'marvin:bts-broadcasts', 31 | 'marvin:status', 32 | 'marvin:detectors' 33 | ]); 34 | 35 | // Common 36 | api.addFiles([ 37 | 'gsm-readings.js' 38 | ]); 39 | 40 | api.export("GSMReadings"); 41 | }); 42 | 43 | Package.onTest(function(api) { 44 | api.use([ 45 | 'ecmascript', 46 | 'tinytest', 47 | 'underscore' 48 | ]); 49 | 50 | api.use([ 51 | 'marvin:gsm-readings', 52 | 'marvin:arfcns', 53 | 'marvin:basestations', 54 | 'marvin:bts-broadcasts', 55 | ]); 56 | 57 | api.addFiles([ 58 | 'gsm-readings-tests.js' 59 | ], 'server'); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/gsm-scanners/gsm-scanners-server.js: -------------------------------------------------------------------------------- 1 | _.extend(GSMScanners, { 2 | _locked: false, 3 | _current: 0, 4 | isLocked: function() { 5 | return this._locked 6 | }, 7 | // [{scanner: SCANNER_OBJ, options: OPTIONS}] 8 | runOrder: [], 9 | reset: function() { 10 | this.runOrder = [] 11 | }, 12 | // Runs all the scanners in the list N times 13 | runThroughN: function(n, callback) { 14 | this._current = 0 15 | return this.runN(this.runOrder.length, callback) 16 | }, 17 | // same thing as run but only for N iterations 18 | // callback after all N runs 19 | runN: function(n, callback) { 20 | var thiz = this 21 | if(n > 0) { 22 | thiz.runOne(thiz.currentIndex(), function(error, result) { 23 | thiz._current++ 24 | thiz.runN(n-1, callback) 25 | }) 26 | } else { 27 | callback(undefined, thiz.currentIndex()) 28 | } 29 | }, 30 | // LOOPS INFINITELY, BE SURE YOU WANT TO CALL THIS 31 | run: function(callback) { 32 | var thiz = this 33 | thiz.runOne(thiz.currentIndex(), function(error, result) { 34 | // if(error) 35 | // callback(error) 36 | // callback(error, thiz.currentIndex()) 37 | thiz._current++ 38 | thiz.run(callback) 39 | }) 40 | }, 41 | // call back run at end of 42 | runOne: function(index, callback) { 43 | var scanner = this.runOrder[index].scanner 44 | var options = this.runOrder[index].options || {} 45 | scanner.run(options, callback) 46 | }, 47 | currentIndex: function() { 48 | if(this._current && (this._current < this.runOrder.length)) 49 | return this._current 50 | 51 | return this._current = 0 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /packages/gsm-scanners/gsm-scanners-tests.js: -------------------------------------------------------------------------------- 1 | if(Meteor.isServer) { 2 | Tinytest.addAsync('GSMScanners - run multiple scanners in correct order', function(test, done) { 3 | GSMScanners._current = 0 // reset counter 4 | GSMScanners.runOrder = [ 5 | {scanner: GSMScanners.TestScanner, options: {}}, 6 | {scanner: GSMScanners.TestScanner}, 7 | {scanner: GSMScanners.TestScanner, options: {}} 8 | ] 9 | // TODO, have locked stop scans after a certain time 10 | GSMScanners.runN(5, function(error, response) { 11 | test.isUndefined(error, "Could not run scanner " + response + " - ") 12 | 13 | test.equal(response, 2) 14 | done() 15 | }) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /packages/gsm-scanners/gsm-scanners.js: -------------------------------------------------------------------------------- 1 | GSMScanners = {} 2 | -------------------------------------------------------------------------------- /packages/gsm-scanners/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'marvin:gsm-scanners', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: '', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.2.1'); 15 | 16 | // Core packages 17 | api.use([ 18 | 'ecmascript', 19 | 'mongo', 20 | 'underscore' 21 | ]); 22 | 23 | // Community 24 | api.use([ 25 | 'harrison:papa-parse@1.1.1', 26 | 'marvin:arfcns', 27 | 'marvin:gsm-readings', 28 | 'marvin:status', 29 | 'marvin:bts-broadcasts', 30 | 'marvin:arfcn-bands', 31 | ]); 32 | 33 | // Common 34 | api.addFiles([ 35 | 'gsm-scanners.js' 36 | ]); 37 | 38 | // Server 39 | api.addFiles([ 40 | 'gsm-scanners-server.js', 41 | 'scanners/test-scanner.js', 42 | 'scanners/p1-rtlsdr-scanner.js', 43 | 'scanners/p2-bts.js', 44 | 'scanners/p1-kal.js' 45 | ], 'server'); 46 | 47 | api.export("GSMScanners"); 48 | }); 49 | 50 | Package.onTest(function(api) { 51 | api.use([ 52 | 'ecmascript', 53 | 'tinytest', 54 | 'underscore' 55 | ]); 56 | 57 | api.use([ 58 | 'marvin:gsm-scanners', 59 | 'marvin:arfcns', 60 | 'marvin:gsm-readings', 61 | 'marvin:arfcn-bands', 62 | ]); 63 | 64 | api.addFiles([ 65 | 'gsm-scanners-tests.js', 66 | 'scanners/p2-bts-tests.js', 67 | 'scanners/p1-kal-tests.js', 68 | ]); 69 | 70 | api.addFiles([ 71 | 'scanners/p1-rtlsdr-scanner-tests.js', 72 | ], 'server') 73 | }); 74 | -------------------------------------------------------------------------------- /packages/gsm-scanners/scanners/p1-kal-tests.js: -------------------------------------------------------------------------------- 1 | if(Meteor.isServer) { 2 | var gsm850 = { 3 | "name": "GSM-850 Downlink", 4 | "startARFCN": 240, 5 | "endARFCN": 251, 6 | "isActive": true, 7 | "m": 0.2, 8 | "b": 843.6 9 | } 10 | gsm850id = ARFCNBands.ARFCNBands.insert(gsm850) 11 | 12 | Tinytest.add('GSMScanners - P1Kal - arfcnBandToCommand GSM800', function (test) { 13 | test.equal(GSMScanners.P1Kal.arfcnBandToCommand(gsm850id), "GSM850") 14 | }) 15 | 16 | var gsm9001 = { 17 | name: "E-GSM-900 Downlink (0-124)", 18 | "startARFCN": 0, 19 | "endARFCN": 124, 20 | "isActive": true, 21 | "m": 0.2, 22 | "b": 935 23 | } 24 | gsm9001id = ARFCNBands.ARFCNBands.insert(gsm9001) 25 | Tinytest.add('GSMScanners - P1Kal - arfcnBandToCommand GSM900-1', function (test) { 26 | test.equal(GSMScanners.P1Kal.arfcnBandToCommand(gsm9001id), "GSM900") 27 | }) 28 | 29 | var gsm9002 = { 30 | name: "E-GSM-900 Downlink (975-1023)", 31 | "startARFCN": 975, 32 | "endARFCN": 1023, 33 | "isActive": true, 34 | "m": 0.2, 35 | "b": 730.2 36 | } 37 | 38 | gsm9002id = ARFCNBands.ARFCNBands.insert(gsm9002) 39 | Tinytest.add('GSMScanners - P1Kal - arfcnBandToCommand GSM-900-2', function (test) { 40 | test.equal(GSMScanners.P1Kal.arfcnBandToCommand(gsm9002id), "GSM900") 41 | }) 42 | 43 | Tinytest.add('GSMScanners - P1Kal - parseOutput', function (test) { 44 | GSMReadings.GSMReadings.remove({}) 45 | var data = "GSM-850:\n" + 46 | " chan: 22222222 (892.2MHz + 789Hz) power: 82626.95\n" + 47 | " chan: 11 (892.2MHz + 789Hz) power: 9.95\n" + 48 | " chan: 3 (892.2MHz + 789Hz) power: 1.0" 49 | 50 | ARFCNs.ARFCNs.insert({ 51 | channelNumber: 22222222, 52 | arfcnBandId: gsm850id, 53 | startFreq: 100, 54 | centerFreq: 110, 55 | endFreq: 130 56 | }) 57 | 58 | ARFCNs.ARFCNs.insert({ 59 | channelNumber: 11, 60 | arfcnBandId: gsm850id, 61 | startFreq: 100, 62 | centerFreq: 110, 63 | endFreq: 130 64 | }) 65 | 66 | ARFCNs.ARFCNs.insert({ 67 | channelNumber: 3, 68 | arfcnBandId: gsm850id, 69 | startFreq: 100, 70 | centerFreq: 110, 71 | endFreq: 130 72 | }) 73 | 74 | GSMScanners.P1Kal._arfcnBandId = gsm850id 75 | GSMScanners.P1Kal._threshold = 9324 76 | GSMScanners.P1Kal._testParseOutput(data) 77 | var readings = GSMReadings.GSMReadings.find().fetch() 78 | 79 | test.equal(readings.length, 3) 80 | _.each(readings, function(reading) { 81 | test.equal(reading.frequency, 892.2) 82 | test.equal(reading.signalStrength, 9324) 83 | }) 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /packages/gsm-scanners/scanners/p1-kal.js: -------------------------------------------------------------------------------- 1 | GSMScanners.P1Kal = { 2 | _arfcnBandId: null, 3 | _threshold: null, 4 | // options: arfcnBandId, threshold 5 | run: function(options, callback) { 6 | Status.set("Entering P1HackRF...") 7 | this._arfcnBandId = options.arfcnBandId 8 | this._threshold = options.threshold 9 | scan(options, callback) 10 | }, 11 | arfcnBandToCommand: function(arfcnBandId) { 12 | var centerFreq = ARFCNBands.centerFreq(arfcnBandId) 13 | 14 | var band = _.find(bandCMDConversion, function(band) { 15 | return centerFreq >= band.startFrequency && 16 | centerFreq <= band.endFrequency 17 | }) 18 | 19 | return band.name 20 | }, 21 | _testParseOutput: function(data) { 22 | parseOutput(data) 23 | } 24 | } 25 | 26 | var scan = function(options, callback) { 27 | Status.set("Scanning P1Kal...") 28 | var spawn = Npm.require('child_process').spawn; 29 | 30 | var kalBand = options.kalBand || GSMScanners.P1Kal.arfcnBandToCommand(options.arfcnBandId) 31 | var cmd = spawn(options.kalLocation,[ 32 | '-s', kalBand, 33 | ]) 34 | 35 | cmd.stdout.on('data', Meteor.bindEnvironment( 36 | function (data) { 37 | Status.set("P1Kal (stdout): " + data) 38 | parseOutput(data) 39 | }, 40 | function () {Status.set("P1Kal error handling data")} 41 | )); 42 | 43 | cmd.stderr.on('data', Meteor.bindEnvironment( 44 | function (data) { 45 | // Status.set("P1RTLSDR (stderr)" + data) 46 | }, 47 | function () {Status.set("P1HackRF error handling error")} 48 | )); 49 | 50 | cmd.on('exit', Meteor.bindEnvironment( 51 | function (code) { 52 | Status.set("P1HackRF exiting w/ code " + code + " ...") 53 | if(code == 0) { // success 54 | return callback(undefined, true) 55 | } 56 | callback(code) 57 | }, 58 | function () {Status.set("P1HackRF error exiting")} 59 | )); 60 | 61 | } 62 | 63 | var parseOutput = function(data) { 64 | data = data.toString() 65 | var pat = /\s+chan\:\s([0-9]+)\s\(([0-9]+\.?[0-9]*).*[\+\-]\s([0-9]+\.?[0-9]*).*power:\s([0-9]+\.?[0-9]*)/ 66 | var regx = new RegExp(pat) 67 | 68 | _.each(data.split('\n'), function(line) { 69 | // Status.set("Testing line: " + line) 70 | if(regx.test(line)) { 71 | // Status.set("Line passed: " + line) 72 | var channel = parseInt(regx.exec(line)[1]) 73 | var freq = parseFloat(regx.exec(line)[2]) 74 | var offset = parseFloat(regx.exec(line)[3]) 75 | var power = parseFloat(regx.exec(line)[4]) 76 | // Status.set("channel: " + channel) 77 | // Status.set("freq: " + freq) 78 | // Status.set("offset: " + offset) 79 | // Status.set("power: " + power) 80 | 81 | arfcn = ARFCNs.ARFCNs.findOne({ 82 | // arfcnBandId: GSMScanners.P1Kal._arfcnBandId, 83 | channelNumber: channel 84 | }) 85 | 86 | if(arfcn) { 87 | reading = { 88 | timestamp: new Date(), 89 | arfcnId: arfcn._id, 90 | scanner: "P1Kal", 91 | frequency: freq, 92 | signalStrength: GSMScanners.P1Kal._threshold 93 | } 94 | 95 | GSMReadings.GSMReadings.insert(reading) 96 | } 97 | } 98 | }) 99 | } 100 | 101 | var bandCMDConversion = [ 102 | { 103 | name: "GSM850", 104 | startFrequency: 869.2, 105 | endFrequency: 893.8 106 | }, 107 | { 108 | name: "GSM900", 109 | startFrequency: 925, 110 | endFrequency: 960 111 | }, 112 | { 113 | name: "GSM-R", 114 | startFrequency: 921.0, 115 | endFrequency: 960.0 116 | }, 117 | { 118 | name: "EGSM", 119 | startFrequency: 925.0, 120 | endFrequency: 960.0 121 | }, 122 | { 123 | name: "DCS", 124 | startFrequency: 1805.2, 125 | endFrequency: 1879.8 126 | }, 127 | { 128 | name: "PCS", 129 | startFrequency: 1850.2, 130 | endFrequency: 1989.8 131 | }, 132 | ] 133 | -------------------------------------------------------------------------------- /packages/gsm-scanners/scanners/p1-rtlsdr-scanner-tests.csv: -------------------------------------------------------------------------------- 1 | Time (UTC), Frequency (MHz),Level (dB/Hz) 2 | 1447999247.0, 868.0, -37.4672271523 3 | 1447999247.0, 878.041015625, -42.7910412278 4 | 1447999247.0, 890.21484375, -48.6701972862 5 | 1447999247.0, 890.36328125, -48.5179004445 6 | 1447999247.0, 890.216796875, -48.5837019451 7 | -------------------------------------------------------------------------------- /packages/gsm-scanners/scanners/p1-rtlsdr-scanner-tests.js: -------------------------------------------------------------------------------- 1 | if(Meteor.isServer) { 2 | Tinytest.add('GSMScanners - P1RTLSDR - testParseRow', function (test) { 3 | function removeAll() { 4 | ARFCNs.ARFCNs.remove({}) 5 | test.equal(ARFCNs.ARFCNs.find({}).count(), 0) 6 | GSMReadings.GSMReadings.remove({}) 7 | test.equal(GSMReadings.GSMReadings.find({}).count(), 0) 8 | } 9 | 10 | removeAll() 11 | var arfcn1 = { 12 | channelNumber: 600, 13 | arfcnBandId: "test", 14 | startFreq: 867, 15 | centerFreq: 868, 16 | endFreq: 869 17 | } 18 | var arfcn1Id = ARFCNs.ARFCNs.insert(arfcn1) 19 | 20 | var arfcn2 = { 21 | channelNumber: 700, 22 | arfcnBandId: "test", 23 | startFreq: 877, 24 | centerFreq: 878, 25 | endFreq: 879 26 | } 27 | var arfcn2Id = ARFCNs.ARFCNs.insert(arfcn2) 28 | 29 | var arfcn3 = { 30 | channelNumber: 900, 31 | arfcnBandId: "test", 32 | startFreq: 889, 33 | centerFreq: 890, 34 | endFreq: 891 35 | } 36 | var arfcn3Id = ARFCNs.ARFCNs.insert(arfcn3) 37 | 38 | GSMScanners.P1RTLSDR_SCANNER.testParseScanOutput({ 39 | logLocation: Meteor.settings.testing.testRTLSDRScannerCSV 40 | }) 41 | 42 | // New reading should be created for row 43 | test.equal(GSMReadings.GSMReadings.find().count(), 3) 44 | 45 | var reading1 = GSMReadings.GSMReadings.findOne({arfcnId: arfcn1Id}) 46 | var arfcn1 = ARFCNs.ARFCNs.findOne(arfcn1Id) 47 | test.equal(reading1.timestamp, new Date(1447999247)) 48 | test.equal(reading1.frequency, 868.0) 49 | test.equal(reading1.signalStrength, -37.4672271523) 50 | test.equal(reading1.scanner, "P1RTLSDR") 51 | test.isNotUndefined(arfcn1.firstSeen) 52 | test.isNotUndefined(arfcn1.lastSeen) 53 | test.equal(arfcn1.lastSignalStrength, reading1.signalStrength) 54 | 55 | var reading2 = GSMReadings.GSMReadings.findOne({arfcnId: arfcn2Id}) 56 | var arfcn2 = ARFCNs.ARFCNs.findOne(arfcn2Id) 57 | test.equal(reading2.timestamp, new Date(1447999247)) 58 | test.equal(reading2.frequency, 878.041015625) 59 | test.equal(reading2.signalStrength, -42.7910412278) 60 | test.equal(reading2.scanner, "P1RTLSDR") 61 | test.isNotUndefined(arfcn2.firstSeen) 62 | test.isNotUndefined(arfcn2.lastSeen) 63 | test.equal(arfcn2.lastSignalStrength, reading2.signalStrength) 64 | 65 | var reading3 = GSMReadings.GSMReadings.findOne({arfcnId: arfcn3Id}) 66 | var arfcn3 = ARFCNs.ARFCNs.findOne(arfcn3Id) 67 | test.equal(reading3.timestamp, new Date(1447999247)) 68 | test.equal(reading3.frequency, 890.36328125) 69 | test.equal(reading3.signalStrength, -48.5179004445) 70 | test.equal(reading3.scanner, "P1RTLSDR") 71 | test.isNotUndefined(arfcn3.firstSeen) 72 | test.isNotUndefined(arfcn3.lastSeen) 73 | test.equal(arfcn3.lastSignalStrength, reading3.signalStrength) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /packages/gsm-scanners/scanners/p1-rtlsdr-scanner.js: -------------------------------------------------------------------------------- 1 | var maxReadings = {} 2 | GSMScanners.P1RTLSDR_SCANNER = { 3 | // options: arfcnBandId, pythonLocation, rtlsdrscanLocation, numSweeps, dwell, fft, logLocation 4 | run: function(options, callback) { 5 | Status.set("Entering P1RTLSDR...") 6 | maxReadings = {} 7 | arfcnBand = ARFCNBands.ARFCNBands.findOne(options.arfcnBandId) 8 | scan(arfcnBand, options, callback) 9 | }, 10 | testParseScanOutput: function(options) { 11 | maxReadings = {} 12 | parseScanOutput(options) 13 | } 14 | } 15 | 16 | // {pythonLocation: , rtlsdrscanLocation: , numSweeps: , dwell: , fft: , logLocation:} 17 | var scan = function(arfcnBand, options, callback) { 18 | Status.set("Scanning P1RTLSDR...") 19 | var spawn = Npm.require('child_process').spawn; 20 | 21 | var startARFCN = ARFCNs.ARFCNs.findOne({arfcnBandId: arfcnBand._id, channelNumber: arfcnBand.startARFCN}) 22 | var endARFCN = ARFCNs.ARFCNs.findOne({arfcnBandId: arfcnBand._id, channelNumber: arfcnBand.endARFCN}) 23 | 24 | // Give some padding freq to the scanner 25 | // need to be whole integers 26 | var startScanAtFreq = parseInt(startARFCN.startFreq) - 2 27 | var endScanAtFreq = parseInt(endARFCN.endFreq) + 2 28 | Status.set("Scanning " + startScanAtFreq + "MHz - " + endScanAtFreq + "MHz") 29 | var cmd = spawn(options.pythonLocation, [ 30 | options.rtlsdrscanLocation, // scan with RTLSDR-Scanner 31 | '-s', startScanAtFreq, // start at frequency 32 | '-e', endScanAtFreq, // end at frequency 33 | '-w', options.numSweeps, // number of sweeps 34 | '-d', options.dwell, // dwell (ms), 35 | '-f', options.fft, // number of fft bins 36 | options.logLocation // output dir for scan results 37 | ]) 38 | 39 | cmd.stdout.on('data', Meteor.bindEnvironment( 40 | function (data) { 41 | // Status.set("P1RTLSDR (stdout): " + data) 42 | }, 43 | function () { 44 | // Status.set("P1RTLSDR error handling data") 45 | } 46 | )); 47 | 48 | cmd.stderr.on('data', Meteor.bindEnvironment( 49 | function (data) { 50 | // Status.set("P1RTLSDR (stderr)" + data) 51 | }, 52 | function () {Status.set("P1RTLSDR error handling error")} 53 | )); 54 | 55 | cmd.on('exit', Meteor.bindEnvironment( 56 | function (code) { 57 | // Status.set("P1RTLSDR exiting w/ code " + code + " ...") 58 | if(code == 0) { // success 59 | parseScanOutput(options) 60 | 61 | if(callback) 62 | callback(undefined, true) 63 | } 64 | }, 65 | function () { 66 | // Status.set("P1RTLSDR error exiting") 67 | } 68 | )); 69 | } 70 | 71 | var parseScanOutput = function(options) { 72 | Status.set("P1RTLSDR parseScanOutput") 73 | var fs = Npm.require("fs") 74 | var scanFile = fs.readFileSync(options.logLocation, "utf8") 75 | 76 | Papa.parse(scanFile, { 77 | step: function(row) { 78 | parseRows(row.data) 79 | }, 80 | delimiter: "", // auto-detect 81 | newline: "", // auto-detect 82 | header: true, 83 | dynamicTyping: true, 84 | skipEmptyLines: true, 85 | }) 86 | 87 | Status.set("P1RTLSDR parseScanOutput adding discovered readings...") 88 | _.each(maxReadings, function(reading, arfcnId) { 89 | GSMReadings.GSMReadings.insert(reading) 90 | ARFCNs.ARFCNs.update(reading.arfcnId, {$set: { 91 | lastSeen: new Date() 92 | }, 93 | $setOnInsert: { 94 | firstSeen: new Date() 95 | }}) 96 | }) 97 | Status.set("P1RTLSDR parseScanOutput complete") 98 | } 99 | 100 | // Loop through rows in CSV and parse each one 101 | var parseRows = function(rows) { 102 | // Status.set("P1RTLSDR parseRows") 103 | _.each(rows, function(row) { 104 | parseRow(row); 105 | }) 106 | } 107 | 108 | // { 109 | // 'Time (UTC)': 1447884411, 110 | // 'Frequency (MHz)': 933.798828125, 111 | // 'Level (dB/Hz)': -48.4894048613 112 | // } 113 | var parseRow = function(row, callback) { 114 | row = _.map(row, function(value, key) { 115 | return value; 116 | }); 117 | 118 | // quit on malformed rows 119 | if(row.length != 3) 120 | return 121 | 122 | // Status.set("P1RTLSDR parseRow3: " + row[0] + ", "+ row[1] + ", "+ row[2]) 123 | var arfcn = ARFCNs.findOneByFreq(row[1]) 124 | 125 | if(arfcn) { 126 | if(maxReadings[arfcn._id] && maxReadings[arfcn._id].signalStrength > parseFloat(row[2])) { 127 | // Status.set("P1RTLSDR parseRow6: " + arfcn._id) 128 | } else { 129 | // Status.set("P1RTLSDR parseRow4: ") 130 | maxReadings[arfcn._id] = { 131 | timestamp: new Date(), 132 | frequency: row[1], 133 | scanner: "P1RTLSDR", 134 | arfcnId: arfcn._id, 135 | signalStrength: row[2] 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /packages/gsm-scanners/scanners/p2-bts-tests.js: -------------------------------------------------------------------------------- 1 | if(Meteor.isServer) { 2 | Tinytest.addAsync('GSMScanners - P2BTS - test scan from file', function(test, done) { 3 | GSMReadings.GSMReadings.remove({}) 4 | var arfcn1 = { 5 | channelNumber: 123, 6 | arfcnBandId: "test", 7 | startFreq: 10, 8 | centerFreq: 11, 9 | endFreq: 13 10 | } 11 | var arfcn1Id = ARFCNs.ARFCNs.insert(arfcn1) 12 | // options: arfcnId, fromFile = true, fileLocation, tSharkLocation, [tsharkCaptures] 13 | GSMScanners.P2BTS.run({ 14 | arfcnId: arfcn1Id, 15 | fromFile: true, 16 | fileLocation: Meteor.settings.testing.testPCAPFile, 17 | tSharkLocation: Meteor.settings.tSharkLocation, 18 | scanner: 'aTest', 19 | tSharkCaptures: [ 20 | "gsm_a.bssmap.cell_ci", // CID 21 | 'e212.mcc', // MCC 22 | 'e212.mnc', // MNC 23 | 'gsm_a.lac', // LAC 24 | 'gsmtap.signal_dbm', // Signal Strength 25 | 'gsmtap.snr_db', // Signal/Noise Ratio 26 | 'gsm_a.rr.t3212' // location update timer 27 | ] 28 | }, function(error, result) { 29 | test.isUndefined(error) 30 | test.isTrue(GSMReadings.GSMReadings.find({scanner: "aTest"}).count() > 0) 31 | done() 32 | }) 33 | 34 | 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /packages/gsm-scanners/scanners/p2-bts.js: -------------------------------------------------------------------------------- 1 | var tSharkCMD, airprobeCMD, tSharkCaptures, arfcn, scanner, callback // things that must be file global 2 | 3 | GSMScanners.P2BTS = { 4 | // options: arfcnId, fromFile = false, duration, pythonLocation, airprobeLocation, [tsharkCaptures], scanner (optional) 5 | // options: arfcnId, fromFile = true, fileLocation, tsharkLocation, [tsharkCaptures], scanner (optional) 6 | run: function(options, callbck) { 7 | Status.set("Entering BTS...") 8 | callback = callbck 9 | tSharkCaptures = options.tSharkCaptures 10 | scanner = options.scanner 11 | arfcn = ARFCNs.ARFCNs.findOne(options.arfcnId) 12 | startTShark(options, callback) 13 | 14 | if(options.fromFile) { 15 | Status.set("Read from file") 16 | } else { 17 | Status.set("Read from airprobe") 18 | deepScanCurrentARFCN(options) 19 | Meteor.setTimeout(function() { 20 | Status.set("Stopping scan on frequency: " + arfcn.centerFreq); 21 | airprobeCMD.kill(); 22 | tSharkCMD.kill(); 23 | }, options.duration * 1000); // 100 ms to let previous scan die 24 | } 25 | } 26 | } 27 | 28 | var startTShark = function(options, callback) { 29 | var spawn = Npm.require('child_process').spawn 30 | var tSharkArgs = buildTSharkArgs(options) 31 | 32 | tSharkCMD = spawn(options.tSharkLocation, tSharkArgs); 33 | 34 | tSharkCMD.stdout.on('data', Meteor.bindEnvironment( // required because defining callback on server 35 | parseTSharkCSV, 36 | function () { Status.set("Failed to bind environment!"); } 37 | )); 38 | 39 | tSharkCMD.stderr.on('data', Meteor.bindEnvironment( 40 | function (data) { 41 | //Status.set('tsharkCMDstderr: ' + data); 42 | }, function () { Status.set("Failed to bind environment!"); })); 43 | 44 | tSharkCMD.on('exit', Meteor.bindEnvironment( 45 | function (code) { 46 | Status.set('tsharkCMDchild process exited with code ' + code); 47 | // give time to shutdown properly 48 | Meteor.setTimeout(function() { 49 | callback(undefined, true) 50 | }, 2 * 1000) 51 | }, function () { Status.set("Failed to bind environment!"); })); 52 | } 53 | 54 | // Parse CSV data generated by tshark into individual readings 55 | var parseTSharkCSV = function(data) { 56 | // Status.set("tShark STDOUT: " + data.toString()); 57 | 58 | var config = { 59 | delimiter: "", // auto-detect 60 | newline: "", // auto-detect 61 | dynamicTyping: true, 62 | } 63 | 64 | var results = Papa.parse(data.toString(), config); 65 | var readings = results.data; 66 | var errors = results.errors; 67 | var metadata = results.metadata; 68 | 69 | if(readings) { 70 | // Status.set("Captured new batch of data"); 71 | readings.forEach(function(reading) { 72 | // Status.set("Parsing individual packet: " + reading); 73 | 74 | if(reading.length > 0) { 75 | // Status.set("Packet ACCEPTED"); 76 | var pat = /^0x.*/ 77 | var hexRegex = new RegExp(pat) 78 | // Status.set(reading) 79 | reading = _.map(reading, function(v) { 80 | // convert hex and strings to int 81 | if(hexRegex.test(v)) { 82 | return parseInt(v) 83 | } 84 | return parseFloat(v) 85 | }) 86 | 87 | var defaultVal = -9999 88 | var cid = sanitizedReadingAtt(reading, 'gsm_a.bssmap.cell_ci', defaultVal) 89 | var imsi = sanitizedReadingAtt(reading, 'gsm_a.tmsi', defaultVal) 90 | var tmsi = sanitizedReadingAtt(reading, 'gsm_a.imsi', defaultVal) 91 | /** var mobileIdentity = sanitizedReadingAtt(reading, 'gsm_a.ie.mobileid.type', defaultVal) 92 | var statusPagingChannel = sanitizedReadingAtt(reading, 'gsm_a.rr.nln_pch', defaultVal) 93 | **/ 94 | /**delete the "4" and "1" in the imsi-array **/ 95 | if (imsi < 10) { imsi = -9999 } 96 | if (tmsi < 10) { tmsi = -9999 } 97 | 98 | if((cid > 0) || (imsi > 0) || (tmsi > 0) 99 | /**|| (mobileIdentity > 0) || (statusPagingChannel > 0) **/ 100 | ) 101 | { 102 | 103 | var now = new Date() 104 | var readingId = GSMReadings.GSMReadings.insert({ 105 | scanner: scanner || "P2BTS", 106 | arfcnId: arfcn._id, 107 | frequency: arfcn.centerFreq, 108 | cid: cid, 109 | mcc: sanitizedReadingAtt(reading, 'e212.mcc', defaultVal), 110 | mnc: sanitizedReadingAtt(reading, 'e212.mnc', defaultVal), 111 | lac: sanitizedReadingAtt(reading, 'gsm_a.lac', defaultVal), 112 | signalStrength: sanitizedReadingAtt(reading, 'gsmtap.signal_dbm', defaultVal), 113 | signalNoise: sanitizedReadingAtt(reading, 'gsmtap.snr_db', defaultVal), 114 | locUpTimer: sanitizedReadingAtt(reading, 'gsm_a.rr.t3212', defaultVal), 115 | /** mobileIdentity: mobileIdentity, 116 | statusPagingChannel: statusPagingChannel, 117 | pagingChannel: sanitizedReadingAtt(reading, 'gsm_a.rr.nln_status_pch', defaultVal), 118 | **/ imsi: imsi, 119 | tmsi: tmsi, 120 | timestamp: now 121 | }); 122 | } 123 | } 124 | }) 125 | } 126 | 127 | if(errors) { 128 | // Status.set("tsharkCMD errors: " + errors); 129 | } 130 | 131 | if(metadata) { 132 | // Status.set("tsharkCMD meta: " + metadata.delimeter); 133 | } 134 | } 135 | 136 | // looks up the reading for a given attribute name or return a default 137 | var sanitizedReadingAtt = function(reading, attName, def) { 138 | var r = reading[tSharkArgIndex(attName)] 139 | return r || def 140 | } 141 | 142 | // Return attribute position in response based on order in original request 143 | var tSharkArgIndex = function(arg) { 144 | return tSharkCaptures.indexOf(arg) 145 | } 146 | 147 | // Starts Airprobe to scan for GSM data 148 | var deepScanCurrentARFCN = function(options) { 149 | Status.set("Starting Airprobe on " + arfcn.centerFreq + "MHz"); 150 | var spawn = Npm.require('child_process').spawn 151 | airprobeCMD = spawn(options.pythonLocation, [ 152 | options.airprobeLocation, 153 | '-f', arfcn.centerFreq.toString() + 'M' 154 | ]); 155 | 156 | airprobeCMD.stdout.on('data', function (data) { 157 | // Status.set("stdout: " + data); 158 | }); 159 | 160 | airprobeCMD.stderr.on('data', function (data) { 161 | // Status.set('stderr: ' + data); 162 | }); 163 | 164 | airprobeCMD.on('exit', Meteor.bindEnvironment( 165 | function (code, signal) { 166 | Status.set('deepScanCurrentARFCN child process exited with code ' + code + " and signal " + signal); 167 | if(signal == "SIGTERM") { // success 168 | Status.set("Finished scanning"); 169 | } 170 | }, function () { Status.set("deepScanCurrentARFCN Failed to bind environment!"); } 171 | )); 172 | }; 173 | 174 | var buildTSharkArgs = function(options) { 175 | var tSharkArgs = [ // capture using wireshark terminal tool 176 | '-T', 'fields', '-E', 'separator=,', // output to CSV 177 | '-l', 178 | ] 179 | 180 | // Args depending on if reading from file or from loopback 181 | if(options.fromFile) { 182 | // Status.set("buildTSharkArgs from file") 183 | tSharkArgs = tSharkArgs.concat([ 184 | '-r', options.fileLocation // Read from fileLocation 185 | ]) 186 | 187 | } else { 188 | // Status.set("buildTSharkArgs from Airprobe") 189 | tSharkArgs = tSharkArgs.concat([ 190 | '-i', 'lo', // on the loopback interface 191 | '-f', 'udp dst port 4729', // filter everything but UDP on port 4729 (gsmtap) 192 | ]) 193 | } 194 | 195 | // Add each capture to tSharkArgs as `-e arg` 196 | var captureArgs = [] 197 | _.each(tSharkCaptures, function(arg) { 198 | captureArgs.push('-e') 199 | captureArgs.push(arg) 200 | }) 201 | 202 | tSharkArgs = tSharkArgs.concat(captureArgs) 203 | 204 | return tSharkArgs 205 | } 206 | -------------------------------------------------------------------------------- /packages/gsm-scanners/scanners/test-scanner.js: -------------------------------------------------------------------------------- 1 | GSMScanners.TestScanner = { 2 | run: function(options, callback) { 3 | Meteor.setTimeout(function() { 4 | callback(undefined, true) 5 | }, 0.1 * 1000); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/status/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'marvin:status', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: '', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.2.1'); 15 | 16 | api.use([ 17 | 'ecmascript', 18 | 'mongo', 19 | 'tracker', 20 | 'underscore' 21 | ]); 22 | 23 | // Community 24 | api.use([ 25 | 'aldeed:collection2@2.5.0', 26 | 'aldeed:simple-schema@1.4.0', 27 | 'momentjs:moment@2.10.6' 28 | ]) 29 | 30 | api.use([ 31 | 'marvin:threats' 32 | ]) 33 | 34 | api.addFiles([ 35 | 'status.js', 36 | ]); 37 | 38 | api.export("Status"); 39 | }); 40 | 41 | Package.onTest(function(api) { 42 | api.use([ 43 | 'ecmascript', 44 | 'tinytest' 45 | ]); 46 | 47 | api.use([ 48 | 'momentjs:moment@2.10.6' 49 | ]) 50 | 51 | api.use([ 52 | 'marvin:status', 53 | 'marvin:threats' 54 | ]); 55 | 56 | api.addFiles('status-tests.js'); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/status/status-tests.js: -------------------------------------------------------------------------------- 1 | // Write your tests here! 2 | // Here is an example. 3 | if(Meteor.isServer){ 4 | Tinytest.add('Status - calculate threat score & level', function (test) { 5 | // Initially threats 6 | Threats.Threats.remove({}) 7 | Status.ensureDefault() 8 | Status.setStartRecordingAt(new Date()) 9 | test.equal(Status.threatScore(), 0); 10 | test.equal(Status.threatLevel(), "Green"); 11 | 12 | // Can filter out old threats 13 | Threats.Threats.insert({ 14 | timestamp: new Date(), 15 | score: 20, 16 | detector: "Test", 17 | message: "test" 18 | }) 19 | Threats.Threats.insert({ 20 | timestamp: moment().subtract(1, 'days').toDate(), 21 | score: 30, 22 | detector: "Test", 23 | message: "test" 24 | }) 25 | Threats.Threats.insert({ 26 | timestamp: new Date(), 27 | score: 20, 28 | detector: "Test", 29 | message: "test" 30 | }) 31 | test.equal(Status.threatScore(), 40); 32 | test.equal(Status.threatLevel(), "Yellow"); 33 | 34 | // Can change reference date to include more threats 35 | Status.setStartRecordingAt(moment().subtract(2, 'days').toDate()) 36 | test.equal(Status.threatScore(), 70); 37 | test.equal(Status.threatLevel(), "Red"); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /packages/status/status.js: -------------------------------------------------------------------------------- 1 | Status = { 2 | Status: CollectionName = new Mongo.Collection("StatusStatus"), 3 | ensureDefault: function() { 4 | if (Status.Status.find({}).count() === 0) { 5 | Status.Status.insert({ 6 | status: "Initializing", 7 | isStartup: true, 8 | startRecordingAt: new Date(), 9 | numPages: 0 10 | }) 11 | } 12 | }, 13 | status: function() { 14 | this.ensureDefault() 15 | return this.Status.findOne() 16 | }, 17 | get: function() { 18 | if(this.status()) 19 | return this.status().status 20 | 21 | return "Starting up..." 22 | }, 23 | isStartingUp: function() { 24 | return this.status().isStartingUp 25 | }, 26 | set: function(status) { 27 | if(this.status()) { 28 | this.Status.update(this.status()._id, {$set: {status: status}}) 29 | console.log(status); 30 | } 31 | }, 32 | setIsStartup: function(isStartup) { 33 | this.Status.update(this.status()._id, {$set: {isStartup: isStartup}}) 34 | }, 35 | setStartRecordingAt: function(startRecordingAt) { 36 | this.Status.update(this.status()._id, {$set: {startRecordingAt: startRecordingAt}}) 37 | }, 38 | threatScore: function() { 39 | var threats = Threats.Threats.find({timestamp: {$gte: this.status().startRecordingAt}}).fetch() 40 | return _.reduce(threats, function(score, threat) { 41 | return score + threat.score 42 | }, 0) 43 | }, 44 | threatLevel: function() { 45 | var score = this.threatScore() 46 | var level = _.find(Threats.LEVELS, function(_level) { 47 | return score >= _level.min && score <= _level.max 48 | }) 49 | 50 | if(level) 51 | return level.name 52 | 53 | return THREATS.UNDEFINED 54 | }, 55 | } 56 | 57 | if(Meteor.isClient){ 58 | Tracker.autorun(function() { 59 | Status.threatScore = function() { 60 | var threats = Threats.Threats.find({timestamp: {$gte: this.status().startRecordingAt}}).fetch() 61 | return _.reduce(threats, function(score, threat) { 62 | return score + threat.score 63 | }, 0) 64 | } 65 | }); 66 | 67 | Tracker.autorun(function() { 68 | Status.threatLevel = function() { 69 | var score = Status.threatScore() 70 | var level = _.find(Threats.LEVELS, function(_level) { 71 | return score >= _level.min && score <= _level.max 72 | }) 73 | 74 | return level.name 75 | } 76 | }); 77 | } 78 | 79 | Meteor.startup(function(){ 80 | Status.ensureDefault() 81 | }); 82 | 83 | Status.Status.attachSchema(new SimpleSchema({ 84 | status: { 85 | type: String, 86 | }, 87 | isStartup: { 88 | type: Boolean 89 | }, 90 | startRecordingAt: { 91 | type: Date 92 | } 93 | })) 94 | 95 | 96 | // Equivalent of autopublish and insecure plugins 97 | // TODO REMOVE FOR SECURITY 98 | 99 | if(Meteor.isServer) { 100 | Meteor.publish("status", function() { 101 | return Status.Status.find() 102 | }); 103 | } 104 | 105 | if(Meteor.isClient){ 106 | Meteor.startup(function(){ 107 | Meteor.subscribe('status') 108 | }); 109 | } 110 | 111 | Status.Status.allow({ 112 | insert: function() { 113 | return true; 114 | }, 115 | update: function(){ 116 | return true; 117 | }, 118 | remove: function(){ 119 | return true; 120 | } 121 | }); 122 | -------------------------------------------------------------------------------- /packages/threats/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'marvin:threats', 3 | version: '0.0.1', 4 | // Brief, one-line summary of the package. 5 | summary: '', 6 | // URL to the Git repository containing the source code for this package. 7 | git: '', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | api.versionsFrom('1.2.1'); 15 | api.use('ecmascript'); 16 | 17 | // Community 18 | api.use([ 19 | 'aldeed:collection2@2.5.0', 20 | 'aldeed:simple-schema@1.4.0', 21 | 'matb33:collection-hooks@0.8.1', 22 | ]) 23 | 24 | api.addFiles('threats.js'); 25 | 26 | api.export("Threats"); 27 | }); 28 | 29 | Package.onTest(function(api) { 30 | api.use('ecmascript'); 31 | api.use('tinytest'); 32 | api.use('marvin:threats'); 33 | api.addFiles('threats-tests.js'); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/threats/threats-tests.js: -------------------------------------------------------------------------------- 1 | // Write your tests here! 2 | // Here is an example. 3 | // Tinytest.add('Threats ', function (test) { 4 | // test.equal(true, true); 5 | // }); 6 | // creating a new threat should increase global threat score 7 | -------------------------------------------------------------------------------- /packages/threats/threats.js: -------------------------------------------------------------------------------- 1 | Threats = { 2 | LEVELS: [ 3 | {name: "Green", min: -99999, max: 29}, // min/max are inclusive 4 | {name: "Yellow", min: 30, max: 59}, 5 | {name: "Red", min: 60, max: 999999999} 6 | ], 7 | UNDEFINED: "ERROR CALCULATING" 8 | } 9 | 10 | Threats.Threats = new Mongo.Collection("Threats"); 11 | 12 | Threats.Threats.attachSchema(new SimpleSchema({ 13 | timestamp: { 14 | type: Date, 15 | denyUpdate: true 16 | }, 17 | gsmReadingId: { 18 | type: String, 19 | optional: true, 20 | denyUpdate: true 21 | }, 22 | arfcnId: { 23 | type: String, 24 | denyUpdate: true, 25 | optional: true 26 | }, 27 | basestationId: { 28 | type: String, 29 | denyUpdate: true, 30 | optional: true 31 | }, 32 | score: { 33 | type: Number, 34 | denyUpdate: true, 35 | }, 36 | detector: { 37 | type: String, 38 | denyUpdate: true 39 | }, 40 | message: { 41 | type: String 42 | } 43 | })) 44 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | Meteor.startup(function() { 2 | Status.set("Starting app...") 3 | prepopulateARFCNBands() 4 | }); 5 | -------------------------------------------------------------------------------- /server/fixtures.js: -------------------------------------------------------------------------------- 1 | // Prepopulate with desired frequencies 2 | // frequency will be calutated as f = mx + b 3 | if(Meteor.isServer) { 4 | Meteor.startup(function() { 5 | prepopulateARFCNBands() 6 | CountryCodes.CountryCodes.remove({}) 7 | CountryCodes.seed() 8 | }); 9 | } 10 | 11 | prepopulateARFCNBands = function() { 12 | Status.set("prepopulateARFCNBands") 13 | if (ARFCNBands.ARFCNBands.find({}).count() === 0) { 14 | Status.set("No existing bands, creating...") 15 | var bands = [ 16 | // { name: "GSM-850 Downlink", "startARFCN": 128, "endARFCN": 251, "isActive": false, "m": 0.2, "b": 843.6}, 17 | { name: "E-GSM-900 Downlink (0-124)", "startARFCN": 0, "endARFCN": 174, "isActive": true, "m": 0.2, "b": 925}, 18 | // { name: "DCS-1800 Downlink (975-1023)", "startARFCN": 512, "endARFCN": 885, "isActive": true, "m": 0.2, "b": 1805}, 19 | ] 20 | _.each(bands, function(arfcnBand) { 21 | var arfcnBandId = ARFCNBands.ARFCNBands.insert(arfcnBand); 22 | Status.set("Created band " + arfcnBand.name + " (" + arfcnBandId + ")") 23 | ARFCNBands.createARFCNs(arfcnBandId) 24 | }); 25 | } else { 26 | Status.set("Bands already exists, skipping...") 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /server/games/hide-and-seek.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | 'games/hide-and-seek/play': function(loopForever) { 3 | 4 | Meteor.call('scanners/p1-rtlsdr-scanner/run', function(error, result) { 5 | Status.set('scanners/p1-rtlsdr-scanner/run FINISHED') 6 | 7 | Meteor.call('scanners/p2-airprobe/run', function(error, result) { 8 | Status.set('scanners/p2-airprobe/run FINISHED') 9 | 10 | Meteor.call('scanners/p3-airprobe/run', function(error, result) { 11 | Status.set('scanners/p3-airprobe/run FINISHED') 12 | 13 | if(loopForever) 14 | Meteor.call('games/hide-and-seek/play', loopForever) 15 | }); 16 | }); 17 | }); 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /server/games/trick-or-treat.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | 'games/trick-or-treat/play': function(loopForever) { 3 | 4 | Meteor.call('scanners/p1-kal-rtl/run', function(error, result) { 5 | Status.set('scanners/p1-kal-rtl/run FINISHED') 6 | 7 | Meteor.call('scanners/p2-airprobe/run', function(error, result) { 8 | Status.set('scanners/p2-airprobe/run FINISHED') 9 | 10 | Meteor.call('scanners/p3-airprobe/run', function(error, result) { 11 | Status.set('scanners/p3-airprobe/run FINISHED') 12 | 13 | if(loopForever) 14 | Meteor.call("games/trick-or-treat/play", loopForever) 15 | }); 16 | }); 17 | }); 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | 3 | Meteor.startup(() => { 4 | // code to run on server at startup 5 | }); 6 | -------------------------------------------------------------------------------- /server/scanners/helpers.js: -------------------------------------------------------------------------------- 1 | doScan = function(callback) { 2 | var scan = Meteor.wrapAsync(GSMScanners.runThroughN, GSMScanners) 3 | try { 4 | res = scan(1) 5 | 6 | if(callback) 7 | callback() 8 | } catch (error) { 9 | throw new Meteor.Error("scan-error", error.message); 10 | } 11 | return res 12 | } 13 | 14 | Meteor.methods({ 15 | setDetectors: function(detectorList) { 16 | var detectors = [] 17 | _.each(detectorList, function(detector) { 18 | if(detector === Detectors.ChangingBasestation.name) { 19 | detectors.push(Detectors.ChangingBasestation) 20 | } else if (detector === Detectors.MissingChannel.name) { 21 | detectors.push(Detectors.MissingChannel) 22 | } else if (detector === Detectors.NewChannel.name) { 23 | detectors.push(Detectors.NewChannel) 24 | } else if (detector === Detectors.Paging.name) { 25 | detectors.push(Detectors.Paging) 26 | } else if (detector === Detectors.SignalStrength.name) { 27 | detectors.push(Detectors.SignalStrength) 28 | } 29 | }) 30 | 31 | Detectors.setDetectors(detectors) 32 | 33 | return true 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /server/scanners/p1KalHack.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | 'p1-kal-hackrf-scanner':function() { 3 | return doScan() 4 | }, 5 | }); 6 | -------------------------------------------------------------------------------- /server/scanners/p1KalRTL.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | 'scanners/p1-kal-rtl/run': function() { 3 | GSMScanners.reset() 4 | 5 | Detectors.setDetectors([ 6 | Detectors.MissingChannel, 7 | Detectors.NewChannel, 8 | Detectors.SignalStrength 9 | ]) 10 | 11 | Status.set("Server building scans for P1Kal...") 12 | 13 | // Get all active ARFCNBands 14 | var arfcnBands = ARFCNBands.ARFCNBands.find({isActive: true}).fetch() 15 | 16 | // Build run instructions for each band and add to runOrder 17 | GSMScanners.runOrder = _.map(arfcnBands, function(band) { 18 | return { 19 | scanner: GSMScanners.P1Kal, 20 | options: { 21 | arfcnBandId: band._id, 22 | threshold: 101, 23 | kalLocation: Meteor.settings.kalRTLLocation 24 | } 25 | } 26 | }) 27 | 28 | return doScan(function() { 29 | // Run MissingChannel detector at end of process 30 | Detectors.MissingChannel.nextIteration() 31 | }) 32 | }, 33 | }) 34 | -------------------------------------------------------------------------------- /server/scanners/p1RTLSDRScanner.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | 'scanners/p1-rtlsdr-scanner/run':function() { 3 | GSMScanners.reset() 4 | Detectors.setDetectors([ 5 | Detectors.MissingChannel, 6 | Detectors.NewChannel, 7 | Detectors.SignalStrength 8 | ]) 9 | 10 | Status.set("Server building scans for P1 RTLSDR Scanner...") 11 | 12 | // Get all active ARFCNBands 13 | var arfcnBands = ARFCNBands.ARFCNBands.find({isActive: true}).fetch() 14 | 15 | // Build run instructions for each band and add to runOrder 16 | GSMScanners.runOrder = _.map(arfcnBands, function(band) { 17 | return { 18 | scanner: GSMScanners.P1RTLSDR_SCANNER, 19 | options: { 20 | arfcnBandId: band._id, 21 | pythonLocation: Meteor.settings.pythonLocation, 22 | rtlsdrscanLocation: Meteor.settings.rtlsdrscanLocation, 23 | numSweeps: Meteor.settings.quickScanNumSweeps, 24 | dwell: Meteor.settings.quickScanDwell, 25 | fft: Meteor.settings.quickScanFFT, 26 | logLocation: Meteor.settings.quickScanLogLocation 27 | } 28 | } 29 | }) 30 | 31 | return doScan(function() { 32 | // Run MissingChannel detector at end of process 33 | Detectors.MissingChannel.nextIteration() 34 | }) 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /server/scanners/p2Airprobe.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | 'scanners/p2-airprobe/run': function() { 3 | Status.set("Building scans for Process 2...") 4 | GSMScanners.reset() 5 | Detectors.setDetectors([ 6 | Detectors.ChangingBasestation 7 | ]) 8 | 9 | var threshold = Meteor.settings.quickScanTolerance 10 | var arfcns = ARFCNs.ARFCNs.find({$and: [ 11 | {lastSignalStrength: {$gte: threshold}}, 12 | {lastSeen: {$gt: new Date(0)}} 13 | ]}).fetch() 14 | 15 | if(arfcns.length > 0) { 16 | GSMScanners.runOrder = _.map(arfcns, function(arfcn) { 17 | return { 18 | scanner: GSMScanners.P2BTS, 19 | options: { 20 | scanner: "P2BTS", 21 | fromFile: false, 22 | arfcnId: arfcn._id, 23 | duration: Meteor.settings.deepScanPeriod, 24 | pythonLocation: Meteor.settings.pythonLocation, 25 | airprobeLocation: Meteor.settings.airprobeLocation, 26 | tSharkLocation: Meteor.settings.tSharkLocation, 27 | tSharkCaptures: [ 28 | "gsm_a.bssmap.cell_ci", // CID 29 | 'e212.mcc', // MCC 30 | 'e212.mnc', // MNC 31 | 'gsm_a.lac', // LAC 32 | 'gsmtap.signal_dbm', // Signal Strength 33 | 'gsmtap.snr_db', // Signal/Noise Ratio 34 | 'gsm_a.rr.t3212' // location update timer 35 | ] 36 | } 37 | } 38 | }) 39 | 40 | return doScan() 41 | } 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /server/scanners/p3Airprobe.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | 'scanners/p3-airprobe/run': function() { 3 | GSMScanners.reset() 4 | Detectors.Paging.reset() 5 | Detectors.setDetectors([ 6 | Detectors.Paging, 7 | ]) 8 | 9 | var basestations = Basestations.Basestations.find().fetch() 10 | 11 | GSMScanners.runOrder = _.map(basestations, function(basestation) { 12 | return { 13 | scanner: GSMScanners.P2BTS, 14 | options: { 15 | scanner: "P3BTS", 16 | fromFile: false, 17 | arfcnId: basestation.arfcnId, 18 | duration: Meteor.settings.deepScanPeriod, 19 | pythonLocation: Meteor.settings.pythonLocation, 20 | airprobeLocation: Meteor.settings.airprobeLocation, 21 | tSharkLocation: Meteor.settings.tSharkLocation, 22 | tSharkCaptures: [ 23 | 'gsm_a.imsi', // IMSI 24 | 'gsm_a.tmsi', // TMSI 25 | ] 26 | } 27 | } 28 | }) 29 | 30 | return doScan() 31 | }, 32 | }); 33 | --------------------------------------------------------------------------------