├── .dockerignore ├── .editorconfig ├── .gitignore ├── .jshintignore ├── .jshintrc ├── .yo-rc.json ├── Dockerfile ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── client ├── lbclient │ ├── .gitignore │ ├── boot │ │ └── replication.js │ ├── build.js │ ├── datasources.json │ ├── datasources.local.js │ ├── lbclient.js │ ├── model-config.json │ ├── models │ │ ├── country.js │ │ ├── country.json │ │ ├── http-requests-interesting.js │ │ ├── http-requests-interesting.json │ │ ├── report.js │ │ └── report.json │ └── package.json └── ngapp │ ├── build │ └── main.js.map │ ├── config │ ├── bundle.js │ └── routes.json │ ├── data │ ├── bluecoat.json │ ├── privoxy.json │ └── squid.json │ ├── favicon.ico │ ├── favicons │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png │ ├── images │ ├── explorer-logo-200.png │ ├── explorer-logo-24.png │ ├── explorer-logo-48.png │ ├── explorer-logo-64.png │ ├── german-federal-foreign-office-logo.png │ └── ooni-loader.svg │ ├── index.html │ ├── robots.txt │ ├── scripts │ ├── app.js │ ├── controllers │ │ ├── country.js │ │ ├── explore.js │ │ ├── highlights.js │ │ ├── measurement.js │ │ ├── nettests.js │ │ ├── nettests │ │ │ └── nettests.js │ │ ├── website.js │ │ └── world.js │ ├── directives │ │ └── directives.js │ └── services │ │ ├── country.js │ │ └── lbclient.js │ ├── styles │ ├── _base.scss │ ├── _country.scss │ ├── _explore.scss │ ├── _highlights.scss │ ├── _loading.scss │ ├── _measurement.scss │ ├── _mixins.scss │ ├── _world.scss │ ├── fonts │ │ ├── charter-bold-italic.woff │ │ ├── charter-bold.woff │ │ ├── charter-italic.woff │ │ ├── charter-regular.woff │ │ ├── fira-sans-bold.otf │ │ ├── fira-sans-light.otf │ │ ├── fira-sans-semi-bold.otf │ │ ├── ooni-icons.eot │ │ ├── ooni-icons.svg │ │ ├── ooni-icons.ttf │ │ ├── ooni-icons.woff │ │ ├── source-code-pro-bold.woff │ │ └── source-code-pro-regular.woff │ ├── main.scss │ ├── ui-grid.ttf │ ├── ui-grid.woff │ └── ui │ │ ├── _buttons.scss │ │ ├── _fonts.scss │ │ ├── _forms.scss │ │ ├── _icons.scss │ │ ├── _more-info-hover.scss │ │ ├── _pagination.scss │ │ ├── _paragraph.scss │ │ ├── _section.scss │ │ ├── _table-of-contents.scss │ │ ├── _table.scss │ │ └── _ui-grid.scss │ └── views │ ├── about.html │ ├── country-view.html │ ├── directives │ ├── ooni-country-bar-chart.directive.html │ ├── ooni-explorer-list-measurement.directive.html │ ├── ooni-filter-list-form.directive.html │ ├── ooni-grid-wrapper-directive.html │ ├── ooni-info-country-list.directive.html │ ├── ooni-info-explorer-list.directive.html │ ├── ooni-loader.directive.html │ ├── ooni-more-info-hover-directive.html │ ├── ooni-pagination.directive.html │ ├── ooni-report-detail-table-row.html │ └── row-template.html │ ├── explore.html │ ├── highlights.html │ ├── nettests │ ├── bridge-reachability.html │ ├── bridget.html │ ├── captive-portal.html │ ├── dns-consistency.html │ ├── dns-injection.html │ ├── dns-spoof.html │ ├── http-header-field-manipulation.html │ ├── http-host.html │ ├── http-invalid-request-line.html │ ├── http-requests.html │ ├── lantern-circumvention-tool-test.html │ ├── meek-fronted-requests-test.html │ ├── multi-protocol-traceroute.html │ ├── nettest.html │ ├── openvpn.html │ ├── psiphon-test.html │ ├── tcp-connect.html │ └── web-connectivity.html │ ├── view-measurement.html │ ├── website-view.html │ └── world.html ├── common └── models │ ├── censorship_method.js │ ├── censorship_method.json │ ├── country.js │ ├── country.json │ ├── nettest.js │ ├── nettest.json │ ├── report.js │ └── report.json ├── deploy.sh ├── docs └── deployment.md ├── global-config.js ├── install.sh ├── ooni-api-spec.json ├── package.json ├── server.js ├── server ├── boot │ ├── angular-routes.js │ ├── authentication.js │ ├── create-default-tables.js │ ├── dev-assets.js │ ├── explorer.js │ ├── extend-models.js │ └── rest-api.js ├── config.json ├── config.local.js ├── config.production.json ├── datasources.development.js ├── datasources.json ├── datasources.production.js ├── middleware.json ├── model-config.json ├── models │ ├── country.js │ └── report.js ├── providers.json └── server.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | npm-debug.log 4 | yarn-error.log 5 | ignore 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.dat 3 | *.iml 4 | *.log 5 | *.out 6 | *.pid 7 | *.seed 8 | *.sublime-* 9 | *.swo 10 | *.swp 11 | *.tgz 12 | *.xml 13 | .DS_Store 14 | .idea 15 | .project 16 | .strong-pm 17 | coverage 18 | node_modules 19 | npm-debug.log 20 | bower_components 21 | client/dist 22 | .tmp 23 | server/providers-private.json 24 | client/ngapp/styles/main.css 25 | client/ngapp/styles/main.css.map 26 | client/ngapp/build/main.js 27 | client/ngapp/data/factbook 28 | ignore/ 29 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | /client/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": false, 6 | "eqeqeq": true, 7 | "eqnull": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": "nofunc", 11 | "newcap": true, 12 | "nonew": true, 13 | "noarg": true, 14 | "quotmark": false, 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "trailing": true, 19 | "sub": true, 20 | "maxlen": false, 21 | "asi": true, 22 | "predef": [ 23 | "angular", 24 | "colorbrewer" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-loopback": {} 3 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Best practices for development, and not for a production deployment 2 | # from https://nodejs.org/en/docs/guides/nodejs-docker-webapp/ 3 | 4 | # Build: run ooni-sysadmin.git/scripts/docker-build from this directory 5 | 6 | FROM node:carbon 7 | 8 | # BEGIN root 9 | USER root 10 | COPY . /usr/src/app 11 | RUN set -ex \ 12 | && yarn add global grunt-cli \ 13 | && yarn global add loopback-sdk-angular-cli \ 14 | && chown -R node:node /usr/src/app \ 15 | && : 16 | # END root 17 | 18 | USER node 19 | WORKDIR /usr/src/app 20 | 21 | # .cache removal leads to two times smaller image and 22 | RUN set -ex \ 23 | && yarn install --frozen-lockfile \ 24 | && npm run build \ 25 | && rm -rf /home/node/.cache \ 26 | && : 27 | 28 | EXPOSE 3000 29 | 30 | USER daemon 31 | CMD [ "npm", "start" ] 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Open Observatory of Network Interference (OONI), The Tor Project 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OONI Explorer Legacy [DEPRECATED] 2 | 3 | For the latest and greatest OONI Explorer code, see: https://github.com/ooni/explorer 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ooni-explorer", 3 | "version": "0.0.0", 4 | "license": "BSD-2-Clause", 5 | "private": true, 6 | "appPath": "client/ngapp", 7 | "testPath": "client/ngapp/test/spec", 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "**/bower_components", 12 | "test", 13 | "tests" 14 | ], 15 | "dependencies": { 16 | "angular": "~1.4", 17 | "json3": "~3.3.1", 18 | "es5-shim": "~3.1.0", 19 | "angular-route": "~1.4", 20 | "angular-resource": "~1.4", 21 | "topojson": "topojson#1.6.20", 22 | "angular-datamaps": "~0.1.0", 23 | "colorbrewer": "~1.0.0", 24 | "angular-typewrite": "~0.0.14", 25 | "flag-icon-css": "", 26 | "angular-ui-grid": "~3.0.0-rc.21", 27 | "font-awesome": "fontawesome#~4.5.0", 28 | "iso-3166-country-codes-angular": "https://github.com/hellais/iso-3166-country-codes-angular/archive/611d0cf4dc2244a74cd337c516744d1dce235e7c.zip", 29 | "json-formatter": "~0.4.2", 30 | "factbook-country-data": "https://github.com/simonv3/factbook-country-data/archive/0cceccd6e393e6a860798e0b7e5be28c4f18efd4.zip", 31 | "angular-daterangepicker": "~0.2.2", 32 | "angular-ui-codemirror": "~0.3.0", 33 | "angular-inview": "~1.5.6" 34 | }, 35 | "devDependencies": { 36 | "angular-mocks": "~1.4", 37 | "angular-scenario": "~1.4" 38 | }, 39 | "resolutions": { 40 | "angular": "1.4" 41 | }, 42 | "overrides": { 43 | "font-awesome": { 44 | "main": [ 45 | "css/font-awesome.css" 46 | ] 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /client/lbclient/.gitignore: -------------------------------------------------------------------------------- 1 | browser.bundle.js 2 | -------------------------------------------------------------------------------- /client/lbclient/boot/replication.js: -------------------------------------------------------------------------------- 1 | // TODO figure out what this is supposed to be 2 | -------------------------------------------------------------------------------- /client/lbclient/build.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var pkg = require('./package.json'); 3 | var fs = require('fs'); 4 | var browserify = require('browserify'); 5 | var boot = require('loopback-boot'); 6 | 7 | module.exports = function buildBrowserBundle(env, callback) { 8 | var b = browserify({ basedir: __dirname }); 9 | b.require('./' + pkg.main, { expose: 'lbclient' }); 10 | 11 | try { 12 | boot.compileToBrowserify({ 13 | appRootDir: __dirname, 14 | env: env 15 | }, b); 16 | } catch(err) { 17 | return callback(err); 18 | } 19 | 20 | var bundlePath = path.resolve(__dirname, 'browser.bundle.js'); 21 | var out = fs.createWriteStream(bundlePath); 22 | var isDevEnv = ~['debug', 'development', 'test'].indexOf(env); 23 | 24 | b.bundle({ 25 | // TODO(bajtos) debug should be always true, the sourcemaps should be 26 | // saved to a standalone file when !isDev(env) 27 | debug: isDevEnv 28 | }) 29 | .on('error', callback) 30 | .pipe(out); 31 | 32 | out.on('error', callback); 33 | out.on('close', callback); 34 | }; 35 | -------------------------------------------------------------------------------- /client/lbclient/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "remote": { 3 | "connector": "remote" 4 | }, 5 | "local": { 6 | "connector": "memory" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/lbclient/datasources.local.js: -------------------------------------------------------------------------------- 1 | var GLOBAL_CONFIG = require('../../global-config'); 2 | 3 | module.exports = { 4 | remote: { 5 | url: GLOBAL_CONFIG.restApiUrl 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /client/lbclient/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": ["../../common/models", "./models"] 4 | }, 5 | "RemoteReport": { 6 | "dataSource": "remote" 7 | }, 8 | "RemoteHttpRequestsInteresting": { 9 | "dataSource": "remote" 10 | }, 11 | "RemoteCountry": { 12 | "dataSource": "remote" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/lbclient/models/country.js: -------------------------------------------------------------------------------- 1 | module.exports = function(Country) { 2 |   var app = require('lbclient');  3 | 4 | Country.prototype.getCountryInfo = function(country_code, callback) { 5 | app.models().country.getCountryInfo(country_code, callback); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/lbclient/models/country.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RemoteCountry", 3 | "base": "country", 4 | "plural": "countries", 5 | "trackChanges": false, 6 | "enableRemoteReplication": true 7 | } 8 | -------------------------------------------------------------------------------- /client/lbclient/models/http-requests-interesting.js: -------------------------------------------------------------------------------- 1 | module.exports = function(HttpRequestsInteresting) { 2 | HttpRequestsInteresting.findInteresting = function(country_code, fields, limit, callback) { 3 | HttpRequestsInteresting.app.models.RemoteHttpRequestsInteresting.findInteresting(country_code, fields, limit, callback) 4 | } 5 | 6 | HttpRequestsInteresting.listInteresting = function(key, callback) { 7 | HttpRequestsInteresting.app.models.RemoteHttpRequestsInteresting.find(key, callback); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /client/lbclient/models/http-requests-interesting.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RemoteHttpRequestsInteresting", 3 | "base": "httpRequestsInteresting", 4 | "plural": "httpRequestsInterestings", 5 | "trackChanges": false, 6 | "enableRemoteReplication": true 7 | } 8 | -------------------------------------------------------------------------------- /client/lbclient/models/report.js: -------------------------------------------------------------------------------- 1 | module.exports = function(Report) { 2 | } 3 | -------------------------------------------------------------------------------- /client/lbclient/models/report.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RemoteReport", 3 | "base": "report", 4 | "plural": "reports", 5 | "trackChanges": false, 6 | "enableRemoteReplication": true 7 | } 8 | -------------------------------------------------------------------------------- /client/lbclient/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "main": "lbclient.js" 4 | } 5 | -------------------------------------------------------------------------------- /client/ngapp/config/bundle.js: -------------------------------------------------------------------------------- 1 | window.CONFIG = { 2 | "routes": { 3 | "/highlights/": { 4 | "controller": "HighlightsCtrl", 5 | "templateUrl": "/views/highlights.html" 6 | }, 7 | "/about/": { 8 | "controller": "HighlightsCtrl", 9 | "templateUrl": "/views/about.html" 10 | }, 11 | "/world/": { 12 | "controller": "WorldCtrl", 13 | "templateUrl": "/views/world.html" 14 | }, 15 | "/explore/": { 16 | "controller": "ExploreViewCtrl", 17 | "templateUrl": "/views/explore.html" 18 | }, 19 | "/measurement/:id": { 20 | "controller": "MeasurementDetailViewCtrl", 21 | "templateUrl": "/views/view-measurement.html" 22 | }, 23 | "/country/:id": { 24 | "controller": "CountryDetailViewCtrl", 25 | "templateUrl": "/views/country-view.html" 26 | }, 27 | "/website/:id*": { 28 | "controller": "WebsiteDetailViewCtrl", 29 | "templateUrl": "/views/website-view.html" 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /client/ngapp/config/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/highlights/": { 3 | "controller": "HighlightsCtrl", 4 | "templateUrl": "/views/highlights.html" 5 | }, 6 | "/about/": { 7 | "controller": "HighlightsCtrl", 8 | "templateUrl": "/views/about.html" 9 | }, 10 | "/world/": { 11 | "controller": "WorldCtrl", 12 | "templateUrl": "/views/world.html" 13 | }, 14 | "/explore/": { 15 | "controller": "ExploreViewCtrl", 16 | "templateUrl": "/views/explore.html" 17 | }, 18 | "/measurement/:id": { 19 | "controller": "MeasurementDetailViewCtrl", 20 | "templateUrl": "/views/view-measurement.html" 21 | }, 22 | "/country/:id": { 23 | "controller": "CountryDetailViewCtrl", 24 | "templateUrl": "/views/country-view.html" 25 | }, 26 | "/website/:id*": { 27 | "controller": "WebsiteDetailViewCtrl", 28 | "templateUrl": "/views/website-view.html" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/ngapp/data/bluecoat.json: -------------------------------------------------------------------------------- 1 | { 2 | "vendor":"Bluecoat" 3 | } 4 | -------------------------------------------------------------------------------- /client/ngapp/data/privoxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "vendor":"Privoxy" 3 | } 4 | -------------------------------------------------------------------------------- /client/ngapp/data/squid.json: -------------------------------------------------------------------------------- 1 | { 2 | "vendor":"Squid" 3 | } 4 | -------------------------------------------------------------------------------- /client/ngapp/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicon.ico -------------------------------------------------------------------------------- /client/ngapp/favicons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/android-icon-144x144.png -------------------------------------------------------------------------------- /client/ngapp/favicons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/android-icon-192x192.png -------------------------------------------------------------------------------- /client/ngapp/favicons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/android-icon-36x36.png -------------------------------------------------------------------------------- /client/ngapp/favicons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/android-icon-48x48.png -------------------------------------------------------------------------------- /client/ngapp/favicons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/android-icon-72x72.png -------------------------------------------------------------------------------- /client/ngapp/favicons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/android-icon-96x96.png -------------------------------------------------------------------------------- /client/ngapp/favicons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/apple-icon-114x114.png -------------------------------------------------------------------------------- /client/ngapp/favicons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/apple-icon-120x120.png -------------------------------------------------------------------------------- /client/ngapp/favicons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/apple-icon-144x144.png -------------------------------------------------------------------------------- /client/ngapp/favicons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/apple-icon-152x152.png -------------------------------------------------------------------------------- /client/ngapp/favicons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/apple-icon-180x180.png -------------------------------------------------------------------------------- /client/ngapp/favicons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/apple-icon-57x57.png -------------------------------------------------------------------------------- /client/ngapp/favicons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/apple-icon-60x60.png -------------------------------------------------------------------------------- /client/ngapp/favicons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/apple-icon-72x72.png -------------------------------------------------------------------------------- /client/ngapp/favicons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/apple-icon-76x76.png -------------------------------------------------------------------------------- /client/ngapp/favicons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /client/ngapp/favicons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/apple-icon.png -------------------------------------------------------------------------------- /client/ngapp/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /client/ngapp/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /client/ngapp/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /client/ngapp/favicons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/ms-icon-144x144.png -------------------------------------------------------------------------------- /client/ngapp/favicons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/ms-icon-150x150.png -------------------------------------------------------------------------------- /client/ngapp/favicons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/ms-icon-310x310.png -------------------------------------------------------------------------------- /client/ngapp/favicons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/favicons/ms-icon-70x70.png -------------------------------------------------------------------------------- /client/ngapp/images/explorer-logo-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/images/explorer-logo-200.png -------------------------------------------------------------------------------- /client/ngapp/images/explorer-logo-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/images/explorer-logo-24.png -------------------------------------------------------------------------------- /client/ngapp/images/explorer-logo-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/images/explorer-logo-48.png -------------------------------------------------------------------------------- /client/ngapp/images/explorer-logo-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/images/explorer-logo-64.png -------------------------------------------------------------------------------- /client/ngapp/images/german-federal-foreign-office-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/images/german-federal-foreign-office-logo.png -------------------------------------------------------------------------------- /client/ngapp/images/ooni-loader.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client/ngapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OONI Explorer 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 47 | 48 | 49 | 50 | 51 | 54 |
55 |
56 |
57 | 58 | 59 |

OONI Explorer

60 |
61 |
62 | World 63 | Explorer 64 | Highlights 65 | About 66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | 74 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /client/ngapp/robots.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/robots.txt -------------------------------------------------------------------------------- /client/ngapp/scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc overview 5 | * @name ooniAPIApp 6 | * @description 7 | * 8 | * Main module of the application. 9 | */ 10 | angular 11 | .module('ooniAPIApp', [ 12 | 'ngRoute', 13 | 'lbServices', 14 | 'ngResource', 15 | 'datamaps', 16 | 'angularTypewrite', 17 | 'ui.grid', 18 | 'ui.grid.pagination', 19 | 'ui.codemirror', 20 | 'iso-3166-country-codes', 21 | 'jsonFormatter', 22 | 'daterangepicker', 23 | 'angular-inview' 24 | ]) 25 | .config(function ($routeProvider, $locationProvider) { 26 | Object.keys(window.CONFIG.routes) 27 | .forEach(function(route) { 28 | var routeDef = window.CONFIG.routes[route]; 29 | $routeProvider.when(route, routeDef); 30 | }); 31 | 32 | $routeProvider 33 | .otherwise({ 34 | redirectTo: '/world' 35 | }); 36 | 37 | $locationProvider.html5Mode(true); 38 | }) 39 | // Things to run before the app loads; 40 | .run(function ($rootScope, $location, $anchorScroll) { 41 | $rootScope.$location = $location; 42 | }); 43 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/country.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name ooniAPIApp.controller:CountryViewCtrl 6 | * @description 7 | * # CountryViewCtrl 8 | * Controller of the ooniAPIApp 9 | */ 10 | 11 | angular.module('ooniAPIApp') 12 | .controller('CountryDetailViewCtrl', function ($q, $scope, $rootScope, $filter, Report, $http, $routeParams, ISO3166, Country) { 13 | $scope.loaded = false; 14 | 15 | $scope.countryCode = $routeParams.id; 16 | $scope.countryName = ISO3166.getCountryName($scope.countryCode); 17 | $scope.encodeInput = window.encodeURIComponent; 18 | 19 | $http.get('data/factbook/' + $scope.countryCode.toLowerCase() + '.json') 20 | .then(function(response) { 21 | $scope.countryDetails = response.data; 22 | }, function(error) { 23 | console.log('error', error) 24 | }) 25 | 26 | Country.findOne({ 27 | filter: { 28 | where: {iso_alpha2: $scope.countryCode}, 29 | include: ['censorship_methods'] 30 | } 31 | }, function(response) { 32 | $scope.censorshipMethods = response.censorship_methods; 33 | }); 34 | 35 | Report.blockpageCount({probe_cc: $scope.countryCode}, function(resp) { 36 | // this goes off and gets processed by the bar-chart directive 37 | $scope.blockpageCount = resp; 38 | }); 39 | 40 | Report.blockpageList({probe_cc: $scope.countryCode}, function(resp) { 41 | $scope.blockpageList = resp; 42 | 43 | $scope.chunkedBlockpageList = {} 44 | 45 | resp.forEach(function(page) { 46 | if ($scope.chunkedBlockpageList[page.input] === undefined) { 47 | $scope.chunkedBlockpageList[page.input] = { 48 | measurements: [page] 49 | } 50 | } else { 51 | $scope.chunkedBlockpageList[page.input].measurements.push(page) 52 | } 53 | }); 54 | 55 | $scope.chunkedArray = []; 56 | 57 | angular.forEach($scope.chunkedBlockpageList, function(val, key) { 58 | val.input = key; 59 | $scope.chunkedArray.push(val) 60 | }) 61 | 62 | $scope.loadedChunks = $scope.chunkedArray.slice(0, 10) 63 | }); 64 | 65 | var loadingMore = false; 66 | var chunkLength = 50; 67 | 68 | $scope.loadMoreChunks = function() { 69 | if ($scope.chunkedArray && !loadingMore) { 70 | loadingMore = true; 71 | var len = $scope.loadedChunks.length; 72 | var next = $scope.chunkedArray.slice(len, len + chunkLength) 73 | $scope.loadedChunks = $scope.loadedChunks.concat(next) 74 | if (next.length < chunkLength) { 75 | $scope.chunkEndReached = true; 76 | } 77 | } 78 | loadingMore = false; 79 | } 80 | 81 | Report.vendors( {probe_cc: $scope.countryCode}, function(resp) { 82 | $scope.vendors = resp; 83 | $scope.vendors.forEach(function(vendor) { 84 | 85 | var url = 'data/' + vendor.vendor + '.json' 86 | $http.get(url) 87 | .then(function(resp) { 88 | vendor.data = resp.data; 89 | }, function(err, resp) { 90 | console.log('err', resp) 91 | }) 92 | }) 93 | }); 94 | 95 | Report.countByCountry(function (result) { 96 | var thisCountry = result.filter(function(item) { return item.alpha2 === $scope.countryCode }) 97 | $scope.count = (thisCountry[0] && thisCountry[0].count) || -1 98 | }) 99 | 100 | // XXX should use external pagination feature of ui grid 101 | // http://ui-grid.info/docs/#/tutorial/314_external_pagination 102 | 103 | $scope.loadMeasurements = function(queryOptions) { 104 | var deferred = $q.defer(); 105 | 106 | queryOptions.where['probe_cc'] = $scope.countryCode; 107 | var query = { 108 | filter: { 109 | fields: { 110 | 'test_name': true, 111 | 'input': true, 112 | 'probe_cc': true, 113 | 'test_start_time': true, 114 | 'id': true, 115 | 'probe_asn': true 116 | }, 117 | where: queryOptions.where, 118 | offset: queryOptions.pageNumber * queryOptions.pageSize, 119 | limit: queryOptions.pageSize 120 | } 121 | } 122 | var params = {} 123 | 124 | if (queryOptions.where) { 125 | params.probe_cc = queryOptions.where.probe_cc 126 | params.input = queryOptions.where.input 127 | params.test_name = queryOptions.where.test_name 128 | params.since = queryOptions.where.test_start_time && queryOptions.where.test_start_time.between[0] 129 | params.until = queryOptions.where.test_start_time && queryOptions.where.test_start_time.between[1] 130 | } 131 | params.order = queryOptions.order 132 | params.page_size = queryOptions.pageSize 133 | params.page_number = queryOptions.pageNumber 134 | 135 | Report.findMeasurements(params, function(data) { 136 | deferred.resolve(data); 137 | 138 | $scope.loaded = true; 139 | }); 140 | 141 | return deferred.promise; 142 | } 143 | 144 | }) 145 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/explore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name ooniAPIApp.controller:ExploreViewCtrl 6 | * @description 7 | * # ExploreViewCtrl 8 | * Controller of the ooniAPIApp 9 | */ 10 | 11 | angular.module('ooniAPIApp') 12 | .controller('ExploreViewCtrl', function ($q, $scope, $anchorScroll, 13 | $location, Nettest, Report, 14 | $routeParams, uiGridConstants, 15 | $rootScope) { 16 | 17 | $scope.loadMeasurements = function(queryOptions) { 18 | 19 | $scope.loaded = false; 20 | 21 | var deferred = $q.defer(); 22 | var query = { 23 | filter: { 24 | fields: { 25 | 'test_name': true, 26 | 'probe_cc': true, 27 | 'probe_asn': true, 28 | 'input': true, 29 | 'test_start_time': true, 30 | 'id': true 31 | }, 32 | where: queryOptions.where, 33 | order: queryOptions.order, 34 | offset: queryOptions.pageNumber * queryOptions.pageSize, 35 | limit: queryOptions.pageSize 36 | } 37 | } 38 | var params = {} 39 | 40 | if (queryOptions.where) { 41 | params.probe_cc = queryOptions.where.probe_cc 42 | params.input = queryOptions.where.input 43 | params.test_name = queryOptions.where.test_name 44 | params.since = queryOptions.where.test_start_time && queryOptions.where.test_start_time.between[0] 45 | params.until = queryOptions.where.test_start_time && queryOptions.where.test_start_time.between[1] 46 | } 47 | params.order = queryOptions.order 48 | params.page_size = queryOptions.pageSize 49 | params.page_number = queryOptions.pageNumber 50 | 51 | Report.total(function (result) { 52 | Report.findMeasurements(params, function(data) { 53 | data.total = result.total; 54 | deferred.resolve(data); 55 | 56 | $scope.loaded = true; 57 | }); 58 | }) 59 | 60 | return deferred.promise; 61 | } 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/highlights.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name ooniAPIApp.controller:OverviewCtrl 6 | * @description 7 | * # OverviewCtrl 8 | * Controller of the ooniAPIApp 9 | */ 10 | 11 | angular.module('ooniAPIApp') 12 | .controller('HighlightsCtrl', function ($rootScope, $location) { 13 | $rootScope.loaded = true; 14 | }); 15 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/measurement.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name ooniAPIApp.controller:MeasurementDetailViewCtrl 6 | * @description 7 | * # ReportDetailViewCtrl 8 | * Controller of the ooniAPIApp 9 | */ 10 | 11 | angular.module('ooniAPIApp') 12 | .controller('MeasurementDetailViewCtrl', function ($q, $scope, $anchorScroll, $rootScope, $location, $http, Report, Nettest, Country, $routeParams, ISO3166) { 13 | 14 | $scope.measurementId = $routeParams.id; 15 | $scope.measurementInput = $routeParams.input; 16 | 17 | $rootScope.loaded = false; 18 | // XXX should use external pagination feature of ui grid 19 | // http://ui-grid.info/docs/#/tutorial/314_external_pagination 20 | $scope.pageNumber = 0; 21 | $scope.pageSize = 100; 22 | $scope.definitions = definitions; 23 | 24 | function loading_success(data) { 25 | $scope.report = data; 26 | $scope.network_information = $scope.report.probe_asn 27 | Report.asnName({asn: $scope.report.probe_asn}, function(result) { 28 | if (result[0] != undefined) { 29 | $scope.network_information = result[0].name + " ( " + $scope.network_information + " )"; 30 | } 31 | }); 32 | 33 | $scope.nettest = Nettest.findOne({ 34 | filter: { 35 | where: { 36 | name: $scope.report.test_name 37 | } 38 | } 39 | }); 40 | 41 | $scope.countryName = ISO3166.getCountryName($scope.report.probe_cc); 42 | 43 | $rootScope.loaded = true; 44 | } 45 | 46 | function loading_failure() { 47 | console.log('failed'); 48 | $rootScope.loaded = true; 49 | $scope.not_found = true; 50 | } 51 | 52 | function getMeasurementJson(reportId, input, cb, eb) { 53 | var query = { 54 | report_id: reportId, 55 | } 56 | if (input !== undefined) { 57 | query['input'] = input 58 | } 59 | return Report.findMeasurements(query, function(result) { 60 | $http.get(result[0].measurement_url) 61 | .then(function(response) { 62 | cb(response.data) 63 | }, eb) 64 | }, eb) 65 | } 66 | 67 | $scope.measurement = getMeasurementJson($scope.measurementId, $scope.measurementInput, loading_success, loading_failure) 68 | 69 | }); 70 | 71 | 72 | var definitions = { 73 | options: { 74 | description: "A dictionary containing the keys and values of options passed to the test", 75 | }, 76 | probe_asn: { 77 | description: "The AS Number of the probe (prefixed by AS, ex. AS1234) or null if includeasn is set to false.", 78 | }, 79 | probe_cc: { 80 | description: "The two letter country code of the probe or null if inlcudecc is set to false.", 81 | }, 82 | as_number: { 83 | external_url: "https://en.wikipedia.org/wiki/Autonomous_system_%28Internet%29" 84 | }, 85 | probe_ip: { 86 | description: "The IPv4 address of the probe or null if includeip is set to false.", 87 | }, 88 | software_name: { 89 | description: "The name of the software that has generated such report (ex. ooniprobe).", 90 | }, 91 | software_version: { 92 | description: "The version of the software that has generated such report (ex. 0.0.10).", 93 | }, 94 | start_time: { 95 | description: "The time at which the test was started in seconds since epoch.", 96 | }, 97 | test_name: { 98 | description: "The name of the test that such report is for (ex. HTTP Requests).", 99 | }, 100 | test_version: { 101 | description: "The version of the test that such report is for (ex. 0.0.10).", 102 | }, 103 | data_format: { 104 | description: "The version string of the data format being used by the test (ex. httpt-000)", 105 | }, 106 | report_id: { 107 | description: "A 64 character mixed case string that is generated by the client used to identify the report.", 108 | }, 109 | test_helpers: { 110 | description: "A dictionary with as keys the names of the options and values the addresses of the test helpers used", 111 | }, 112 | test_input: { 113 | description: "The specific input for this test" 114 | } 115 | } 116 | 117 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/nettests.js: -------------------------------------------------------------------------------- 1 | angular.module('ooniAPIApp') 2 | .controller('HTTPRequestsViewCtrl', function ($scope, $location){ 3 | 4 | $scope.encodeInput = window.encodeURIComponent; 5 | 6 | angular.forEach($scope.report.test_keys.requests, function(request) { 7 | if (request.request.tor === true || request.request.tor.is_tor === true) { 8 | $scope.control = request; 9 | } else { 10 | $scope.experiment = request; 11 | } 12 | }); 13 | 14 | $scope.experiment_body = null; 15 | if ($scope.experiment && $scope.experiment.response && $scope.experiment.response.body) { 16 | $scope.experiment_body = $scope.experiment.response.body; 17 | } 18 | $scope.body_length_match = 'unknown'; 19 | if ($scope.report.test_keys.body_length_match == true) { 20 | $scope.body_length_match = 'true'; 21 | } else if ($scope.report.test_keys.body_length_match == false) { 22 | $scope.body_length_match = 'false'; 23 | } 24 | 25 | if (typeof $scope.experiment === 'undefined') { 26 | $scope.experiment_failure = 'unknown'; 27 | } else { 28 | $scope.experiment_failure = $scope.experiment.failure || 'none'; 29 | } 30 | 31 | if (typeof $scope.control === 'undefined') { 32 | $scope.control_failure = 'unknown'; 33 | } else { 34 | $scope.control_failure = $scope.control.failure || 'none'; 35 | } 36 | 37 | $scope.anomaly = false; 38 | if ($scope.body_length_match === 'false') { 39 | $scope.anomaly = true; 40 | } 41 | if ($scope.experiment_failure !== 'none' && ($scope.control_failure === 'none' || $scope.control_failure === 'unknown')) { 42 | $scope.anomaly = true; 43 | } 44 | if ($scope.report.test_keys.headers_match == false) { 45 | $scope.anomaly = true; 46 | } 47 | 48 | $scope.header_names = []; 49 | if ($scope.control && $scope.control.response) { 50 | for (var header_name in $scope.control.response.headers) { 51 | if ($scope.header_names.indexOf(header_name) == -1) { 52 | $scope.header_names.push(header_name); 53 | } 54 | } 55 | } 56 | 57 | if ($scope.experiment && $scope.experiment.response) { 58 | for (var header_name in $scope.experiment.response.headers) { 59 | if ($scope.header_names.indexOf(header_name) == -1) { 60 | $scope.header_names.push(header_name); 61 | } 62 | } 63 | } 64 | 65 | $scope.code_mirror_options = { 66 | lineWrapping : true, 67 | mode: 'xml' 68 | } 69 | }) 70 | .controller('DNSConsistencyViewCtrl', function ($scope, $location){ 71 | }) 72 | .controller('WebConnectivityViewCtrl', function ($scope, $location){ 73 | $scope.encodeInput = window.encodeURIComponent; 74 | 75 | $scope.code_mirror_options = { 76 | lineWrapping : true, 77 | mode: 'xml' 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/nettests/nettests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // The idea behind this file is to keep a place for all specific nettest 4 | // controllers in one place, as long as they don't have any specific 5 | // functionality 6 | 7 | angular.module('ooniAPIApp') 8 | .directive('ooniNettestDetails', function ($location) { 9 | return { 10 | restrict: 'A', 11 | scope: { 12 | report: '=' 13 | }, 14 | link: function($scope) { 15 | 16 | // Not sure if this is the best way to go about doing this. 17 | // It runs at all times. 18 | $scope.getContentUrl = function() { 19 | var nettestSlug = 'nettest'; 20 | if ($scope.report !== undefined) { 21 | nettestSlug = $scope.report.test_name.replace(/_/g, '-'); 22 | } 23 | var url = '/views/nettests/' + nettestSlug + '.html'; 24 | return url 25 | } 26 | }, 27 | template: '
fasdfdas
' 28 | } 29 | }) 30 | .directive('ooniNettestSummary', function ($location) { 31 | return { 32 | restrict: 'A', 33 | scope: { 34 | report: '=' 35 | }, 36 | link: function($scope) { 37 | 38 | // Not sure if this is the best way to go about doing this. 39 | // It runs at all times. 40 | $scope.getContentUrl = function() { 41 | var nettestSlug = 'nettest'; 42 | if ($scope.report !== undefined) { 43 | nettestSlug = $scope.report.test_name.replace('_', '-'); 44 | } 45 | var url = '/views/nettests/' + nettestSlug + '-summary.html'; 46 | return url; 47 | } 48 | }, 49 | template: 'Build A Summary Template Directive', 50 | // template: '
' 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/website.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * @ngdoc function 5 | * @name ooniAPIApp.controller:WebsiteDetailViewCtrl 6 | * @description 7 | * # WebsiteDetailViewCtrl 8 | * Controller of the ooniAPIApp 9 | */ 10 | 11 | angular.module('ooniAPIApp') 12 | .controller('WebsiteDetailViewCtrl', function ($scope, Report, $http, $routeParams, ISO3166) { 13 | $scope.websiteUrl = $routeParams.id 14 | $scope.encodeInput = window.encodeURIComponent; 15 | 16 | Report.websiteMeasurements({website_url: $scope.websiteUrl}, function (resp) { 17 | $scope.measurementsByCountry = {} 18 | resp.forEach(function (measurement) { 19 | if ($scope.measurementsByCountry[measurement.probe_cc] !== undefined) { 20 | $scope.measurementsByCountry[measurement.probe_cc].measurements 21 | .push(measurement) 22 | } else { 23 | $scope.measurementsByCountry[measurement.probe_cc] = { 24 | measurements: [measurement], 25 | country: ISO3166.getCountryName(measurement.probe_cc) 26 | } 27 | } 28 | }) 29 | }, function (err) { 30 | if (err) console.log('err', err) 31 | }) 32 | 33 | Report.websiteDetails({website_url: $scope.websiteUrl}, function (resp) { 34 | $scope.details = resp[0] 35 | console.log($scope.details) 36 | }, function (err) { 37 | if (err) console.log('err', err) 38 | }) 39 | 40 | // var alexaUrl = 'http://data.alexa.com/data?cli=10&data=snbamz&url=' + $scope.websiteUrl 41 | }) 42 | -------------------------------------------------------------------------------- /client/ngapp/scripts/services/country.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/scripts/services/country.js -------------------------------------------------------------------------------- /client/ngapp/styles/_country.scss: -------------------------------------------------------------------------------- 1 | /* Country */ 2 | 3 | .country { 4 | h1 { 5 | margin-bottom: 1rem; 6 | } 7 | } 8 | 9 | .hang-right { 10 | display: inline-block; 11 | } 12 | 13 | 14 | .map { 15 | margin-top: -80px; 16 | } 17 | 18 | .explanation { 19 | margin: 0rem auto 2rem; 20 | } 21 | 22 | .statistics { 23 | margin-top: 1rem; 24 | } 25 | 26 | .graph-description { 27 | text-align: center; 28 | font-size: .8rem; 29 | margin-top: .5rem; 30 | 31 | i { 32 | display: inline-block; 33 | width: 1em; 34 | height: 1em; 35 | } 36 | 37 | .measured { 38 | background: $color-ooni-blue; 39 | } 40 | .blocked { 41 | background: $color-blocked; 42 | } 43 | } 44 | 45 | .bar-chart-wrapper { 46 | text-align: center; 47 | 48 | a:hover { 49 | color: darken($color-ooni-blue, 10%); 50 | cursor: pointer; 51 | } 52 | } 53 | 54 | .bar-chart { 55 | 56 | max-height: 100%; 57 | max-width: 100%; 58 | width: 100%; 59 | height: 100%; 60 | 61 | 62 | .bar { 63 | cursor: pointer; 64 | display: inline-block; 65 | fill: $color-ooni-blue; 66 | margin-right: 2rem; 67 | 68 | &.blocked { 69 | fill: $color-blocked; 70 | } 71 | } 72 | 73 | text { 74 | fill: $color-grey; 75 | opacity: 1; 76 | font-size: 16px; 77 | font-family: "Fira Sans", sans-serif; 78 | 79 | &.date { 80 | text-anchor: start; 81 | font-size: 14px; 82 | } 83 | &.total { 84 | fill: $color-ooni-blue; 85 | text-anchor: middle; 86 | } 87 | &.blocked { 88 | fill: $color-blocked; 89 | text-anchor: start; 90 | } 91 | } 92 | 93 | .hidden { 94 | opacity: 0; 95 | } 96 | } 97 | 98 | .country section { 99 | margin-top: 2em; 100 | 101 | h2 { 102 | margin-bottom: 1em; 103 | } 104 | 105 | .no-show { 106 | background-color: $color-off-white; 107 | padding: 2rem; 108 | text-align: center; 109 | color: $color-grey; 110 | } 111 | 112 | &.websites-blocked { 113 | ul { 114 | font-size: .8rem; 115 | list-style: none; 116 | } 117 | 118 | li { 119 | display: inline-block; 120 | vertical-align: top; 121 | width: 32%; 122 | margin-left: 0; 123 | padding-left: 0; 124 | } 125 | } 126 | 127 | &.vendors { 128 | ul { 129 | list-style: none; 130 | 131 | li { 132 | display: inline-block; 133 | width: 45%; 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /client/ngapp/styles/_explore.scss: -------------------------------------------------------------------------------- 1 | /* Explore */ 2 | 3 | .explore { 4 | .explorer-list { 5 | list-style: none; 6 | margin: 0; 7 | 8 | li { 9 | margin: 0; 10 | padding: .5rem; 11 | word-wrap: break-word; 12 | 13 | &:nth-child(odd) { 14 | background-color: $color-off-white; 15 | } 16 | } 17 | } 18 | 19 | .column { 20 | display: inline-block; 21 | } 22 | 23 | .probe-cc { 24 | width: 9%; 25 | } 26 | 27 | .name { 28 | width: 37%; 29 | } 30 | 31 | .input { 32 | width: 34%; 33 | } 34 | 35 | .probe-asn { 36 | width: 10%; 37 | text-align: right; 38 | } 39 | 40 | .start-time { 41 | width: 10%; 42 | text-align: right; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /client/ngapp/styles/_highlights.scss: -------------------------------------------------------------------------------- 1 | /* Highlights */ 2 | 3 | .ooni-highlight { 4 | 5 | padding-left: 20px; 6 | padding-right: 20px; 7 | padding-top: 20px; 8 | 9 | margin-right: 10px; 10 | margin-top: 10px; 11 | 12 | border-radius: 3px; 13 | background-color: $color-ooni-blue; 14 | color: white; 15 | 16 | h3 { 17 | margin-top: 10px; 18 | } 19 | 20 | a { 21 | color: $color-grey; 22 | } 23 | 24 | a:hover { 25 | color: white; 26 | } 27 | 28 | .ooni-highlight-number { 29 | font-weight: 500; 30 | font-size: 2em; 31 | } 32 | 33 | .country-name { 34 | float: left; 35 | padding-right: 20px; 36 | 37 | .flag-icon { 38 | height: 50px; 39 | width: 66px; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/ngapp/styles/_loading.scss: -------------------------------------------------------------------------------- 1 | /* Loading */ 2 | 3 | .loading { 4 | 5 | margin-top: 50px; 6 | margin-bottom: 75px; 7 | 8 | .ooni-loader { 9 | text-align: center; 10 | } 11 | 12 | .ooni-loading-text { 13 | margin-top: 35px; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /client/ngapp/styles/_measurement.scss: -------------------------------------------------------------------------------- 1 | /* Measurement */ 2 | 3 | .anomalies { 4 | list-style: none; 5 | 6 | li { 7 | margin-left: 0; 8 | } 9 | } 10 | 11 | .measurement { 12 | 13 | h1 { 14 | margin: .5rem 0; 15 | } 16 | 17 | .measurement-info { 18 | background-color: $color-off-white; 19 | margin: 1rem 0 2rem 0; 20 | padding: 1rem 1.25rem; 21 | 22 | .detail { 23 | 24 | font-family: 'Fira Sans'; 25 | color: $color-grey; 26 | 27 | &.tiny { 28 | font-size: 16px; 29 | } 30 | } 31 | } 32 | 33 | } 34 | 35 | 36 | label[for="anomalies"] { 37 | margin-top: 1rem; 38 | display: block; 39 | } 40 | 41 | .experiment-vs-control { 42 | margin-top: 1rem; 43 | 44 | table { 45 | position: relative; 46 | width: 100%; 47 | margin-top: 1rem; 48 | } 49 | 50 | thead { 51 | font-weight: bold; 52 | td { 53 | padding: 0.25rem 0.5rem 54 | } 55 | } 56 | 57 | .control-value, 58 | .experiment-value { 59 | width: 40%; 60 | } 61 | 62 | .header-name { 63 | width: 20%; 64 | } 65 | 66 | tbody td { 67 | font-size: .8rem; 68 | } 69 | 70 | .difference td { 71 | background-color: transparentize($color-blocked, 0.9); 72 | } 73 | } 74 | 75 | .CodeMirror { 76 | font-size: .8rem; 77 | background-color: $color-off-white; 78 | padding: .5rem; 79 | height: 250px; 80 | transition: height $transition-speed; 81 | } 82 | 83 | .openCodeMirror { 84 | .CodeMirror { 85 | height: 500px; 86 | } 87 | } 88 | 89 | .expandBody { 90 | color: darken($color-off-white, 20%); 91 | } 92 | 93 | .overview, 94 | .details { 95 | margin-top: 1rem; 96 | 97 | label { 98 | font-weight: bold; 99 | } 100 | 101 | .description { 102 | margin-top: 1rem; 103 | } 104 | 105 | .anomalous-result { 106 | background-color: $color-blocked; 107 | padding: 2rem; 108 | margin-bottom: 2rem; 109 | color: $color-off-white; 110 | font-size: 21px; 111 | vertical-align: middle;; 112 | 113 | i { 114 | font-size: 30px; 115 | margin-right: $base-spacing; 116 | } 117 | } 118 | 119 | .normal-result { 120 | background-color: $color-unblocked; 121 | padding: 2rem; 122 | color: $color-off-white; 123 | } 124 | 125 | .result { 126 | margin-bottom: 10px; 127 | text-align: center; 128 | } 129 | 130 | } 131 | 132 | .inline-test-spec { 133 | display: inline-block; 134 | box-sizing: border-box; 135 | margin-top: .5rem; 136 | margin-right: -5px; 137 | margin-left: 0px; 138 | border: 1px solid $color-grey; 139 | border-right: 0; 140 | padding: .5rem 1rem; 141 | 142 | &:last-child { 143 | border-right: 1px solid $color-grey; 144 | } 145 | } 146 | 147 | .note { 148 | padding: .75rem 1rem; 149 | background-color: transparentize($color-ooni-blue, .5); 150 | } 151 | -------------------------------------------------------------------------------- /client/ngapp/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | /* Mixins */ 2 | 3 | @mixin button-mixin($border) { 4 | padding: .35rem .5rem; 5 | font-size: .8rem; 6 | line-height: inherit; 7 | cursor: pointer; 8 | border: $border; 9 | font-family: "Fira Sans"; 10 | font-weight: 300; 11 | transition: $transition-speed background-color, $transition-speed color; 12 | } 13 | 14 | @mixin primary-button-mixin($border, $background-color, $color) { 15 | @include button-mixin($border); 16 | background-color: $background-color; 17 | color: $color; 18 | border: $border; 19 | } 20 | 21 | @mixin placeholder-text($font-family, $font-weight) { 22 | ::-webkit-input-placeholder { 23 | font-family: $font-family; 24 | font-weight: $font-weight; 25 | } 26 | 27 | :-moz-placeholder { /* Firefox 18- */ 28 | font-family: $font-family; 29 | font-weight: $font-weight; 30 | } 31 | 32 | ::-moz-placeholder { /* Firefox 19+ */ 33 | font-family: $font-family; 34 | font-weight: $font-weight; 35 | } 36 | 37 | :-ms-input-placeholder { 38 | font-family: $font-family; 39 | font-weight: $font-weight; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/ngapp/styles/_world.scss: -------------------------------------------------------------------------------- 1 | /* World */ 2 | 3 | .worldMapLegend { 4 | margin-top: -300px; 5 | margin-bottom: 50px; 6 | 7 | dl { 8 | margin-bottom: 15px; 9 | font-size: 16px; 10 | 11 | dd { 12 | display: block; 13 | 14 | span { 15 | display: inline-block; 16 | width: 12px; 17 | height: 12px; 18 | border-radius: 2px; 19 | } 20 | 21 | i { 22 | font-size: 0.6em; 23 | font-family: 'FontAwesome' !important; 24 | color: $color-red; 25 | } 26 | } 27 | } 28 | } 29 | 30 | .countries-list { 31 | list-style: none; 32 | margin: 0; 33 | 34 | li { 35 | width: 30.33%; 36 | display: inline-block; 37 | margin: 0 1.5% 2% 1.5%; 38 | padding: 0 0 1.5rem 0; 39 | 40 | border-bottom: 1px solid $color-grey-light; 41 | } 42 | 43 | .country { 44 | width: 65%; 45 | float: left; 46 | display: inline-block; 47 | font-family: "Fira Sans"; 48 | font-weight: bold; 49 | font-size: 18px; 50 | 51 | i { 52 | margin-right: 10px; 53 | } 54 | } 55 | 56 | .count { 57 | float: right; 58 | background-color: $color-off-white; 59 | border-radius: $border-radius; 60 | display: inline-block; 61 | padding: .25rem .5rem; 62 | text-align: center; 63 | font-size: 14px; 64 | 65 | span { 66 | font-size: 18px; 67 | font-weight: bold; 68 | display: block; 69 | text-align: center; 70 | } 71 | } 72 | 73 | } 74 | 75 | @media (max-width: 1029px) { 76 | 77 | .countries-list { 78 | list-style: none; 79 | margin: 0; 80 | 81 | li { 82 | width: 47%; 83 | } 84 | } 85 | } 86 | 87 | 88 | @media (max-width: 809px) { 89 | 90 | .countries-list { 91 | list-style: none; 92 | margin: 0; 93 | 94 | li { 95 | width: 97%; 96 | } 97 | } 98 | 99 | .world-map-container { 100 | display: none; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /client/ngapp/styles/fonts/charter-bold-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/fonts/charter-bold-italic.woff -------------------------------------------------------------------------------- /client/ngapp/styles/fonts/charter-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/fonts/charter-bold.woff -------------------------------------------------------------------------------- /client/ngapp/styles/fonts/charter-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/fonts/charter-italic.woff -------------------------------------------------------------------------------- /client/ngapp/styles/fonts/charter-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/fonts/charter-regular.woff -------------------------------------------------------------------------------- /client/ngapp/styles/fonts/fira-sans-bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/fonts/fira-sans-bold.otf -------------------------------------------------------------------------------- /client/ngapp/styles/fonts/fira-sans-light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/fonts/fira-sans-light.otf -------------------------------------------------------------------------------- /client/ngapp/styles/fonts/fira-sans-semi-bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/fonts/fira-sans-semi-bold.otf -------------------------------------------------------------------------------- /client/ngapp/styles/fonts/ooni-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/fonts/ooni-icons.eot -------------------------------------------------------------------------------- /client/ngapp/styles/fonts/ooni-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/fonts/ooni-icons.ttf -------------------------------------------------------------------------------- /client/ngapp/styles/fonts/ooni-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/fonts/ooni-icons.woff -------------------------------------------------------------------------------- /client/ngapp/styles/fonts/source-code-pro-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/fonts/source-code-pro-bold.woff -------------------------------------------------------------------------------- /client/ngapp/styles/fonts/source-code-pro-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/fonts/source-code-pro-regular.woff -------------------------------------------------------------------------------- /client/ngapp/styles/main.scss: -------------------------------------------------------------------------------- 1 | /* OONI Explorer - Stylesheet */ 2 | 3 | // Colors 4 | $color-ooni-blue: #0588CB; 5 | $color-white: #fff; 6 | $color-off-white: #f2f2f2; 7 | $color-footer-background-color: #26292c; 8 | $color-footer-color: #b4b4b4; 9 | $color-blocked: #B83564; 10 | $color-unblocked: #4FD156; 11 | $color-red: #cc0000; 12 | $color-grey-light: #d9d9d9; 13 | $color-grey: #c1c1c1; 14 | 15 | // Misc 16 | $base-spacing: 10px; 17 | $border-radius: 3px; 18 | 19 | // Buttons 20 | $button-border: 1px solid $color-ooni-blue; 21 | $button-border-hover: 1px solid darken($color-ooni-blue, 10%); 22 | $button-border-radius: 3px; 23 | 24 | // Other 25 | $transition-speed: .5s; 26 | 27 | @import "mixins"; 28 | @import "ui/fonts", 29 | "ui/table", 30 | "ui/paragraph", 31 | "ui/pagination", 32 | "ui/table-of-contents", 33 | "ui/buttons", 34 | "ui/forms", 35 | "ui/ui-grid", 36 | "ui/section", 37 | "ui/more-info-hover", 38 | "ui/icons", 39 | "base", 40 | "loading", 41 | "country", 42 | "highlights", 43 | "explore", 44 | "measurement", 45 | "world"; 46 | 47 | .capitalize { 48 | text-transform: capitalize; 49 | } 50 | 51 | .header-wrapper { 52 | background-color: $color-ooni-blue; 53 | 54 | .row { 55 | margin-bottom: 1rem; 56 | margin-top: 0; 57 | } 58 | 59 | header { 60 | img { 61 | display: inline-block; 62 | vertical-align: middle; 63 | position: relative; 64 | top: -5px; 65 | left: 0px; 66 | margin-right: 20px; 67 | } 68 | 69 | h1 { 70 | display: inline-block; 71 | color: white; 72 | margin-bottom: 0; 73 | padding: 1rem 0; 74 | } 75 | } 76 | 77 | .view-chooser { 78 | text-align: center; 79 | position: relative; 80 | z-index: 999; 81 | 82 | a { 83 | @include button-mixin(3px); 84 | margin: 0 .85rem; 85 | font-size: 1rem; 86 | font-weight: bold; 87 | color: $color-white; 88 | border-radius: 3px; 89 | 90 | &:hover { 91 | background-color: darken($color-ooni-blue, 10%); 92 | color: white; 93 | } 94 | 95 | &.selected { 96 | @include primary-button-mixin(0px, $color-white, $color-ooni-blue); 97 | border-radius: 0; 98 | font-size: 1rem; 99 | font-weight: bold; 100 | line-height: inherit; 101 | border-radius: 3px; 102 | 103 | &:hover { 104 | background-color: $color-white; 105 | color: $color-ooni-blue; 106 | } 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /client/ngapp/styles/ui-grid.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/ui-grid.ttf -------------------------------------------------------------------------------- /client/ngapp/styles/ui-grid.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ooni/explorer-legacy/19fedce6e4884376e507db6546111bf175695bed/client/ngapp/styles/ui-grid.woff -------------------------------------------------------------------------------- /client/ngapp/styles/ui/_buttons.scss: -------------------------------------------------------------------------------- 1 | /* Buttons */ 2 | 3 | button { 4 | @include primary-button-mixin($button-border, $color-ooni-blue, $color-white); 5 | 6 | &:hover { 7 | background-color: darken($color-ooni-blue, 10%); 8 | color: white; 9 | } 10 | } 11 | 12 | button.secondary { 13 | background-color: $color-white; 14 | color: $color-ooni-blue; 15 | 16 | &:hover { 17 | background-color: darken($color-ooni-blue, 10%); 18 | color: white; 19 | } 20 | } 21 | 22 | button.secondary.selected { 23 | background-color: darken($color-ooni-blue, 10%); 24 | color: $color-white; 25 | } 26 | -------------------------------------------------------------------------------- /client/ngapp/styles/ui/_fonts.scss: -------------------------------------------------------------------------------- 1 | /* Fonts */ 2 | 3 | @font-face { 4 | font-family: "Charter"; 5 | src: url("fonts/charter-regular.woff"); 6 | } 7 | 8 | @font-face { 9 | font-family: "Charter"; 10 | src: url("fonts/charter-bold.woff"); 11 | font-weight: bold; 12 | } 13 | 14 | @font-face { 15 | font-family: "Charter"; 16 | src: url("fonts/charter-italic.woff"); 17 | font-style: italic; 18 | } 19 | 20 | @font-face { 21 | font-family: "Charter"; 22 | src: url("fonts/charter-bold-italic.woff"); 23 | font-weight: bold; font-style: italic; 24 | } 25 | 26 | @font-face { 27 | font-family: "Fira Sans"; 28 | src: url("fonts/fira-sans-bold.otf"); 29 | font-weight: bold; 30 | } 31 | 32 | @font-face { 33 | font-family: "Fira Sans"; 34 | src: url("fonts/fira-sans-semi-bold.otf"); 35 | font-weight: 500; 36 | } 37 | 38 | @font-face { 39 | font-family: "Fira Sans"; 40 | src: url("fonts/fira-sans-light.otf"); 41 | font-weight: 300; 42 | } 43 | 44 | @font-face { 45 | font-family: "Source Code Pro"; 46 | src: url("fonts/source-code-pro-regular.woff"); 47 | } 48 | 49 | @font-face { 50 | font-family: "Source Code Pro"; 51 | src: url("fonts/source-code-pro-bold.woff"); 52 | font-weight: bold; 53 | } 54 | -------------------------------------------------------------------------------- /client/ngapp/styles/ui/_forms.scss: -------------------------------------------------------------------------------- 1 | /* Forms */ 2 | 3 | .fa-chevron-circle-down { 4 | transition: $transition-speed; 5 | } 6 | 7 | .fa-chevron-circle-down.open { 8 | transform: rotate(180deg); 9 | } 10 | 11 | .daterangepicker.dropdown-menu { 12 | display: none; 13 | } 14 | 15 | // Specific Forms 16 | 17 | .filter-toggle { 18 | margin: 1rem 0 1rem; 19 | cursor: pointer; 20 | display: block; 21 | } 22 | 23 | .filter-form { 24 | @include placeholder-text("Fira Sans", 300) 25 | 26 | padding: 1.5rem 1.5rem 0 1.5rem; 27 | background: $color-off-white none repeat scroll 0% 0%; 28 | margin: 0 0 1.5rem 0; 29 | 30 | .form-group { 31 | margin: 0 1.5rem 1.5rem 0; 32 | display: block; 33 | } 34 | 35 | .form-group.inline { 36 | display: inline-block; 37 | } 38 | 39 | select, 40 | input { 41 | font-size: .8rem; 42 | padding: .25rem .5rem; 43 | } 44 | 45 | input { 46 | border-radius: $button-border-radius; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /client/ngapp/styles/ui/_icons.scss: -------------------------------------------------------------------------------- 1 | /* Icons */ 2 | 3 | @font-face { 4 | font-family: 'ooni-icons'; 5 | src: url('fonts/ooni-icons.eot?lb0vhf'); 6 | src: url('fonts/ooni-icons.eot?lb0vhf#iefix') format('embedded-opentype'), 7 | url('fonts/ooni-icons.ttf?lb0vhf') format('truetype'), 8 | url('fonts/ooni-icons.woff?lb0vhf') format('woff'), 9 | url('fonts/ooni-icons.svg?lb0vhf#ooni-icons') format('svg'); 10 | font-weight: normal; 11 | font-style: normal; 12 | } 13 | 14 | @mixin ooni-icons { 15 | /* use !important to prevent issues with browser extensions that change fonts */ 16 | font-family: 'ooni-icons' !important; 17 | speak: none; 18 | font-style: normal; 19 | font-weight: normal; 20 | font-variant: normal; 21 | text-transform: none; 22 | line-height: 1; 23 | 24 | /* Better Font Rendering =========== */ 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | } 28 | 29 | 30 | .icon-analysis { 31 | @include ooni-icons; 32 | } 33 | .icon-analysis:before { 34 | content: "\e900"; 35 | } 36 | .icon-censorhip-tampering { 37 | @include ooni-icons; 38 | } 39 | .icon-censorhip-tampering:before { 40 | content: "\e901"; 41 | } 42 | .icon-censorhip-vendor { 43 | @include ooni-icons; 44 | } 45 | .icon-censorhip-vendor:before { 46 | content: "\e902"; 47 | } 48 | .icon-censorship-confirmed { 49 | @include ooni-icons; 50 | } 51 | .icon-censorship-confirmed:before { 52 | content: "\e903"; 53 | } 54 | .icon-country-statistics { 55 | @include ooni-icons; 56 | } 57 | .icon-country-statistics:before { 58 | content: "\e904"; 59 | } 60 | .icon-decentralization { 61 | @include ooni-icons; 62 | } 63 | .icon-decentralization:before { 64 | content: "\e905"; 65 | } 66 | .icon-distributed { 67 | @include ooni-icons; 68 | } 69 | .icon-distributed:before { 70 | content: "\e906"; 71 | } 72 | .icon-http-body { 73 | @include ooni-icons; 74 | } 75 | .icon-http-body:before { 76 | content: "\e907"; 77 | } 78 | .icon-http-headers { 79 | @include ooni-icons; 80 | } 81 | .icon-http-headers:before { 82 | content: "\e908"; 83 | } 84 | .icon-measurement { 85 | @include ooni-icons; 86 | } 87 | .icon-measurement:before { 88 | content: "\e909"; 89 | } 90 | .icon-progress { 91 | @include ooni-icons; 92 | } 93 | .icon-progress:before { 94 | content: "\e90a"; 95 | } 96 | .icon-research { 97 | @include ooni-icons; 98 | } 99 | .icon-research:before { 100 | content: "\e90b"; 101 | } 102 | .icon-server { 103 | @include ooni-icons; 104 | } 105 | .icon-server:before { 106 | content: "\e90c"; 107 | } 108 | .icon-tor { 109 | @include ooni-icons; 110 | } 111 | .icon-tor:before { 112 | content: "\e90d"; 113 | } 114 | -------------------------------------------------------------------------------- /client/ngapp/styles/ui/_more-info-hover.scss: -------------------------------------------------------------------------------- 1 | /* More Info Hover */ 2 | 3 | .hover-wrapper { 4 | position: relative; 5 | cursor: pointer; 6 | 7 | .hover-definition { 8 | display: none; 9 | position: absolute; 10 | background: rgba(0, 0, 0, .8); 11 | color: white; 12 | padding: .5rem 1rem; 13 | width: 20rem; 14 | opacity: 0; 15 | top: 2rem; 16 | left: 0; 17 | vertical-align: top; 18 | } 19 | 20 | &:hover .hover-definition { 21 | display: inline-block; 22 | opacity: 1; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/ngapp/styles/ui/_pagination.scss: -------------------------------------------------------------------------------- 1 | /* Pagination */ 2 | 3 | .pagination { 4 | margin: 0 1rem 1rem 0; 5 | display: inline-block; 6 | 7 | li { 8 | display: inline-block; 9 | margin: 0; 10 | padding: .25rem .5rem; 11 | border: 1px solid $color-off-white; 12 | border-right: 0px; 13 | } 14 | 15 | a { 16 | cursor: pointer; 17 | } 18 | 19 | li:last-child { 20 | border-top-right-radius: 3px; 21 | border-bottom-right-radius: 3px; 22 | border-right: 1px solid $color-off-white; 23 | } 24 | li:first-child { 25 | border-top-left-radius: 3px; 26 | border-bottom-left-radius: 3px; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/ngapp/styles/ui/_paragraph.scss: -------------------------------------------------------------------------------- 1 | /* Paragraph */ 2 | 3 | .highlight { 4 | background-color: lighten($color-ooni-blue, 50%); 5 | padding-left: .5rem; 6 | padding-right: .5rem; 7 | } 8 | -------------------------------------------------------------------------------- /client/ngapp/styles/ui/_section.scss: -------------------------------------------------------------------------------- 1 | /* Section */ 2 | 3 | .country section, 4 | .website section { 5 | margin-top: 2em; 6 | 7 | h2 { 8 | margin-bottom: 1em; 9 | } 10 | 11 | .no-show { 12 | background-color: $color-off-white; 13 | padding: 2rem; 14 | text-align: center; 15 | color: $color-grey; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/ngapp/styles/ui/_table-of-contents.scss: -------------------------------------------------------------------------------- 1 | /* Table of Contents */ 2 | 3 | .toc { 4 | 5 | border: 1px solid $color-grey; 6 | padding: .5rem 1rem; 7 | display: inline-block; 8 | list-style: none; 9 | vertical-align: top; 10 | margin-right: .5rem; 11 | font-size: 14px; 12 | 13 | li { 14 | padding: 0; 15 | margin: 0; 16 | } 17 | 18 | a { 19 | cursor: pointer; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/ngapp/styles/ui/_table.scss: -------------------------------------------------------------------------------- 1 | /* Table */ 2 | 3 | table { 4 | width: 100%; 5 | border-collapse: collapse; 6 | } 7 | 8 | thead { 9 | th { 10 | font-weight: bold; 11 | text-align: left; 12 | } 13 | } 14 | 15 | tbody { 16 | td, 17 | th { 18 | padding: .25rem .5rem; 19 | transition: $transition-speed background-color; 20 | } 21 | 22 | th { 23 | font-weight: 600; 24 | text-align: left; 25 | } 26 | 27 | tr:nth-child(even) td, 28 | tr:nth-child(even) th{ 29 | background-color: darken($color-white, 2%); 30 | } 31 | 32 | tr:hover td, 33 | tr:hover th { 34 | background-color: $color-off-white; 35 | } 36 | } 37 | 38 | table.countries { 39 | td { 40 | cursor: pointer; 41 | } 42 | } 43 | 44 | .clickable { 45 | cursor: pointer; 46 | 47 | i::before { 48 | color: $color-off-white; 49 | transition: color $transition-speed; 50 | } 51 | 52 | &:hover { 53 | i::before { 54 | color: $color-ooni-blue; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client/ngapp/styles/ui/_ui-grid.scss: -------------------------------------------------------------------------------- 1 | /* UI Grid */ 2 | 3 | .ui-grid { 4 | border: none; 5 | } 6 | 7 | .ui-grid-header { 8 | border-top: 3px solid $color-ooni-blue; 9 | 10 | .ui-grid-top-panel { 11 | background: transparent; 12 | } 13 | 14 | .ui-grid-header-cell { 15 | border-right: none; 16 | } 17 | } 18 | 19 | .ui-grid-row { 20 | cursor: pointer; 21 | 22 | &:hover { 23 | .ui-grid-cell { 24 | background-color: darken($color-white, 10%); 25 | } 26 | } 27 | } 28 | 29 | .ui-grid-viewport { 30 | .ui-grid-cell { 31 | border-right: none; 32 | } 33 | } 34 | 35 | .row-link { 36 | color: #000; 37 | } 38 | -------------------------------------------------------------------------------- /client/ngapp/views/about.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

What is OONI?

4 | 5 |

The Open Observatory of Network Interference (OONI) is a free software project under the Tor Project which collects and processes network measurements with the aim of detecting network anomalies, such as censorship, surveillance and traffic manipulation. Since late 2012, OONI has collected millions of measurements across more than 90 countries around the world.

6 | 7 |

What is OONI Explorer?

8 | 9 |

OONI Explorer is a global map which provides a location to explore and interact with all of the network measurements that have been collected through OONI tests from 2012 until today. These tests are designed to: 10 |

15 | 16 |

17 | 18 |

OONI Explorer serves as a resource for researchers, journalists, lawyers, activists, advocates and anyone interested in exploring network anomalies, such as censorship, surveillance and traffic manipulation, and how the internet works in general.

19 | 20 |

Note: The network measurements included in OONI Explorer were collected in specific moments in time and within specific networks and do not necessarily reflect the overall levels of censorship and traffic manipulation across time or on country-wide levels.

21 | 22 |

Contact

23 |

24 | To contact the OONI team send an email to contact [AT] openobservatory.org. 25 | Encrypted emails can be sent using the following PGP key: 26 |

pub   4096R/6B2943F00CB177B7 2016-03-23 [expires: 2018-03-26]
27 |       Key fingerprint = 4C15 DDA9 96C6 C0CF 48BD  3309 6B29 43F0 0CB1 77B7
28 | uid       [ultimate] OONI - Open Observatory of Network Interference
29 | sub   4096R/8EBD2087374399AB 2016-03-23 [expires: 2018-03-26]
30 |

31 |

Credits

32 | 33 |

OONI Explorer contributors: 34 | 35 |

47 | 48 |

49 | 50 |

Special thanks to: 51 | 52 |

58 | 59 |

60 |
61 |
62 | -------------------------------------------------------------------------------- /client/ngapp/views/country-view.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | 6 | 7 |

8 |
9 | 10 | 30 | 31 |
32 | 33 |

34 | Here's what we know about anomalies with internet connections located in {{ countryName | lowercase }}. 35 |

36 | 37 |

{{ count | number }} Measurements Collected

38 |
39 | 40 |
43 |
44 | 45 |
47 |
48 | 49 |
52 |
53 | 54 |
55 |

56 | Country Internet Statistics 57 |

58 |

Datasource: World Fact Book

59 | 60 | 61 | 62 | 66 | 67 | 70 | 71 | 74 | 75 | 78 | 79 | 80 |
81 |
82 | 83 |
84 |

Methods of Censorship

85 |
    86 |
  • 87 |

    {{censorshipMethod.name}}

    88 |

    {{censorshipMethod.description}}

    89 |
  • 90 |
91 |
92 | 93 |
94 |

Identified Vendors

95 |
    96 |
  • 97 |

    {{vendor.vendor}}

    98 | ASN: {{ vendor.probe_asn }}
    99 | Identified on: {{ vendor.test_start_time | date:'shortDate'}}
    100 | view measurement » 101 |
  • 102 |
103 |
104 | We've identified no vendors for this country. 105 |
106 |
107 | 108 |
109 |

Blocked Websites

110 |
    111 |
  • 112 | {{ obj.input }}: 113 | 117 | 118 | 121 |
      122 |
    • 123 | {{ measurement.test_start_time | date:'shortDate' }} 124 | view »
    • 125 |
    126 |
  • 127 |
128 |
130 | loading more... 131 |
132 |
133 | We've identified no blocked pages for this country. 134 |
135 |
136 | 137 |
138 | -------------------------------------------------------------------------------- /client/ngapp/views/directives/ooni-country-bar-chart.directive.html: -------------------------------------------------------------------------------- 1 |
2 | 4 | 5 | 6 | 8 | 9 | View Older 10 |     11 | 13 | View Newer 14 | 15 | 16 |
17 | 18 |

19 | The graph above shows when we've run tests, and how many sites we've found to be blocked out of all measured . 20 |

21 | -------------------------------------------------------------------------------- /client/ngapp/views/directives/ooni-explorer-list-measurement.directive.html: -------------------------------------------------------------------------------- 1 |
2 | {{ measurement.probe_cc}} 3 |
4 |
{{ measurement.test_name }} 5 | 7 | 8 | View measurement » 9 | View » 10 | 11 |
12 |
{{ measurement.input }} 13 | 16 | 17 | View Page Information » 18 | View » 19 | 20 |
21 |
{{ measurement.probe_asn }}
22 |
{{ measurement.test_start_time|date:'shortDate' }}
23 | -------------------------------------------------------------------------------- /client/ngapp/views/directives/ooni-filter-list-form.directive.html: -------------------------------------------------------------------------------- 1 | 2 | Filter Results 3 | 4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 17 |
18 |
19 | 20 | 21 | 27 | 28 |
29 |
30 | 31 |
32 | 33 | 38 |
39 | 40 |
41 | -------------------------------------------------------------------------------- /client/ngapp/views/directives/ooni-grid-wrapper-directive.html: -------------------------------------------------------------------------------- 1 |
5 |
6 | 7 |
11 |
12 | -------------------------------------------------------------------------------- /client/ngapp/views/directives/ooni-info-country-list.directive.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 |
7 |
8 | 9 | 16 | -------------------------------------------------------------------------------- /client/ngapp/views/directives/ooni-info-explorer-list.directive.html: -------------------------------------------------------------------------------- 1 |
6 |
7 |
12 | 13 |
14 | 15 | 21 | 22 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /client/ngapp/views/directives/ooni-loader.directive.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |

Loading...

7 |

 8 |   
9 |
10 | -------------------------------------------------------------------------------- /client/ngapp/views/directives/ooni-more-info-hover-directive.html: -------------------------------------------------------------------------------- 1 | 2 |  {{ content }} 3 | {{ definition.description }} 4 | 5 | -------------------------------------------------------------------------------- /client/ngapp/views/directives/ooni-pagination.directive.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | {{ total|number }} measurements 13 | 14 | -------------------------------------------------------------------------------- /client/ngapp/views/directives/ooni-report-detail-table-row.html: -------------------------------------------------------------------------------- 1 | 2 | {{ label }} 3 | 4 | 5 | {{ content }} 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /client/ngapp/views/directives/row-template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
{{row.entity.title}}
4 |
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /client/ngapp/views/explore.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Explore our measurements from various types of tests and countries. Use the filter to narrow down what you're looking for. 4 |

5 |
7 |
8 | 13 | 14 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /client/ngapp/views/highlights.html: -------------------------------------------------------------------------------- 1 |

Highlights

2 | 3 |

Below is a selection of highlights which illustrate the presence of internet censorship and/or traffic manipulation within certain countries.

4 | 5 |
6 |
7 |

10.8 million network measurements have been collected across 96 countries around the world in 3 years (from late 2012 to early 2016).

8 |
9 | 10 |
11 |

Network anomalies have been detected in 71 out of 91 tested countries.

12 |

However, not all network anomalies are necessarily cases of censorship and/or traffic manipulation.

13 |
14 | 15 |
16 | 17 |
18 |
19 |

12 countries have confirmed cases of censorship: Russia, China, Iran, Saudi Arabia, Turkey, India, Indonesia, Greece, Sudan, Belgium, Cyprus and Korea.

20 |

By "confirmed censorship" we mean that we have an accurate heuristic to determine when blocking is occurring. Generally this means that we are looking for a block page in the response body of a HTTP request test.

21 |
22 | 23 |
24 |

3 vendors have been identified through our network measurements: Blue Coat, Squid and Privoxy. The use of their software has been detected in 12 countries around the world: USA, Canada, Portugal, Spain, Italy, the Netherlands, Switzerland, Moldova, Iraq, Uganda, Myanmar and Great Britain.

25 |

Such software may or may not have been responsible for online censorship and/or traffic manipulation.

26 |
27 |
28 | 29 |
30 |
31 |
32 | 33 |

Iran

34 |
35 |

Through our (HTTP request) tests in Iran, we detected 362 blocked websites, including www.iranrights.org, www.iranpressnews.com and www.iransocialforum.org.

36 |

However, our Psiphon tests showed that access to Psiphon was not blocked in Iran and this proxy can potentially still be used for accessing blocked websites.

37 |

go to country page »

38 |
39 | 40 |
41 |
42 | 43 |

Saudi Arabia

44 |
45 |

Through our (HTTP request) tests in Saudi Arabia, we detected 50 blocked websites, including www.arabtimes.com, www.mossad.gov.il and www.anonym.to, a URL shortening service with privacy properties.

46 |

go to country page »

47 |
48 | 49 |
50 |
51 |
52 |
53 | 54 |

Uganda

55 |
56 |

Following reports that Facebook and Twitter were blocked in Uganda leading up to its 2016 general elections, OONI tests were run in the country. While we were unable to detect block pages, the following are possibilities: 57 |

62 |

63 |

go to country page »

64 |
65 | 66 |
67 |
68 | 69 |

Myanmar

70 |
71 |

Blue Coat software (some types of which can potenially be used for internet filtering, censorship and surveillance) was detected in Myanmar through one of our (HTTP-field-manipulation) tests in late 2012. It's unclear, however, if this was a countrywide deployment or if it was only used in the specific network that we tested.

72 |

Two years later we ran the same test in the same network in Myanmar, but did not detect the presence of Blue Coat.

73 |

go to country page »

74 |
75 |
76 | 77 |
78 |
79 |
80 | 81 |

Cuba

82 |
83 |

While we did not detect any block pages in Cuba, many websites triggered connectivity errors. Such websites include www.cubafreepress.org and www.cubademocraciayvida.org, which were likely censored.

84 |

go to country page »

85 |
86 | 87 |
88 |
89 | 90 |

India

91 |
92 |

In January 2015 the Government of India ordered Internet Service Licensees to block 32 websites under Section 69A of the Information Technology Act, 2000, and under the Information Technology (Procedures and Safeguards for Blocking of Access of Information by Public) Rules, 2009.

93 | 94 |

We ran network measurements on those 32 websites and detected block pages (confirmed cases of censorship) for 23 of them, including dailymotion.com, pastebin.com and archive.org.

95 |
96 |
97 | 98 |
99 |
100 |
101 | 102 |

Pakistan

103 |
104 |

Multiple (HTTP-invalid-request-line) tests conducted in Pakistan show that "middle boxes" (software which could potentially be used for censorship and/or traffic manipulation) were present in Pakistani networks. However, it remains unclear if such software was actually used for the purpose of censorship and/or traffic manipulation.

105 |

go to country page »

106 |
107 | 108 |
109 |
110 | 111 |

Vietnam

112 |
113 |

Multiple (HTTP-invalid-request-line) tests conducted in Vietnam from 2013 to 2015 show that "middle boxes" (software which could potentially be used for censorship and/or traffic manipulation) were present in Vietnamese networks. However, it remains unclear if such software was actually used for the purpose of censorship and/or traffic manipulation.

114 |

go to country page »

115 |
116 |
117 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/bridge-reachability.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ report.test_keys.body_length_match }} 5 | {{ report.input }} 6 |
7 | 8 |
9 |

Tor Bridge Reachability

10 | 11 |

This test examines whether Tor bridges work in tested networks.

12 | 13 |

Tor is free and open 14 | source software which enables online anonymity and censorship 15 | circumvention. It was designed to bounce communications around a 16 | distributed network of relays run by volunteers around the world, 17 | thus hiding users' IP address and circumventing online tracking and 18 | censorship. However, Internet Service Providers (ISPs) in various 19 | countries around the world are often ordered by their governments 20 | to block users' access to Tor. As a result, 21 | Tor bridges were developed to enable users to 22 | connect to the Tor network in countries where such access is blocked.

23 | 24 | 25 |

This test runs Tor with a list of bridges and if it's able to connect to 26 | them successfully, we consider that Tor bridges are not blocked in the tested 27 | network. If the test, however, is unable to bootstrap a connection, then the 28 | Tor bridges are either offline or blocked.

29 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/bridget.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ report.test_keys.body_length_match }} 5 | {{ report.input }} 6 |
7 | 8 |
9 |

Tor Bridge

10 |

Detect whether or not a Tor bridge is reachable from a specific network vantage point on a network.

11 |

TODO: This description needs to be made better. 12 |

13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/captive-portal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Details for this specific test:

5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 |
14 |

Captive Portal

15 |

16 | This test emulates the requests that common software vendors use to 17 | detect the presence of a captive portal. A captive portal is a 18 | device between the users and the internet, generally used to 19 | implement authentication to the network. 20 |

21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/dns-consistency.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ report.test_keys.inconsistent }} 5 |
6 | {{ report.input }} 7 | View Website Information » 8 |
9 | 10 |
11 |

DNS Consistency

12 |

This test compares the DNS query results from a DNS resolver which is considered to be reliable with one that is tested for tampering.

13 |

14 | The domain name system (DNS) is what is responsible for transforming a host name (e.g. torproject.org) into an IP address (e.g. 38.229.72.16). ISPs, amongst others, run DNS resolvers which map IP addresses to host names. In certain circumstances though, ISPs map the wrong IP addresses to the wrong host names. This is a form of tampering, which OONI can detect by running its DNS consistency test.

15 |

This test compares the IP address of a given host name allocated by the Google DNS resolver (which we assume to not be tampered with) with the IP address mapped to that website by a provider. If the two IP addresses of the same website are different, then there is a sign of network interference. When ISPs tamper with DNS answers, users are redirected to other websites or fail to connect to their intended websites.

16 |

17 | Note: DNS resolvers, such as Google or your local ISP, often provide users with IP addresses that are closest to them geographically. Often this is not done with the intent of network tampering, but merely for the purpose of providing users faster access to websites. As a result, some false positives might arise in OONI measurements.

18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/dns-injection.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

DNS Injection

5 |

6 | Ability to detect the presence of DNS Injection and list of domains 7 | that are being blocked via DNS injection. 8 |

9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/dns-spoof.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ report.test_keys.body_length_match }} 5 | {{ report.input }} 6 |
7 | 8 |
9 |

DNS Spoof

10 |

This test assesses if the blocking is happening due to DNS 11 | spoofing or not. DNS spoofing occurs when your connection to a DNS 12 | resolver is spoofed and you always receive a falsified answer. For 13 | example, you know that a certain domain is blocked on a given network. 14 | So you take this domain, the DNS resolver of the network and 15 | then that of a DNS resolver which you know that doesn't lie 16 | (e.g. Google DNS resolver). If they both match, that means that 17 | something between you and Google has spoofed the response and thus 18 | DNS censorship is happening by spoofing the responses.

19 |

20 | Note: If you run this test with a site that is not censored, it 21 | will tell you that spoofing is occurring (because they match), 22 | while it's actually not. In such cases, you may have false 23 | positives. 24 |

25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/http-header-field-manipulation.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ report.test_keys.tampering.header_field_name}} 5 |
6 | {{ report.test_keys.tampering.header_field_value}} 7 |
8 | {{ report.test_keys.tampering.header_field_number}} 9 |
10 | {{ report.test_keys.tampering.header_name_capitalization}} 11 |
12 | {{ report.test_keys.tampering.request_line_capitalization}} 13 |
14 | {{ report.test_keys.tampering.total }} 15 |
16 | {{ report.test_keys.tampering.header_name_diff }} 17 |
18 | 19 |
20 |

HTTP Header Manipulation

21 | 22 |

This test tries to detect the presence of censorship and/or 23 | surveillance software (“middle box”) which could be responsible for 24 | traffic manipulation.

25 | 26 |

HTTP is a protocol which transfers or exchanges data across the internet. It 27 | does so by handling a client's request to connect to a server, and a server's 28 | response to a client's request. Every time you connect to a server, you (the 29 | client) send a request through the HTTP protocol to that server. Such requests 30 | include “HTTP headers”, which transmit various types of information, including 31 | your device's operating system and the type of browser that it's using. If you 32 | are using Firefox on Windows, for example, the “user agent header” in your HTTP 33 | request will tell the server that you're trying to connect to that you're using 34 | a Firefox browser on a Windows operating system.

35 | 36 |

This test emulates what would have been a valid HTTP request towards a 37 | server, but instead sends HTTP headers that have variations in capitalization. 38 | In other words, this test sends HTTP requests which include valid, but non-standard 39 | HTTP headers. Such requests are sent to a backend control server which sends back 40 | any data it receives. If we receive the HTTP headers exactly as we sent them, 41 | then we assume that there is no “middle box” in the network which could be 42 | responsible for censorship, surveillance and/or traffic manipulation. If, 43 | however, such software is present in the network that we are testing, it will 44 | likely normalize the invalid headers that we are sending or add extra headers.

45 | 46 |

Depending on whether the HTTP headers that we send and receive from a 47 | backend control server are the same or not, we are able to evaluate whether 48 | software – which could be responsible for traffic manipulation – is present in 49 | the network that we are testing.

50 | 51 |

Note: A false negative could potentially occur in the 52 | hypothetical instance that ISPs are using highly sophisticated software 53 | that is specifically designed to not interfere with invalid HTTP headers 54 | when it receives them. Furthermore, the presence of a middle box is not 55 | necessarily indicative of traffic manipulation, as they are often used in 56 | networks for caching purposes.

57 | 58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/http-host.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ report.test_keys.send_host_header }} 5 |
6 | {{ report.test_keys.filtering_of_subdomain }} 7 |
8 | {{ report.test_keys.transparent_http_proxy }} 9 |
10 | {{ report.test_keys.filtering_add_tab_to_host }} 11 |
12 | {{ report.test_keys.filtering_via_fuzzy_matching }} 13 |
14 | {{ report.test_keys.filtering_prepend_newline_to_method }} 15 |
16 | {{ report.input }} 17 |
18 | 19 |
20 |

HTTP Host

21 |

This test attempts to:

22 | 23 |
    24 |
  • examine whether the domain names of websites are blocked
  • 25 |
  • detect the presence of “middle boxes” (software which could be used for censorship and/or traffic manipulation) in tested networks
  • 26 |
  • assess which censorship circumvention techniques are capable of bypassing the censorship implemented by the “middle box”
  • 27 |
28 | 29 |

HTTP is a protocol which transfers or exchanges data across the internet. It 30 | does so by handling a client's request to connect to a server, and a server's 31 | response to a client's request. Every time you connect to a server, you (the 32 | client) send a request through the HTTP protocol to that server. Such requests 33 | include “HTTP headers”, some of which (the “Host header”) include information 34 | about the specific domain that you want to connect to. When you connect to 35 | torproject.org, for example, the host header of your HTTP request includes 36 | information which communicates that you want to connect to that domain.

37 | 38 |

This test implements a series of techniques which help it evade getting 39 | detected from censors and then uses a list of domain names (such as bbc.co.uk) 40 | to connect to an OONI backend control server, which sends a JSON structure 41 | containing the host headers of those domain names back to us. If a “middle box” 42 | is detected between the network path of the probe and the OONI backend control 43 | server, its fingerprint might be included in the JSON data that we receive from 44 | the backend control server. Such data also informs us if the tested domain 45 | names are blocked or not, as well as how the censor tried to fingerprint the 46 | censorship of those domains. This can sometimes lead to the identification of 47 | the type of infrastructure being used to implement censorship.

48 | 49 |

Note: The presence of a middle box is not necessarily 50 | indicative of censorship and/or traffic manipulation, as they are often used in 51 | networks for caching purposes.

52 | 53 |
54 |
55 |
56 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/http-invalid-request-line.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Details for this specific test:

5 | 6 | 7 | 8 | 12 | 13 | 14 |
15 |
16 | 17 |
18 |

HTTP Invalid Request

19 |

This test tries to detect the presence of censorship and/or surveillance software (“middle box”) which could be responsible for traffic manipulation.

20 | 21 |

A very useful debugging and measurement tool is an echo service, which simply sends back to the originating source any data it receives. Instead of sending a normal HTTP request, this test sends an invalid HTTP request line - containing an invalid HTTP version number, an invalid field count and a huge request method – to an echo service listening on the standard HTTP port. If a middle box is not present in the network between the user and an echo service, then the echo service will send the invalid HTTP request line back to the user, exactly as it received it. In such cases, we assume that there is no visible traffic manipulation in the tested network.

22 | 23 |

If, however, a middle box is present in the tested network, the invalid HTTP request line will be intercepted by the middle box and this may trigger an error and that will subsequently be sent back to OONI. Such errors indicate that software for traffic manipulation is likely placed in the tested network, though it's not always clear what that software is. In some cases though, we are able to identify censorship and/or surveillance vendors through the error messages in the received invalid HTTP responses.

24 | 25 |

So far, we have detected, thanks to this technique, the use of BlueCoat, Squid and Privoxy in networks across 11 countries around the world.

26 | 27 |

Note: A false negative could potentially occur in the hypothetical instance that ISPs are using highly sophisticated censorship and/or surveillance software that is specifically designed to not trigger errors when receiving invalid HTTP request lines like the ones of this test. Furthermore, the presence of a middle box is not necessarily indicative of traffic manipulation, as they are often used in networks for caching purposes.

28 | 29 |
30 | 31 |
32 |

Content of responses from middlebox

33 |
34 |
35 |

Response number {{$index}}

36 |
39 |
40 |
41 |
42 |

Response number {{$index}} was empty

43 |
44 |
45 |
46 | 47 |
48 |
49 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/http-requests.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | This measurement contains data that could be a sign of network tampering or censorship. 6 |
7 |
8 | This measurement looks normal. 9 |
10 |
11 |
12 |

Website

13 | {{ report.input }} 14 |
15 | {{ experiment_failure }} 16 |
17 | {{ body_length_match }} 18 |
19 |

20 | View measurements for URL 21 |

22 |
23 |
24 | 25 |
26 |

HTTP Requests

27 |

This test tries to detect online censorship based on a comparison of HTTP requests over Tor and over the network of the user.

28 | 29 |

HTTP is a protocol which allows communication between a client and a server. It does so by handling a client's request to connect to a server, and a server's response to a client's request. Every time you connect to a website, your browser (the client) sends a request through the HTTP protocol to the server which is hosting that website. A server normally responds with the content of the website it is hosting. In some cases though, Internet Service Providers (ISP) prevent users from accessing certain websites by blocking or interfering with the connection between them and the server.

30 | 31 |

To detect such cases of censorship, we have developed a test which performs HTTP requests to given websites over the network of its user, and then over the Tor network. As Tor software is designed to circumvent censorship by making its user's traffic appear to come from a different part of the world, we have chosen to use the Tor network as a baseline for comparing HTTP requests to websites. If the two results match, then there is no clear sign of network interference; but if the results are different, then the website that the user is testing is likely censored.

32 | 33 |

If one of the following is present in the results, then there is a sign of network interference:

34 |
    35 |
  • The length of the body of the two websites (over Tor and over the user's network) differs by some percentage
  • 36 |
  • The HTTP request over the user's network fails
  • 37 |
  • The HTTP headers do not match
  • 38 |
39 |

40 |

41 | Note: False positives might occur when the Tor control connection is being discriminated by the server. This happens, for example, when a CloudFlare CAPTCHA page appears. 42 |

43 |
44 |
45 |
46 | 47 |

HTTP Response Headers

48 |
49 | View: 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 61 | 64 | 65 | 66 | 67 | 68 |
Header NameControlExperiment
{{ header_name }} 62 | 63 | {{ control.response.headers[header_name] }}{{ experiment.response.headers[header_name] }}
69 |
70 |
71 |

HTTP Response Body

72 |
76 |
77 |
expand ›
78 |
79 |
80 |
81 | 82 |
83 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/lantern-circumvention-tool-test.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ report.test_keys.success }} 5 |
6 | 7 |
8 |

Lantern Circumvention Tool

9 | 10 |

This test provides an automated way of examining whether Lantern works in a tested network.

11 | 12 |

Lantern is a centralized and 13 | peer-to-peer proxy, which is used as a circumvention tool. It detects whether 14 | websites are blocked and, if so, it allows you to access them via Lantern 15 | servers or via the network of Lantern users.

16 | 17 |

This test runs Lantern and checks to see if it is working. If it's 18 | able to connect to a Lantern server and reach a control website over it, 19 | then we consider that Lantern can be used for censorship circumvention 20 | within the tested network. If however the test is unable to connect 21 | to Lantern servers, then it is likely the case that they are blocked 22 | within the tested network.

23 | 24 |
25 | 26 |
27 |

Console standard output

28 |
31 |
32 | 33 |

Console error output

34 |
37 |
38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/meek-fronted-requests-test.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ report.test_keys.success }} 5 |
6 | 7 |
8 |

Meek Fronted Requests

9 | 10 |

This test examines whether the domains used by Meek (a type of Tor bridge) work in tested networks.

11 | 12 |

Meek is a pluggable transport which uses non-blocked domains, such as 13 | google.com, awsstatic.com (Amazon cloud infrastructure) and 14 | ajax.aspnetcdn.com (Microsoft azure cloud infrastructure), to proxy its users over 15 | Tor to blocked websites, while hiding 16 | both the fact that they are connecting to such websites and how they are 17 | connecting to them. As such, Meek is useful for not only connecting to websites 18 | that are blocked, but for also hiding which websites you are connecting to.

19 | 20 |

Below is a simplified explanation of how this works:

21 | 22 |

[user] → [https://www.google.com] → [Meek hosted on the cloud] → [Tor] → [blocked-website]

23 | 24 |

The user will receive a response (access to a blocked website, 25 | for example) from cloud-fronted domains, such as google.com, 26 | through the following way:

27 | 28 |

[blocked-website] → [Tor] → [Meek hosted on the cloud] → [https://www.google.com] → [user]

29 | 30 |

In short, this test does an encrypted connection to 31 | cloud-fronted domains over HTTPS and examines whether it can 32 | connect to them or not.

33 | 34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/multi-protocol-traceroute.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ report.test_keys.body_length_match }} 5 | {{ report.input }} 6 |
7 | 8 |
9 |

Multi-Protocol Traceroute

10 |

11 | Every time we connect to a server, our devices make requests which are encapsulated in a packet. A traceroute is a way of understanding which paths packets take in a network. OONI constructs packets in such a way that they perform a traceroute from multiple protocols and ports simultaneously, in an attempt to detect traffic manipulation. 12 |

13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/nettest.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/openvpn.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ report.test_keys.body_length_match }} 5 | {{ report.input }} 6 |
7 | 8 |
9 |

OpenVPN

10 | 11 |

This test provides an automated way of examining whether OpenVPN works in a tested network.

12 | 13 |

OpenVPN is an open source 14 | application VPN (Virtual Private Network) protocol that allows a user to send and receive 15 | network data as if they were connected directly to another private network. 16 | As such it is commonly used for censorship circumvention purposes. 17 |

18 | 19 |

This test runs OpenVPN and checks to see if it is working. If it's able to 20 | connect to an OpenVPN server, then we consider that OpenVPN can be used for 21 | censorship circumvention within the tested network. If however the test is 22 | unable to connect to OpenVPN servers, then it is likely the case that they are 23 | blocked within the tested network.

24 | 25 |
26 |
27 |
28 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/psiphon-test.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ report.test_keys.request_success }} 5 |
6 | {{ report.test_keys.bootstrapped_success }} 7 |
8 | 9 |
10 |

Psiphon

11 | 12 |

This test provides an automated way of examining whether Psiphon works in a tested network.

13 | 14 |

Psiphon is a free and open 15 | source tool that utilises SSH, VPN and HTTP proxy technology for 16 | censorship circumvention.

17 | 18 |

This test runs Psiphon and checks to see if it is working. If 19 | it's able to connect to a Psiphon server and reach a website over it, 20 | then we consider that Psiphon can be used for censorship 21 | circumvention within the tested network. If however the test is 22 | unable to connect to Psiphon servers, then it is likely the case 23 | that they are blocked within the tested network.

24 | 25 |
26 | 27 |
28 |

Console standard output

29 |
32 |
33 | 34 |

Console error output

35 |
38 |
39 |
40 | 41 | 42 |
43 |
44 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/tcp-connect.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Details for this specific test:

5 | 6 | 7 | 11 | 12 | 15 | 16 | 19 | 20 | 23 | 24 | 25 |
26 |
27 | 28 |
29 |

TCP Connect

30 |

This test attempts to connect to a particular endpoint, such as 31 | a website, and it sees if it's able to connect. If it's not able 32 | to, then that's a sign of network tampering. 33 |

34 | 35 |

36 | Note: False positives can occur when the endpoint is down. 37 |

38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /client/ngapp/views/nettests/web-connectivity.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | This measurement contains data that could be a sign of network tampering or censorship. 6 |
7 |
8 | This measurement looks normal. 9 |
10 |
11 |
12 |

Website

13 | {{ report.input }} 14 |
15 | {{ report.test_keys.blocking }} 16 |
17 | 18 |

19 | View measurements for URL 20 |

21 |
22 |
23 | 24 |
25 |

Web Connectivity

26 |

This test examines whether websites are reachable and if they are not, it 27 | attempts to determine whether access to them is blocked through DNS tampering, 28 | TCP connection RST/IP blocking or by a transparent HTTP proxy.

29 | 30 |

Specifically, this test is designed to perform the following:

31 | 32 |
    33 |
  • Resolver identification
  • 34 | 35 |
  • DNS lookup
  • 36 | 37 |
  • TCP connect
  • 38 | 39 |
  • HTTP GET request
  • 40 |
41 | 42 |

By default, this test performs the above (excluding the first step, which is 43 | performed only over the network of the user) both over a control server and over 44 | the network of the user. If the results from both networks match, then there is 45 | no clear sign of network interference; but if the results are different, then 46 | the websites that the user is testing are likely censored.

47 | 48 |

Below we provide information about how each step performed under the web 49 | connectivity test works.

50 | 51 |

1. Resolver identification

52 | 53 |

The domain name system (DNS) is what is responsible for transforming a host name 54 | (e.g. torproject.org) into an IP address (e.g. 38.229.72.16). Internet Service 55 | Providers, amongst others, run DNS resolvers which map IP addresses to host 56 | names. In some circumstances though, ISPs map the requested host names to the 57 | wrong IP addresses, which is a form of tampering.

58 | 59 |

As a first step, the web connectivity test attempts to identify which DNS 60 | resolver is being used by the user. It does so by performing a DNS query to 61 | special domains (such as whoami.akamai.com) which will disclose the IP address 62 | of the resolver.

63 | 64 |

2. DNS lookup

65 | 66 |

Once the web connectivity test has identified the DNS resolver of the user, it 67 | then attempts to identify which addresses and are mapped to the tested host 68 | names by the resolver. It does so by performing a DNS lookup, which asks the 69 | resolver to disclose which IP addresses are mapped to the tested host names, as 70 | well as which other host names are linked to the tested host names under DNS 71 | queries.

72 | 73 |

3. TCP connect

74 | 75 |

The web connectivity test will then try to connect to the tested websites by 76 | attempting to establish a TCP session on port 80 (or port 443 for URLs that 77 | begin with HTTPS) for the list of IP addresses that were identified in the 78 | previous step (DNS lookup).

79 | 80 |

4. HTTP GET request

81 | 82 |

As the web connectivity test connects to tested websites (through the previous 83 | step), it sends requests through the HTTP protocol to the servers which are 84 | hosting those websites. A server normally responds to an HTTP GET request with 85 | the content of the webpage that is requested.

86 | 87 |

Comparison of results: Identifying censorship

88 | 89 |

Once the above steps of the web connectivity test are performed *both* over a 90 | control server and over the network of the user, the collected results are then 91 | compared with the aim of identifying whether and how tested websites are 92 | tampered with. If the compared results do *not* match, then there is a sign of 93 | network interference.

94 | 95 |

Below are the conditions under which the following types of blocking are 96 | identified:

97 | 98 |
    99 |
  • DNS blocking: If the DNS responses (such as the IP addresses mapped to 100 | host names) do not match
  • 101 | 102 |
  • TCP/IP blocking: If a TCP session to connect to websites was not 103 | established over the network of the user
  • 104 | 105 |
  • HTTP blocking: If the HTTP request over the user's network failed, or the
  • 106 | 107 |
  • HTTP status codes don't match, or all of the following apply: 108 |
      109 |
    • The body length of compared websites (over the control server and the 110 | network of the user) differs by some percentage
    • 111 |
    • The HTTP headers names do not match
    • 112 |
    • The HTML title tags do not match
    • 113 |
    114 |
  • 115 | 116 |

    The examples below (testing piratebay.se and google.com for censorship in Italy) show 117 | what the output of the web connectivity test could look like:

    118 | 119 |

    Note: DNS resolvers, such as Google or your local ISP, often provide users 120 | with IP addresses that are closest to them geographically. Often this is not 121 | done with the intent of network tampering, but merely for the purpose of 122 | providing users faster access to websites. As a result, some false positives 123 | might arise in OONI measurements. Other false positives might occur when tested 124 | websites serve different content depending on the country that the user is 125 | connecting from, or in the cases when websites return failures even though they 126 | are not tampered with. 127 |

    128 |
129 |
130 |
131 | 132 |
133 |

DNS query answers

134 | Resolver: {{ report.test_keys.client_resolver }} 135 |

Answers

136 | 141 |
142 | 143 |
144 |

HTTP Response Body

145 |
149 |
150 |
expand ›
151 |
152 | 153 |
154 |

TCP Connect results

155 | 168 |
169 | 170 | 171 | 172 |
173 | -------------------------------------------------------------------------------- /client/ngapp/views/view-measurement.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |

404 - Report Not Found

5 |

This can either mean that the report you submitted has not yet been 6 | processed or that it was not submitted successfully. Please try again later.

7 |

Go Back Home

8 |
9 | 10 |
11 |
12 |

13 | 15 | 16 |

17 | Back to Country 18 |
19 |
20 |
21 |
22 | Measurement 23 |

24 | {{ nettest.long_name }} 25 | {{report.measurement_start_time | date : "yyyy-MM-dd HH:mm:ss UTC" : 'UTC' }} 26 |

27 |

ID: {{measurementId}}

28 |

Test runtime: {{report.test_runtime}} seconds

29 | 30 |
31 | Probe 32 |
33 |
34 | 39 |
40 |
41 | 46 |
47 |
48 | 53 |
54 |
55 |
56 |
57 | 58 |
61 |

Inspect Raw Measurement Data:

62 | 63 | 64 | 65 |
66 |
67 |
68 |
69 | -------------------------------------------------------------------------------- /client/ngapp/views/website-view.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | 6 |

7 |
8 |
9 |

Category:

10 |

We started measuring this URL on . 11 |

12 |

13 | The URL was suggested for measuring by . 14 |

15 |
16 |
17 |

This URL is blocked in:

18 |
  • 19 | {{ obj.country | lowercase }} 20 |
  • 21 |
    22 |
    23 |

    Anomalous measurements Run on the URL:

    24 |
      25 |
    • 26 |

      {{ key }}

      27 |
        28 | 29 |
      • 30 | {{ measurement.test_start_time | date:'shortDate' }}: view » 31 |
      • 32 |
      33 |
    • 34 |
    35 |
    36 |
    37 |
    38 | -------------------------------------------------------------------------------- /client/ngapp/views/world.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 | 6 |
    7 |
    8 | Measurements 9 |
    10 |
    11 | More than 100,000 12 |
    13 |
    14 | More than 10,000 15 |
    16 |
    17 | Less than 10,000 18 |
    19 |
    20 | Discoveries 21 |
    22 | 25 |
    26 | Vendors identified 27 |
    28 |
    29 |
    30 |
    31 |
    32 |
    33 |
    34 | 35 |
    36 | 37 |
    38 |

    Country List

    39 |

    40 | Browse countries to find the one you want to investigate measurements for 41 |

    42 |
    43 |
    44 | 45 |
    46 | -------------------------------------------------------------------------------- /common/models/censorship_method.js: -------------------------------------------------------------------------------- 1 | module.exports = function(CensorshipMethod) { 2 | 3 | }; 4 | -------------------------------------------------------------------------------- /common/models/censorship_method.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "censorship_method", 3 | "plural": "censorship_methods", 4 | "base": "PersistedModel", 5 | "idInjection": false, 6 | "options": { 7 | "validateUpsert": true, 8 | "postgresql": { 9 | "table": "censorship_methods" 10 | } 11 | }, 12 | "properties": { 13 | "id": { 14 | "type": "Number", 15 | "id": true 16 | }, 17 | "name": { 18 | "type": "String" 19 | }, 20 | "description": { 21 | "type": "String" 22 | } 23 | }, 24 | "validations": [], 25 | "relations": {}, 26 | "acls": [], 27 | "methods": {} 28 | } 29 | -------------------------------------------------------------------------------- /common/models/country.js: -------------------------------------------------------------------------------- 1 | module.exports = function(Country) { 2 | 3 | }; 4 | -------------------------------------------------------------------------------- /common/models/country.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "country", 3 | "plural": "countries", 4 | "base": "PersistedModel", 5 | "idInjection": false, 6 | "options": { 7 | "validateUpsert": true 8 | }, 9 | "properties": { 10 | "id": { 11 | "type": "Number", 12 | "id": true 13 | }, 14 | "name": { 15 | "type": "String" 16 | }, 17 | "iso_alpha2": { 18 | "type": "String" 19 | }, 20 | "iso_alpha3": { 21 | "type": "String" 22 | } 23 | }, 24 | "validations": [], 25 | "relations": { 26 | "censorship_methods": { 27 | "type": "hasAndBelongsToMany", 28 | "model": "censorship_method", 29 | "foreignKey": "" 30 | } 31 | }, 32 | "acls": [], 33 | "methods": {} 34 | } 35 | -------------------------------------------------------------------------------- /common/models/nettest.js: -------------------------------------------------------------------------------- 1 | module.exports = function(Nettest) { 2 | 3 | }; 4 | -------------------------------------------------------------------------------- /common/models/nettest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nettest", 3 | "plural": "nettests", 4 | "base": "PersistedModel", 5 | "idInjection": true, 6 | "options": { 7 | "validateUpsert": true 8 | }, 9 | "properties": { 10 | "long_name": { 11 | "type": "string", 12 | "required": true 13 | }, 14 | "name": { 15 | "type": "string", 16 | "required": true 17 | }, 18 | "description": { 19 | "type": "string", 20 | "required": true 21 | }, 22 | "spec_url": { 23 | "type": "string", 24 | "required": true 25 | } 26 | }, 27 | "validations": [], 28 | "relations": {}, 29 | "acls": [], 30 | "methods": {} 31 | } 32 | -------------------------------------------------------------------------------- /common/models/report.js: -------------------------------------------------------------------------------- 1 | var axios = require('axios') 2 | var qs = require('qs') 3 | 4 | var countries = require('country-data').countries 5 | 6 | var apiClient = axios.create({ 7 | baseURL: 'https://api.ooni.io/api/', // yes, that's hardcoded 8 | timeout: 90000, // Maybe set this lower once performance is boosted 9 | }) 10 | 11 | module.exports = function(Report) { 12 | 13 | Report.findMeasurements = function(probe_cc, input, order, page_number, 14 | page_size, since, until, test_name, report_id, callback) { 15 | var apiQuery = {} 16 | if (probe_cc) { 17 | apiQuery.probe_cc = probe_cc 18 | } 19 | if (input) { 20 | apiQuery.input = input 21 | } 22 | if (report_id) { 23 | apiQuery.report_id = report_id 24 | } 25 | if (test_name) { 26 | apiQuery.test_name = test_name 27 | } 28 | 29 | if (order) { 30 | apiQuery.order_by = order.split(' ')[0] 31 | apiQuery.order = order.split(' ')[1] 32 | } 33 | 34 | if (page_number && page_size) { 35 | apiQuery.offset = page_number * page_size 36 | apiQuery.limit = page_size 37 | } 38 | if (since) { 39 | apiQuery.since= since 40 | } 41 | if (until) { 42 | apiQuery.until = until 43 | } 44 | 45 | apiClient.get(`/v1/measurements?${qs.stringify(apiQuery)}`) 46 | .then(function(response) { 47 | // This is a workaround 48 | callback(null, response.data.results.map(function(row) { 49 | row['test_start_time'] = row['measurement_start_time'] 50 | return row 51 | })) 52 | }) 53 | .catch(function(error) { 54 | callback(error, null); 55 | }) 56 | } 57 | Report.remoteMethod( 58 | 'findMeasurements', 59 | { http: { verb: 'get' }, 60 | description: 'Returns the list of measurements matching the query', 61 | accepts: [ 62 | {arg: 'probe_cc', type: 'string'}, 63 | {arg: 'input', type: 'string'}, 64 | {arg: 'order', type: 'string'}, 65 | {arg: 'page_number', type: 'string'}, 66 | {arg: 'page_size', type: 'string'}, 67 | {arg: 'since', type: 'string'}, 68 | {arg: 'until', type: 'string'}, 69 | {arg: 'test_name', type: 'string'}, 70 | {arg: 'report_id', type: 'string'} 71 | ], 72 | returns: {arg: 'data', type: ['Object'], root: true} 73 | } 74 | ); 75 | 76 | 77 | Report.blockpageList = function(probe_cc, callback) { 78 | var apiQuery = { 79 | probe_cc: probe_cc 80 | } 81 | apiClient.get(`/_/blockpages?${qs.stringify(apiQuery)}`) 82 | .then(function(response) { 83 | callback(null, response.data.results) 84 | }) 85 | .catch(function(error) { 86 | callback(error, null); 87 | }) 88 | } 89 | 90 | Report.remoteMethod( 91 | 'blockpageList', 92 | { http: { verb: 'get' }, 93 | description: 'Returns the list of URLs that appear to be blocked in a given country', 94 | accepts: {arg: 'probe_cc', type: 'string'}, 95 | returns: {arg: 'data', type: ['Object'], root: true} 96 | } 97 | ); 98 | 99 | Report.total = function(callback) { 100 | apiClient.get(`/_/measurement_count_total`) 101 | .then(function(response) { 102 | callback(null, response.data) 103 | }) 104 | .catch(function(error) { 105 | callback(error, null); 106 | }) 107 | } 108 | 109 | Report.remoteMethod( 110 | 'total', 111 | { http: { verb: 'get' }, 112 | description: 'Returns the total number of measurements collected', 113 | returns: {arg: 'data', type: 'Object', root: true} 114 | } 115 | ); 116 | 117 | Report.websiteDetails = function (website_url, callback) { 118 | var ds = Report.dataSource 119 | var wildcard_url = '%' + website_url 120 | 121 | var sql = 'SELECT * FROM domains WHERE url LIKE $1' 122 | ds.connector.query(sql, [wildcard_url], callback) 123 | } 124 | 125 | Report.remoteMethod( 126 | 'websiteDetails', 127 | { http: { verb: 'get' }, 128 | description: 'Returns website details', 129 | accepts: {arg: 'website_url', type: 'string'}, 130 | returns: {arg: 'data', type: ['Object'], root: true} 131 | } 132 | ) 133 | 134 | Report.asnName = function (asn, callback) { 135 | var ds = Report.dataSource 136 | 137 | var sql = 'SELECT name FROM asns WHERE asn = $1' 138 | ds.connector.query(sql, [asn], callback) 139 | } 140 | 141 | Report.remoteMethod( 142 | 'asnName', 143 | { http: { verb: 'get' }, 144 | description: 'Returns ASN name', 145 | accepts: {arg: 'asn', type: 'string'}, 146 | returns: {arg: 'data', type: ['Object'], root: true} 147 | } 148 | ) 149 | 150 | Report.websiteMeasurements = function (website_url, callback) { 151 | var apiQuery = { 152 | input: website_url 153 | } 154 | apiClient.get(`/_/website_measurements?${qs.stringify(apiQuery)}`) 155 | .then(function(response) { 156 | callback(null, response.data.results) 157 | }) 158 | .catch(function(error) { 159 | callback(error, null); 160 | }) 161 | } 162 | 163 | Report.remoteMethod( 164 | 'websiteMeasurements', 165 | { http: { verb: 'get' }, 166 | description: 'Returns website\'s measurements', 167 | accepts: {arg: 'website_url', type: 'string'}, 168 | returns: {arg: 'data', type: ['Object'], root: true} 169 | } 170 | ) 171 | 172 | Report.vendors = function(probe_cc, callback) { 173 | var ds = Report.dataSource; 174 | var sql = "SELECT * FROM identified_vendors"; 175 | 176 | if (typeof(probe_cc) !== "undefined"){ 177 | sql += " WHERE probe_cc = $1"; 178 | ds.connector.query(sql, [probe_cc], callback); 179 | } else { 180 | ds.connector.query(sql, callback); 181 | } 182 | 183 | } 184 | 185 | Report.remoteMethod( 186 | 'vendors', 187 | { http: { verb: 'get' }, 188 | description: 'Returns the identified vendors of censorship and surveillance equipment', 189 | accepts: {arg: 'probe_cc', type: 'string'}, 190 | returns: {arg: 'data', type: ['Object'], root: true} 191 | } 192 | ); 193 | 194 | Report.blockpageDetected = function(callback) { 195 | apiClient.get(`/_/blockpage_detected`) 196 | .then(function(response) { 197 | callback(null, response.data.results) 198 | }) 199 | .catch(function(error) { 200 | callback(error, null); 201 | }) 202 | } 203 | 204 | Report.remoteMethod( 205 | 'blockpageDetected', 206 | { http: { verb: 'get' }, 207 | description: 'Returns the country codes where we detected the presence of a blockpage', 208 | returns: { arg: 'data', type: ['Object'], root: true } 209 | } 210 | ); 211 | 212 | Report.blockpageCount = function(probe_cc, callback) { 213 | var apiQuery = { 214 | probe_cc: probe_cc 215 | } 216 | apiClient.get(`/_/blockpage_count?${qs.stringify(apiQuery)}`) 217 | .then(function(response) { 218 | callback(null, response.data.results) 219 | }) 220 | .catch(function(error) { 221 | callback(error, null); 222 | }) 223 | } 224 | 225 | Report.remoteMethod( 226 | 'blockpageCount', 227 | { http: { verb: 'get' }, 228 | description: 'Returns the number of blockpages detected per total', 229 | accepts: {arg: 'probe_cc', type: 'string'}, 230 | returns: { arg: 'data', type: ['Object'], root: true } 231 | } 232 | ); 233 | 234 | Report.countByCountry = function(callback) { 235 | apiClient.get(`/_/measurement_count_by_country`) 236 | .then(function(response) { 237 | var result = []; 238 | response.data.results.forEach(function(row) { 239 | var country = countries[row['probe_cc']]; 240 | if (country !== undefined) { 241 | result.push({ 242 | name: country.name, 243 | alpha3: country.alpha3, 244 | alpha2: country.alpha2, 245 | count: row['count'] 246 | }); 247 | } 248 | }); 249 | callback(null, result) 250 | }) 251 | .catch(function(error) { 252 | callback(error, null); 253 | }) 254 | } 255 | 256 | Report.remoteMethod( 257 | 'countByCountry', 258 | { http: { verb: 'get' }, 259 | description: 'Get number of reports by country code', 260 | accepts: [], 261 | returns: { arg: 'data', type: ['Object'], root: true } 262 | } 263 | ); 264 | }; 265 | -------------------------------------------------------------------------------- /common/models/report.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "report", 3 | "base": "PersistedModel", 4 | "idInjection": false, 5 | "options": { 6 | "validateUpsert": true, 7 | "postgresql": { 8 | "table": "metrics" 9 | } 10 | }, 11 | "properties": { 12 | "id": { 13 | "type": "String", 14 | "postgresql": { 15 | "columnName": "report_id" 16 | } 17 | }, 18 | "input": { 19 | "type": "String" 20 | }, 21 | "options": { 22 | "type": "Object" 23 | }, 24 | "probe_asn": { 25 | "type": "String" 26 | }, 27 | "probe_cc": { 28 | "type": "String", 29 | "required": true 30 | }, 31 | "probe_ip": { 32 | "type": "String" 33 | }, 34 | "software_name": { 35 | "type": "String" 36 | }, 37 | "software_version": { 38 | "type": "String" 39 | }, 40 | "report_filename": { 41 | "type": "String" 42 | }, 43 | "test_start_time": { 44 | "type": "Date" 45 | }, 46 | "test_runtime": { 47 | "type": "Number" 48 | }, 49 | "measurement_start_time": { 50 | "type": "Date" 51 | }, 52 | "test_name": { 53 | "type": "String", 54 | "required": true 55 | }, 56 | "data_format_version": { 57 | "type": "String" 58 | }, 59 | "test_helpers": { 60 | "type": "Object" 61 | }, 62 | "test_keys": { 63 | "type": "Object" 64 | } 65 | }, 66 | "validations": [], 67 | "relations": { 68 | "country": { 69 | "type": "belongsTo", 70 | "model": "country", 71 | "foreignKey": "probe_cc" 72 | } 73 | }, 74 | "acls": [], 75 | "methods": {} 76 | } 77 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script used to deploy to heroku 3 | set -e 4 | 5 | git branch -D deploy || echo "First deployment" 6 | git checkout -b deploy 7 | grunt build:production 8 | git add -f client/dist/ 9 | git commit -m "Deploying to Heroku" 10 | git push heroku -f deploy:master 11 | git checkout master 12 | -------------------------------------------------------------------------------- /docs/deployment.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | The production deployment relies on systemd for running the node service and is 4 | fronted by nginx. 5 | 6 | The requirements for deploying ooni-explorer in production are the same as those 7 | for running it in a development environment. 8 | 9 | You will need to have installed recent versions of: 10 | 11 | * [Node with npm](https://nodejs.org/en/download/) 12 | 13 | * [Bower](http://bower.io/#install-bower) 14 | 15 | Once these are installed you should setup a user to run the explorer as. 16 | 17 | You should clone the repository of the explorer in the home of the user: 18 | 19 | ``` 20 | git clone https://github.com/TheTorProject/ooni-explorer.git 21 | ``` 22 | 23 | Install the node and bower requirements: 24 | 25 | ``` 26 | bower install 27 | npm install 28 | ``` 29 | 30 | Edit the `server/datasources.production.js` to include the details of your 31 | database. 32 | 33 | To build the production version of the application run: 34 | 35 | ``` 36 | grunt build 37 | ``` 38 | 39 | Then configure a systemd service with a file like this (assuming the user is 40 | called ooni-explorer): 41 | 42 | `$ cat /etc/systemd/system/node-ooni-explorer.service` 43 | ``` 44 | [Service] 45 | ExecStart=/usr/bin/node /home/ooni-explorer/ooni-explorer/server.js 46 | Restart=always 47 | StandardOutput=syslog 48 | StandardError=syslog 49 | SyslogIdentifier=node-ooni-explorer 50 | User=ooni-explorer 51 | Group=ooni-explorer 52 | Environment=NODE_ENV=production 53 | 54 | [Install] 55 | WantedBy=multi-user.target 56 | ``` 57 | 58 | By default ooni-explorer will bind on port 3000, so you can either use nginx to 59 | proxy connection to port 3000 (recommended) or use an iptables rule if you don't 60 | need TLS. 61 | 62 | This is how the nginx configuration should look like 63 | (`/etc/nginx/sites-enabled/ooni-explorer`): 64 | 65 | ``` 66 | server { 67 | listen 80; 68 | server_name explorer.ooni.torproject.org explorer.ooni.io; 69 | 70 | location / { 71 | proxy_pass http://127.0.0.1:3000; 72 | proxy_http_version 1.1; 73 | proxy_set_header Upgrade $http_upgrade; 74 | proxy_set_header Connection 'upgrade'; 75 | proxy_set_header Host $host; 76 | proxy_cache_bypass $http_upgrade; 77 | } 78 | } 79 | server { 80 | # SSL configuration 81 | listen 443 ssl; 82 | server_name explorer.ooni.torproject.org explorer.ooni.io; 83 | ssl_certificate /path/to/fullchain.pem; 84 | ssl_certificate_key /path/to/privkey.pem; 85 | 86 | location / { 87 | proxy_pass http://127.0.0.1:3000; 88 | proxy_http_version 1.1; 89 | proxy_set_header Upgrade $http_upgrade; 90 | proxy_set_header Connection 'upgrade'; 91 | proxy_set_header Host $host; 92 | proxy_cache_bypass $http_upgrade; 93 | } 94 | } 95 | ``` 96 | 97 | # Starting and stopping 98 | 99 | To start the service run: 100 | 101 | ``` 102 | service node-ooni-explorer start 103 | ``` 104 | 105 | To stop the service run: 106 | 107 | ``` 108 | service node-ooni-explorer stop 109 | ``` 110 | 111 | # Updating 112 | 113 | ``` 114 | cd /home/ooni-explorer/ooni-explorer/ 115 | git pull 116 | grunt build 117 | service node-ooni-explorer restart 118 | ``` 119 | -------------------------------------------------------------------------------- /global-config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Global configuration shared by components. 3 | */ 4 | 5 | var url = require('url'); 6 | 7 | var conf = { 8 | hostname: 'localhost', 9 | port: 3000, 10 | restApiRoot: '/api', // The path where to mount the REST API app 11 | legacyExplorer: false 12 | }; 13 | 14 | // The URL where the browser client can access the REST API is available. 15 | // Replace with a full url (including hostname) if your client is being 16 | // served from a different server than your REST API. 17 | conf.restApiUrl = url.format({ 18 | protocol: 'http', 19 | slashes: true, 20 | hostname: conf.hostname, 21 | port: conf.port, 22 | pathname: conf.restApiRoot 23 | }); 24 | 25 | module.exports = conf; 26 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | print_usage () { ###1 3 | echo "Valid options are OS={debian,centos,osx} " 4 | echo "For example if you are running CentOS you can try:" 5 | printf '\tOS=centos sh install.sh' 6 | } 7 | npm_install () { ###1 8 | NPM_GLOBAL_DEPENDENCIES='jshint bower grunt-cli strongloop' 9 | 10 | echo "Installing npm global dependencies..." 11 | npm install -g $NPM_GLOBAL_DEPENDENCIES || sudo npm install -g $NPM_GLOBAL_DEPENDENCIES 12 | 13 | echo "Installing npm development dependencies..." 14 | npm install --development 15 | } 16 | ###1 Parse arguments 17 | while [ $# -gt 0 ]; do 18 | case "$1" in 19 | -h|--help) 20 | print_usage 21 | exit 0 22 | ;; 23 | -d|--debug) 24 | set -xe 25 | ;; 26 | esac 27 | shift 28 | done 29 | 30 | ###1 Determine the OS 31 | case "$OS" in 32 | "debian"|"centos"|"osx") 33 | # accept manually set values 34 | ;; 35 | *) 36 | [ -n "$OS" ] && >&2 echo "Ignoring invalid OS=${OS}" 37 | if type uname >/dev/null 2>/dev/null; then 38 | case "$(uname)" in 39 | Darwin) 40 | OS="osx" 41 | ;; 42 | Linux) 43 | if type lsb_release >/dev/null 2>/dev/null; then 44 | # n.b.: lsb_release is not a default package 45 | case "$(lsb_release -i)" in 46 | *Debian) 47 | OS="debian" 48 | ;; 49 | *Centos) 50 | OS="centos" 51 | ;; 52 | esac 53 | elif [ -f "/etc/redhat-release" || -f "/etc/centos-release" ]; then 54 | OS="centos" 55 | elif [ -f "/etc/os-release" ]; then 56 | if grep debian /etc/os-release >/dev/null 2>/dev/null; then 57 | OS="debian" 58 | fi 59 | fi 60 | ;; 61 | esac 62 | fi 63 | ;; 64 | esac 65 | 66 | ###1 Installation action 67 | case "$OS" in 68 | "debian") 69 | sudo apt-get update 70 | 71 | echo "Installing build-essential..." 72 | sudo apt-get install -y build-essential 73 | 74 | echo "Installing Node..." 75 | sudo apt-get install -y npm nodejs nodejs-legacy 76 | 77 | npm_install 78 | ;; 79 | "centos") 80 | # This is tested on CentOS 7 81 | 82 | echo "Installing Node..." 83 | curl --silent --location https://rpm.nodesource.com/setup > install_node.sh 84 | chmod +x install_node.sh 85 | # XXX this is super ghetto 86 | sudo ./install_node.sh 87 | 88 | echo "Installing make dependencies" 89 | sudo yum install -y gcc-c++ make nodejs 90 | 91 | npm_install 92 | ;; 93 | "osx") 94 | if ! type node >/dev/null 2>/dev/null; then 95 | echo "Node not found. Installing node..." 96 | if ! type brew >/dev/null 2>/dev/null; then 97 | echo "This install script relies on Homebrew and it doesn't seem to be installed." 98 | printf "http://brew.sh\n\n" 99 | print_usage 100 | exit 4 101 | fi 102 | brew install node 103 | fi 104 | 105 | if ! type make >/dev/null 2>/dev/null; then 106 | echo "make doesn't seem to be installed" 107 | echo "You may want to install make and gcc with XCode or Homebrew." 108 | exit 5 109 | fi 110 | 111 | npm_install 112 | ;; 113 | *) 114 | echo "Unable to detect a supported operating system." 115 | print_usage 116 | exit 99 117 | ;; 118 | esac 119 | # vim: set ft=sh tw=0 ts=2 sw=2 sts=2 fdm=marker fmr=###,### et: 120 | -------------------------------------------------------------------------------- /ooni-api-spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "OONI Pipeline API", 6 | "description": "This is the main API to access the OONI data pieline", 7 | "termsOfService": "http://api.ooni.io/tos.md", 8 | "license": { 9 | "name": "MIT" 10 | } 11 | }, 12 | "host": "api.ooni.io", 13 | "basePath": "/api", 14 | "schemes": [ 15 | "http" 16 | ], 17 | "consumes": [ 18 | "application/json" 19 | ], 20 | "produces": [ 21 | "application/json" 22 | ], 23 | "paths": { 24 | "/reports": { 25 | "get": { 26 | "description": "Returns all the reports that have been collected", 27 | "operationId": "findReports", 28 | "produces": [ 29 | "application/json", 30 | "application/xml", 31 | "text/xml", 32 | "text/html" 33 | ], 34 | "parameters": [ 35 | { 36 | "name": "country_code", 37 | "in": "query", 38 | "description": "country codes to filter by", 39 | "required": false, 40 | "type": "array", 41 | "items": { 42 | "type": "string" 43 | }, 44 | "collectionFormat": "csv" 45 | }, 46 | { 47 | "name": "limit", 48 | "in": "query", 49 | "description": "maximum number of results to return", 50 | "required": false, 51 | "type": "integer", 52 | "format": "int32" 53 | } 54 | ], 55 | "responses": { 56 | "200": { 57 | "description": "report response", 58 | "schema": { 59 | "type": "array", 60 | "items": { 61 | "$ref": "#/definitions/Report" 62 | } 63 | } 64 | }, 65 | "default": { 66 | "description": "unexpected error", 67 | "schema": { 68 | "$ref": "#/definitions/ErrorModel" 69 | } 70 | } 71 | } 72 | } 73 | }, 74 | "/reports/{id}": { 75 | "get": { 76 | "description": "Returns a the report that matches the specified ID", 77 | "operationId": "findReportById", 78 | "produces": [ 79 | "application/json", 80 | "application/xml", 81 | "text/xml", 82 | "text/html" 83 | ], 84 | "parameters": [ 85 | { 86 | "name": "id", 87 | "in": "path", 88 | "description": "ID of report to fetch", 89 | "required": true, 90 | "type": "string" 91 | } 92 | ], 93 | "responses": { 94 | "200": { 95 | "description": "report response", 96 | "schema": { 97 | "$ref": "#/definitions/Report" 98 | } 99 | }, 100 | "default": { 101 | "description": "unexpected error", 102 | "schema": { 103 | "$ref": "#/definitions/ErrorModel" 104 | } 105 | } 106 | } 107 | } 108 | } 109 | }, 110 | "definitions": { 111 | "Report": { 112 | "required": [ 113 | "probe_cc", 114 | "report_id", 115 | "test_name" 116 | ], 117 | "properties": { 118 | "backend_version": { 119 | "type": "string" 120 | }, 121 | "input_hashes": { 122 | "type": "array", "items": "string" 123 | }, 124 | "options": { 125 | "type": "string" 126 | }, 127 | "probe_asn": { 128 | "type": "string" 129 | }, 130 | "probe_cc": { 131 | "type": "string" 132 | }, 133 | "probe_ip": { 134 | "type": "string" 135 | }, 136 | "record_type": { 137 | "type": "string" 138 | }, 139 | "report_filename": { 140 | "type": "string" 141 | }, 142 | "report_id": { 143 | "type": "string" 144 | }, 145 | "software_name": { 146 | "type": "string" 147 | }, 148 | "software_version": { 149 | "type": "string" 150 | }, 151 | "start_time": { 152 | "type": "string" 153 | }, 154 | "test_name": { 155 | "type": "string" 156 | }, 157 | "test_version": { 158 | "type": "string" 159 | }, 160 | "data_format_version": { 161 | "type": "string" 162 | }, 163 | "test_helpers": { 164 | "type": "string" 165 | } 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ooni-explorer", 3 | "version": "1.0.0", 4 | "license": "BSD-3-Clause", 5 | "main": "server/server.js", 6 | "scripts": { 7 | "pretest": "jshint .", 8 | "test": "grunt test", 9 | "build": "grunt build", 10 | "start": "node server.js", 11 | "postinstall": "node -e \"try { require('fs').symlinkSync(require('path').resolve('node_modules/@bower_components'), 'bower_components', 'junction') } catch (e) { }\"" 12 | }, 13 | "dependencies": { 14 | "@bower_components/angular": "angular/bower-angular#~1.4", 15 | "@bower_components/angular-datamaps": "git://github.com/dmachat/angular-datamaps.git#~0.1.0", 16 | "@bower_components/angular-daterangepicker": "git://github.com/fragaria/angular-daterangepicker.git#~0.2.2", 17 | "@bower_components/angular-inview": "thenikso/angular-inview#~1.5.6", 18 | "@bower_components/angular-mocks": "angular/bower-angular-mocks#~1.4", 19 | "@bower_components/angular-resource": "angular/bower-angular-resource#~1.4", 20 | "@bower_components/angular-route": "angular/bower-angular-route#~1.4", 21 | "@bower_components/angular-scenario": "angular/bower-angular-scenario#~1.4", 22 | "@bower_components/angular-typewrite": "antoniocapelo/Angular-Typewrite#~0.0.14", 23 | "@bower_components/angular-ui-codemirror": "git://github.com/angular-ui/ui-codemirror.git#~0.3.0", 24 | "@bower_components/angular-ui-grid": "git://github.com/angular-ui/bower-ui-grid.git#~3.0.0-rc.21", 25 | "@bower_components/bootstrap": "git://github.com/twbs/bootstrap.git#^3.0.0", 26 | "@bower_components/bootstrap-daterangepicker": "dangrossman/bootstrap-daterangepicker#^2.0.0", 27 | "@bower_components/codemirror": "https://registry.yarnpkg.com/codemirror/-/codemirror-5.22.0.tgz", 28 | "@bower_components/colorbrewer": "git://github.com/undashes/colorbrewer.git#~1.0.0", 29 | "@bower_components/d3": "mbostock-bower/d3-bower#^3.5.6", 30 | "@bower_components/datamaps": "markmarkoh/datamaps#~0.4.0", 31 | "@bower_components/es5-shim": "git://github.com/es-shims/es5-shim.git#~3.1.0", 32 | "@bower_components/factbook-country-data": "git://github.com/simonv3/factbook-country-data#0cceccd6e393e6a860798e0b7e5be28c4f18efd4", 33 | "@bower_components/flag-icon-css": "lipis/flag-icon-css#*", 34 | "@bower_components/font-awesome": "git://github.com/FortAwesome/Font-Awesome.git#~4.5.0", 35 | "@bower_components/iso-3166-country-codes-angular": "git://github.com/hellais/iso-3166-country-codes-angular#611d0cf4dc2244a74cd337c516744d1dce235e7c", 36 | "@bower_components/jquery": "jquery/jquery-dist#1.9.1 - 2", 37 | "@bower_components/json-formatter": "git://github.com/mohsen1/json-formatter.git#~0.4.2", 38 | "@bower_components/json3": "git://github.com/bestiejs/json3.git#~3.3.1", 39 | "@bower_components/moment": "moment/moment#>=2.9.0", 40 | "@bower_components/topojson": "git://github.com/mbostock/topojson.git#1.6.20", 41 | "async": "~2.1.4", 42 | "axios": "^0.16.2", 43 | "compression": "^1.0.3", 44 | "cors": "^2.5.2", 45 | "country-data": "0.0.31", 46 | "errorhandler": "^1.1.1", 47 | "loopback": "^2.14.0", 48 | "loopback-boot": "^2.6.5", 49 | "loopback-component-storage": "~1.0.5", 50 | "loopback-connector-postgresql": "~2.1.0", 51 | "loopback-datasource-juggler": "^2.19.0", 52 | "node-sass": "^4.5.3", 53 | "proquint": "0.0.1", 54 | "qs": "^6.5.1", 55 | "serve-favicon": "^2.0.1" 56 | }, 57 | "optionalDependencies": { 58 | "loopback-explorer": "^1.1.0" 59 | }, 60 | "devDependencies": { 61 | "bower": "^1.3.8", 62 | "browserify": "~4.2.3", 63 | "connect-livereload": "^0.4.0", 64 | "grunt": "^0.4.5", 65 | "grunt-autoprefixer": "^0.8.2", 66 | "grunt-cli": "", 67 | "grunt-concurrent": "^0.5.0", 68 | "grunt-contrib-clean": "^0.5.0", 69 | "grunt-contrib-concat": "^0.5.0", 70 | "grunt-contrib-connect": "^0.8.0", 71 | "grunt-contrib-copy": "^0.5.0", 72 | "grunt-contrib-cssmin": "^0.10.0", 73 | "grunt-contrib-htmlmin": "^0.3.0", 74 | "grunt-contrib-imagemin": "^0.8.1", 75 | "grunt-contrib-jshint": "^0.10.0", 76 | "grunt-contrib-uglify": "^0.5.1", 77 | "grunt-contrib-watch": "^0.6.1", 78 | "grunt-filerev": "^0.2.1", 79 | "grunt-google-cdn": "^0.4.0", 80 | "grunt-karma": "^0.8.3", 81 | "grunt-mocha-test": "^0.12.6", 82 | "grunt-newer": "^0.7.0", 83 | "grunt-ng-annotate": "^1.0.1", 84 | "grunt-sass": "^1.1.0", 85 | "grunt-svgmin": "^0.4.0", 86 | "grunt-usemin": "^2.3.0", 87 | "grunt-wiredep": "^1.8.0", 88 | "http-perf": "0.0.5", 89 | "jshint-stylish": "^0.4.0", 90 | "karma": "^0.12.17", 91 | "karma-chrome-launcher": "^0.1.4", 92 | "karma-jasmine": "^0.1.5", 93 | "load-grunt-tasks": "^0.6.0", 94 | "loopback-testing": "^1.0.4", 95 | "mocha": "^2.1.0", 96 | "time-grunt": "^0.4.0" 97 | }, 98 | "repository": { 99 | "type": "", 100 | "url": "" 101 | }, 102 | "description": "ooni-explorer", 103 | "engines": { 104 | "yarn": ">= 1.0.0" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | server/server.js -------------------------------------------------------------------------------- /server/boot/angular-routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | var routes = require('../../client/ngapp/config/routes'); 3 | Object 4 | .keys(routes) 5 | .forEach(function(route) { 6 | app.get(route, function(req, res) { 7 | res.sendFile(app.get('indexFile')); 8 | }); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /server/boot/authentication.js: -------------------------------------------------------------------------------- 1 | module.exports = function enableAuthentication(server) { 2 | // enable authentication 3 | server.enableAuth(); 4 | }; 5 | -------------------------------------------------------------------------------- /server/boot/create-default-tables.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | module.exports = function(app) { 3 | var ds = app.dataSources.db; 4 | 5 | async.series([ 6 | createNettestRows, 7 | createCensorshipMethods, 8 | createCountryRows 9 | ], function(err, result) { 10 | if (err) { console.log(err);return } 11 | console.log("Mapping method to country"); 12 | mapMethodToCountry(function(err, result) { 13 | if (err) { console.log(err);return } 14 | console.log("Mapping done"); 15 | }); 16 | }); 17 | 18 | function createCountryRows(cb) { 19 | var country_rows = []; 20 | var countries = require('country-data').countries; 21 | countries.all.forEach(function(country, idx) { 22 | if (country.alpha2 === 'TW') { 23 | country.name = 'Taiwan'; 24 | } 25 | country_rows.push({ 26 | 'iso_alpha2': country.alpha2, 27 | 'iso_alpha3': country.alpha3, 28 | 'name': country.name, 29 | 'id': idx 30 | }); 31 | }); 32 | console.log('Inserting country data'); 33 | ds.automigrate('country', function(err) { 34 | if (err) return cb(err); 35 | console.log('Automigrated country data'); 36 | app.models.country.create(country_rows, cb); 37 | }); 38 | } 39 | 40 | function mapMethodToCountry(cb) { 41 | var dns_hijacking = 1; 42 | var http_proxy = 2; 43 | var tcp_ip = 3; 44 | 45 | var methodsByCountry = { 46 | 'TR': [dns_hijacking], 47 | 'IR': [dns_hijacking,http_proxy], 48 | 'SA': [http_proxy], 49 | 'ID': [http_proxy], 50 | 'CN': [dns_hijacking,tcp_ip], 51 | 'RU': [http_proxy], 52 | 'GR': [dns_hijacking,http_proxy] 53 | } 54 | 55 | /* This horror of spaghetti code is the result of many hours of debugging loopback and trying to decipher it's cryptic error messages just so that I could write 2 integers inside of a table. 56 | * This is not what a ORM is supposed to do. 57 | * I am not going to fix this. 58 | * */ 59 | async.mapValues(methodsByCountry, function(methods, iso_alpha2, cb2) { 60 | console.log("" + iso_alpha2 + "->" + methods); 61 | app.models.country.findOne({'where': {'iso_alpha2': iso_alpha2}}, function(err, country) { 62 | if (err) {console.log(err); return cb2(err)} 63 | async.map(methods, function(methodId) { 64 | app.models.censorship_method.findById(methodId, function(err, method) { 65 | country.censorship_methods.add(method); 66 | }); 67 | }, function(err, results) { 68 | if (err) return cb2(err); 69 | cb2(null, results); 70 | }) 71 | }); 72 | }, function(err, results) { 73 | if (err) {return cb(err)} 74 | cb(null, results); 75 | }); 76 | } 77 | 78 | function createCensorshipMethods(cb) { 79 | var methods = [{ 80 | 'id': 1, 81 | 'name': 'DNS hijacking', 82 | 'description': 'DNS hijacking involves sending falsified DNS query responses to requests sent by clients' 83 | },{ 84 | 'id': 2, 85 | 'name': 'Transparent HTTP proxy', 86 | 'description': 'A transparent HTTP proxy is a middle box that will intercept the requests of users and either block them or display an error message' 87 | },{ 88 | 'id': 3, 89 | 'name': 'TCP/IP blocking', 90 | 'description': 'TCP/IP based blocking is blocking or impeding the ability of a client to connect to the server' 91 | }]; 92 | ds.automigrate(['censorship_method', 'countrycensorship_method'], function(err) { 93 | if (err) return cb(err); 94 | console.log('Automigrated censorship methods'); 95 | app.models.censorship_method.create(methods, cb); 96 | }); 97 | } 98 | 99 | function createNettestRows(cb) { 100 | var nettest_rows = [ 101 | { 102 | 'name': 'bridget', 103 | 'long_name': 'BridgeT', 104 | 'description': 'Test to measure reachability of Tor bridges.', 105 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-001-bridget.md' 106 | }, 107 | { 108 | 'name': 'dns_consistency', 109 | 'long_name': 'DNS consistency', 110 | 'description': 'Test to measure consistency amongst DNS responses from a set of test resolvers and a control resolver.', 111 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-002-dnsconsistency.md' 112 | }, 113 | { 114 | 'name': 'http_requests', 115 | 'long_name': 'HTTP requests', 116 | 'description': 'Test to compare HTTP GET request responses from a control and experiment vantage point.', 117 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-003-http-requests.md' 118 | }, 119 | 120 | { 121 | 'name': 'http_host', 122 | 'long_name': 'HTTP host', 123 | 'description': 'Test to identify the presence of a transparent HTTP proxy and verify if certain censorship evasion technique will work against it.', 124 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-004-httphost.md' 125 | }, 126 | { 127 | 'name': 'dns_spoof', 128 | 'long_name': 'DNS spoof', 129 | 'description': 'Test to determine if DNS censorship is happening by means of DNS spoofing.', 130 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-005-dnsspoof.md' 131 | }, 132 | { 133 | 'name': 'http_header_field_manipulation', 134 | 'long_name': 'HTTP header field manipulation', 135 | 'description': 'Test to verify if the HTTP headers sent by the client are being altered when transmitted to the control backend.', 136 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-006-header-field-manipulation.md' 137 | }, 138 | { 139 | 'name': 'http_invalid_request_line', 140 | 'long_name': 'HTTP invalid request line', 141 | 'description': 'Sends invalid HTTP request lines in the attempt to trigger error responses from transparent HTTP proxies.', 142 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-007-http-invalid-request-line.md' 143 | }, 144 | { 145 | 'name': 'tcp_connect', 146 | 'long_name': 'TCP connect', 147 | 'description': 'Performs a TCP handshake with the endpoint.', 148 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-008-tcpconnect.md' 149 | }, 150 | { 151 | 'name': 'multi_protocol_traceroute', 152 | 'long_name': 'Multi protocol traceroute', 153 | 'description': 'Performs a ICMP, TCP and UDP traceroute with varying destination ports in an attempt to spot traceroute path biases.', 154 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-009-multi-port-traceroute.md' 155 | }, 156 | { 157 | 'name': 'captive_portal', 158 | 'long_name': 'Captive portal', 159 | 'description': 'Attempts to detect the presence of a captive portal', 160 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-010-captive-portal.md' 161 | }, 162 | { 163 | 'name': 'bridge_reachability', 164 | 'long_name': 'Bridge reachability', 165 | 'description': 'Tests accessibility of Tor bridges.', 166 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-011-bridge-reachability.md' 167 | }, 168 | { 169 | 'name': 'dns_injection', 170 | 'long_name': 'DNS injection', 171 | 'description': 'Tests to see if the answers to DNS queries for particular hostnames are being injected or spoofed', 172 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-012-dns-injection.md' 173 | }, 174 | { 175 | 'name': 'lantern_circumvention_tool_test', 176 | 'long_name': 'Lantern', 177 | 'description': 'Tests to see if the censorship circumvention tool lantern works.', 178 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-012-lantern.md' 179 | }, 180 | { 181 | 'name': 'meek_fronted_requests_test', 182 | 'long_name': 'Meek fronted requests', 183 | 'description': 'Tests the meek cloudfronted frontend to verify accessibility', 184 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-013-meek-fronted-requests.md' 185 | }, 186 | 187 | { 188 | 'name': 'psiphon_test', 189 | 'long_name': 'Psiphon', 190 | 'description': 'Tests to see if the censorship circumvention tool psiphon works.', 191 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-014-psiphon.md' 192 | }, 193 | { 194 | 'name': 'openvpn', 195 | 'long_name': 'OpenVPN', 196 | 'description': 'Tests to see if the openvpn client works with a set of openvpn endpoints', 197 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-015-openvpn.md' 198 | }, 199 | { 200 | 'name': 'vanilla_tor', 201 | 'long_name': 'Vanilla Tor', 202 | 'description': 'Test to see if tor with no bridges works', 203 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-016-vanilla-tor.md' 204 | }, 205 | { 206 | 'name': 'web_connectivity', 207 | 'long_name': 'Web Connectivity', 208 | 'description': 'Examines whether access to websites is blocked through DNS tampering, TCP or IP blocking, or by a transparent HTTP proxy', 209 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-017-web-connectivity.md' 210 | }, 211 | { 212 | 'name': 'whatsapp', 213 | 'long_name': 'WhatsApp', 214 | 'description': 'Checks to see if WhatsApp is working', 215 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-018-whatsapp.md' 216 | }, 217 | { 218 | 'name': 'facebook_messenger', 219 | 'long_name': 'Facebook Messenger', 220 | 'description': 'Checks to see if Facebook Messenger is working', 221 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-019-facebook-messenger.md' 222 | }, 223 | { 224 | 'name': 'telegram', 225 | 'long_name': 'Telegram', 226 | 'description': 'Checks to see if Telegram is working', 227 | 'spec_url': 'https://github.com/TheTorProject/ooni-spec/blob/master/test-specs/ts-020-telegram.md' 228 | }, 229 | 230 | ]; 231 | console.log('Inserting nettest data'); 232 | ds.automigrate('nettest', function(err) { 233 | if (err) return cb(err); 234 | console.log('Automigrated nettest data'); 235 | app.models.nettest.create(nettest_rows, cb); 236 | }); 237 | } 238 | 239 | } 240 | -------------------------------------------------------------------------------- /server/boot/dev-assets.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = function(app) { 4 | if (!app.get('isDevEnv')) return; 5 | 6 | var serveDir = app.loopback.static; 7 | 8 | app.use(serveDir(projectPath('.tmp'))); 9 | app.use('/bower_components', serveDir(projectPath('bower_components'))); 10 | app.use('/lbclient', serveDir(projectPath('client/lbclient'))); 11 | }; 12 | 13 | function projectPath(relative) { 14 | return path.resolve(__dirname, '../..', relative); 15 | } 16 | -------------------------------------------------------------------------------- /server/boot/explorer.js: -------------------------------------------------------------------------------- 1 | module.exports = function mountLoopBackExplorer(server) { 2 | var explorer; 3 | try { 4 | explorer = require('loopback-explorer'); 5 | } catch(err) { 6 | // Print the message only when the app was started via `server.listen()`. 7 | // Do not print any message when the project is used as a component. 8 | server.once('started', function(baseUrl) { 9 | console.log( 10 | 'Run `npm install loopback-explorer` to enable the LoopBack explorer' 11 | ); 12 | }); 13 | return; 14 | } 15 | 16 | var restApiRoot = server.get('restApiRoot'); 17 | 18 | var explorerApp = explorer(server, { basePath: restApiRoot }); 19 | server.use('/explorer', explorerApp); 20 | server.once('started', function() { 21 | var baseUrl = server.get('url').replace(/\/$/, ''); 22 | // express 4.x (loopback 2.x) uses `mountpath` 23 | // express 3.x (loopback 1.x) uses `route` 24 | var explorerPath = explorerApp.mountpath || explorerApp.route; 25 | console.log('Browse your REST API at %s%s', baseUrl, explorerPath); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /server/boot/extend-models.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 |   var remotes = app.remotes(), 3 | StorageService = require('loopback-component-storage').StorageService, 4 | providers = null; 5 | 6 | try { 7 | providers = require('../providers-private.json'); 8 | } catch(err) { 9 | providers = require('../providers.json'); 10 | } 11 | 12 | var storageHandler = new StorageService({ 13 | provider: 'amazon', 14 | key: providers.amazon.key, 15 | keyId: providers.amazon.keyId, 16 | }); 17 | 18 | remotes.after('report.listReports', function(ctx, next) { 19 | app.models.httpRequestsInteresting.listInteresting(ctx.args.by, function(err, data) { 20 | if (err) { 21 | throw err; 22 | } 23 | Object.keys(data).forEach(function(key) { 24 | ctx.result[key]["interesting"] = data[key]["count"]; 25 | }); 26 | next(); 27 | }); 28 | }); 29 | 30 | remotes.after('report.findReports', function(ctx, next) { 31 | app.models.httpRequestsInteresting.findInteresting(ctx.args.country_code, undefined, 32 | ["report_id"], undefined, 33 | function(err, data) { 34 | ctx.result.reports.forEach(function(report, idx) { 35 | if (data[report.report_id]) { 36 | ctx.result.reports[idx]["interesting"] = data[report.report_id].length; 37 | } 38 | }); 39 | next(); 40 | }); 41 | }); 42 | 43 | app.get("/reportFiles/:year/:report_filename", function(req, res) { 44 | var container = "ooni-public", 45 | filename = "reports-sanitised/yaml/" + req.params.year + "/" + req.params.report_filename; 46 | storageHandler.download(container, filename, res, function(err, result) { 47 | if (err) { 48 | res.status(500).send(err); 49 | } 50 | }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /server/boot/rest-api.js: -------------------------------------------------------------------------------- 1 | module.exports = function mountRestApi(server) { 2 | var restApiRoot = server.get('restApiRoot'); 3 | server.use(restApiRoot, server.loopback.rest()); 4 | }; 5 | -------------------------------------------------------------------------------- /server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "0.0.0.0", 3 | "remoting": {} 4 | } 5 | -------------------------------------------------------------------------------- /server/config.local.js: -------------------------------------------------------------------------------- 1 | var GLOBAL_CONFIG = require('../global-config'); 2 | 3 | var isDevEnv = (process.env.NODE_ENV || 'development') === 'development'; 4 | 5 | module.exports = { 6 | hostname: GLOBAL_CONFIG.hostname, 7 | restApiRoot: GLOBAL_CONFIG.restApiRoot, 8 | livereload: process.env.LIVE_RELOAD, 9 | isDevEnv: isDevEnv, 10 | indexFile: require.resolve(isDevEnv ? 11 | '../client/ngapp/index.html' : '../client/dist/index.html'), 12 | port: GLOBAL_CONFIG.port, 13 | legacyExplorer: GLOBAL_CONFIG.legacyExplorer, 14 | remoting: { 15 | errorHandler: { 16 | handler: function(error, req, res, next) { 17 | if (error instanceof Error) { 18 | console.log( 19 | 'Error in %s %s: errorName=%s errorMessage=%s \n errorStack=%s', 20 | req.method, req.url, error.name, error.message, error.stack); 21 | } 22 | else { 23 | console.log(req.method, req.originalUrl, res.statusCode, error); 24 | } 25 | next(); 26 | } 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /server/config.production.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "0.0.0.0", 3 | "port": 3000 4 | } 5 | -------------------------------------------------------------------------------- /server/datasources.development.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "db": { 3 | "host": process.env.DB_HOST || "localhost", 4 | "port": process.env.DB_PORT || "5432", 5 | "database": process.env.DB_NAME || "ooni_pipeline", 6 | "username": process.env.DB_USERNAME || "ooni", 7 | "password": process.env.DB_PASSWORD || "", 8 | "name": "postgres", 9 | "debug": false, 10 | "connector": "postgresql", 11 | "ssl": process.env.DISABLE_SSL !== 'true' 12 | }, 13 | "mem": { 14 | "name": "mem", 15 | "connector": "memory" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "name": "db", 4 | "connector": "memory" 5 | }, 6 | "mem": { 7 | "name": "mem", 8 | "connector": "memory" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /server/datasources.production.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "db": { 3 | "host": process.env.DB_HOST || "localhost", 4 | "port": process.env.DB_PORT || "5432", 5 | "database": process.env.DB_NAME || "ooni_pipeline", 6 | "username": process.env.DB_USERNAME || "ooni", 7 | "password": process.env.DB_PASSWORD || "", 8 | "name": "postgres", 9 | "debug": false, 10 | "connector": "postgresql", 11 | "ssl": true 12 | }, 13 | "mem": { 14 | "name": "mem", 15 | "connector": "memory" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/middleware.json: -------------------------------------------------------------------------------- 1 | { 2 | "initial:before": { 3 | "loopback#favicon": { 4 | "params": "$!../client/ngapp/favicon.ico" 5 | } 6 | }, 7 | "initial": { 8 | "compression": {}, 9 | "cors": { 10 | "params": { 11 | "origin": true, 12 | "credentials": true, 13 | "maxAge": 86400 14 | } 15 | } 16 | }, 17 | "session": {}, 18 | "auth": {}, 19 | "parse": {}, 20 | "routes": {}, 21 | "files": {}, 22 | "final": { 23 | "loopback#urlNotFound": {} 24 | }, 25 | "final:after": { 26 | "errorhandler": {} 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": [ 4 | "loopback/common/models", 5 | "loopback/server/models", 6 | "../common/models", 7 | "./models" 8 | ], 9 | "mixins": [ 10 | "loopback/common/mixins", 11 | "loopback/server/mixins", 12 | "../common/mixins", 13 | "./mixins" 14 | ] 15 | }, 16 | "User": { 17 | "dataSource": "mem", 18 | "public": false 19 | }, 20 | "AccessToken": { 21 | "dataSource": "mem", 22 | "public": false 23 | }, 24 | "ACL": { 25 | "dataSource": "mem", 26 | "public": false 27 | }, 28 | "RoleMapping": { 29 | "dataSource": "mem", 30 | "public": false 31 | }, 32 | "Role": { 33 | "dataSource": "mem", 34 | "public": false 35 | }, 36 | "report": { 37 | "dataSource": "db", 38 | "public": true 39 | }, 40 | "country": { 41 | "dataSource": "db", 42 | "public": true 43 | }, 44 | "nettest": { 45 | "dataSource": "db", 46 | "public": true 47 | }, 48 | "censorship_method": { 49 | "dataSource": "db", 50 | "public": true 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server/models/country.js: -------------------------------------------------------------------------------- 1 | module.exports = function(Country) { 2 | } 3 | -------------------------------------------------------------------------------- /server/models/report.js: -------------------------------------------------------------------------------- 1 | module.exports = function(Report) { 2 | }; 3 | -------------------------------------------------------------------------------- /server/providers.json: -------------------------------------------------------------------------------- 1 | { 2 | "amazon": { 3 | "key": "DzyYAOiWpDwqzEUk4OpuZuOKBDpepk1ff2y863Zv", 4 | "keyId": "AKIAICLIHHE7CXMFVGDA" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var loopback = require('loopback'); 3 | var boot = require('loopback-boot'); 4 | 5 | var app = module.exports = loopback(); 6 | 7 | // middleware 8 | app.use(loopback.compress()); 9 | 10 | // it's important to register the livereload middleware 11 | // after any response-processing middleware like compress, 12 | // but before any middleware serving actual content 13 | var livereload = app.get('livereload'); 14 | if (livereload) { 15 | app.use(require('connect-livereload')({ 16 | port: livereload 17 | })); 18 | } 19 | 20 | // boot scripts mount components like REST API 21 | boot(app, __dirname); 22 | 23 | // Mount static files like ngapp 24 | // All static middleware should be registered at the end, as all requests 25 | // passing the static middleware are hitting the file system 26 | app.use(loopback.static(path.dirname(app.get('indexFile')))); 27 | 28 | // Requests that get this far won't be handled 29 | // by any middleware. Convert them into a 404 error 30 | // that will be handled later down the chain. 31 | app.use(loopback.urlNotFound()); 32 | 33 | // The ultimate error handler. 34 | app.use(loopback.errorHandler()); 35 | 36 | // optionally start the app 37 | app.start = function() { 38 | // start the web server 39 | return app.listen(function() { 40 | app.emit('started'); 41 | console.log('Web server listening at: %s', app.get('url')); 42 | }); 43 | }; 44 | 45 | if (require.main === module) { 46 | app.start(); 47 | } 48 | --------------------------------------------------------------------------------