├── .env ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql.yml │ ├── docker.yml │ ├── node.js.yml │ └── push.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── babel.config.cjs ├── default.conf ├── eslint.config.cjs ├── index.html ├── package.json ├── public ├── data │ └── rir-country-map.json ├── docs │ └── global_report.md ├── favicon.png ├── imgs │ ├── AS-interdependece.png │ ├── AS-page-AS2497.png │ ├── DF-anomalies.png │ ├── IIJ-logo.svg │ ├── Net-disconnections.png │ ├── aggregated-alarms-architecture.png │ ├── apnic-foundation-logo.png │ ├── cloudflare-logo.png │ ├── disco_AS16322.png │ ├── dns-anomaly-alarm-type-integration.png │ ├── fastly-logo.svg │ ├── forwarding_AS174.png │ ├── global-banner.png │ ├── gsoc-logo.svg │ ├── hegemony_AS2497.png │ ├── ihr_logo.png │ ├── ihr_logo.svg │ ├── isoc-logo.png │ ├── linkdelay_AS7922.png │ ├── manrs-logo.jpg │ ├── netdelay_AS24482.png │ ├── originated-prefixes-chart-AS2497.png │ ├── ripe-logo.png │ └── rv-logo.png └── locales │ ├── en.json │ └── jp.json ├── src ├── App.vue ├── components │ ├── DateTimePicker.vue │ ├── Feedback.vue │ ├── Footer.vue │ ├── Header.vue │ ├── LanguageSwitcher.vue │ ├── LocalStorageBanner.vue │ ├── MetisWidget.vue │ ├── TracerouteMonitor.vue │ ├── UserInfo.vue │ ├── charts │ │ ├── AsInterdependenciesChart.vue │ │ ├── BGPLineChart.vue │ │ ├── BGPPathsChart.vue │ │ ├── CountryHegemonyChart.vue │ │ ├── DelayAndForwardingChart.vue │ │ ├── DelayChart.vue │ │ ├── DiscoChart.vue │ │ ├── IodaChart.vue │ │ ├── IypGenericBarChart.vue │ │ ├── IypGenericBoxPlotChart.vue │ │ ├── IypGenericHeatmapChart.vue │ │ ├── IypGenericPieChart.vue │ │ ├── IypGenericRadarChart.vue │ │ ├── IypGenericTreemapChart.vue │ │ ├── NetworkDelayAlarmsChart.vue │ │ ├── NetworkDelayChart.vue │ │ ├── NetworkTopologyChart.vue │ │ ├── PrefixHegemonyChart.vue │ │ ├── ReactiveChart.vue │ │ ├── RirCountrySunburstChart.vue │ │ ├── TimeSeriesAggregatedAlarmsChart.vue │ │ ├── TracerouteChart.vue │ │ ├── TracerouteRttChart.vue │ │ ├── TreeMapAggregatedAlarmsChart.vue │ │ └── WorldMapAggregatedAlarmsChart.vue │ ├── controllers │ │ ├── AggregatedAlarmsController.vue │ │ ├── GenericCardController.vue │ │ └── IypController.vue │ ├── iyp │ │ ├── as │ │ │ ├── ASAuthoritativeNameservers.vue │ │ │ ├── ASCoLocatedASes.vue │ │ │ ├── ASConnectedASes.vue │ │ │ ├── ASDownstreamsASes.vue │ │ │ ├── ASIXPs.vue │ │ │ ├── ASOriginatedPrefixes.vue │ │ │ ├── ASPopularDomains.vue │ │ │ ├── ASPopularHostNames.vue │ │ │ ├── ASRPKIRouteOriginAuthorization.vue │ │ │ ├── ASRankings.vue │ │ │ ├── ASRipeAtlas.vue │ │ │ ├── ASSiblingASes.vue │ │ │ └── ASUpstreamASes.vue │ │ ├── country │ │ │ ├── CountryASRankings.vue │ │ │ ├── CountryAutonomousSystems.vue │ │ │ ├── CountryIPPrefixes.vue │ │ │ ├── CountryInternetExchangePoints.vue │ │ │ ├── CountryInternetExchangePointsDomesticDistribution.vue │ │ │ ├── CountryInternetExchangePointsInternationalDistribution.vue │ │ │ └── CountryRipeAtlas.vue │ │ ├── hostName │ │ │ ├── HostNameAuthoritativeNameservers.vue │ │ │ ├── HostNameIPAddressesPrefixes.vue │ │ │ ├── HostNameQueryingASes.vue │ │ │ ├── HostNameQueryingCountries.vue │ │ │ └── HostNameRankings.vue │ │ ├── ixp │ │ │ ├── IXPCoLocationFacilities.vue │ │ │ ├── IXPMembers.vue │ │ │ ├── IXPPeeringLANs.vue │ │ │ └── IXPRPKIRouteOriginAuthorization.vue │ │ ├── prefix │ │ │ ├── PrefixAuthoritativeNameservers.vue │ │ │ ├── PrefixLessSpecificPrefixes.vue │ │ │ ├── PrefixMoreSpecificPrefixes.vue │ │ │ ├── PrefixPopularDomains.vue │ │ │ ├── PrefixPopularHostNames.vue │ │ │ ├── PrefixRPKIRouteOriginAuthorization.vue │ │ │ └── PrefixUpstreamASes.vue │ │ ├── rank │ │ │ ├── RankASRankings.vue │ │ │ └── RankHostNameRankings.vue │ │ └── tag │ │ │ ├── TagAutonomousSystems.vue │ │ │ ├── TagPopularHostNames.vue │ │ │ └── TagPrefixes.vue │ ├── maps │ │ ├── DiscoMap.vue │ │ └── WorldMapAggregatedAlarmsMap.vue │ ├── networks │ │ ├── AS.vue │ │ ├── Country.vue │ │ ├── HostName.vue │ │ ├── IXP.vue │ │ ├── Prefix.vue │ │ ├── Rank.vue │ │ ├── Tag.vue │ │ ├── as │ │ │ ├── ASCustom.vue │ │ │ ├── ASDNS.vue │ │ │ ├── ASMonitoring.vue │ │ │ ├── ASOverview.vue │ │ │ ├── ASPeering.vue │ │ │ ├── ASRankings.vue │ │ │ ├── ASRegistration.vue │ │ │ └── ASRouting.vue │ │ ├── country │ │ │ ├── CountryCustom.vue │ │ │ ├── CountryMonitoring.vue │ │ │ ├── CountryOverview.vue │ │ │ ├── CountryPeering.vue │ │ │ ├── CountryRankings.vue │ │ │ └── CountryRouting.vue │ │ ├── hostName │ │ │ ├── HostNameCustom.vue │ │ │ ├── HostNameDNS.vue │ │ │ ├── HostNameRankings.vue │ │ │ └── HostNameRouting.vue │ │ ├── ixp │ │ │ ├── IXPCustom.vue │ │ │ ├── IXPMonitoring.vue │ │ │ ├── IXPOverview.vue │ │ │ ├── IXPPeering.vue │ │ │ └── IXPRouting.vue │ │ ├── prefix │ │ │ ├── PrefixCustom.vue │ │ │ ├── PrefixDNS.vue │ │ │ ├── PrefixOverview.vue │ │ │ └── PrefixRouting.vue │ │ ├── rank │ │ │ └── RankCustom.vue │ │ └── tag │ │ │ ├── TagCustom.vue │ │ │ └── TagOverview.vue │ ├── ripe │ │ ├── Bgplay.vue │ │ ├── Latencymon.vue │ │ ├── ReverseDnsIp.vue │ │ └── Tracemon.vue │ ├── search │ │ ├── LocationSearchBar.vue │ │ └── SearchBar.vue │ └── tables │ │ ├── AggregatedAlarmsTable.vue │ │ ├── AsInterdependenciesTable.vue │ │ ├── BGPMessagesTable.vue │ │ ├── CountryHegemonyTable.vue │ │ ├── DelayAlarmsTable.vue │ │ ├── DiscoAlarmsTable.vue │ │ ├── ForwardingAlarmsTable.vue │ │ ├── MetisTable.vue │ │ ├── NetworkDelayAlarmsTable.vue │ │ ├── NetworkDelayTable.vue │ │ ├── PrefixHegemonyTable.vue │ │ ├── PrefixHegemonyTableStats.vue │ │ ├── TracerouteDestinationsTable.vue │ │ └── TracerouteProbesTable.vue ├── i18n │ ├── index.js │ └── translation.js ├── main.js ├── plugins │ ├── AsNames.js │ ├── GripApi.js │ ├── IhrApi.js │ ├── IodaApi.js │ ├── IypApi.js │ ├── IypGenericTreemapChart.js │ ├── LibraryDelayer.js │ ├── RipeApi.js │ ├── RipeAtlasApi.js │ ├── cache.js │ ├── commonTable.js │ ├── countryName.js │ ├── covid19 │ │ └── lockdowns.js │ ├── delay.js │ ├── disco.js │ ├── layouts │ │ └── layoutsChart.js │ ├── metadata │ │ └── AggregatedAlarmsMetadata.js │ ├── models │ │ ├── AggregatedAlarmsDataModel.js │ │ ├── IodaChartDataModel.js │ │ ├── TableAggregatedAlarmsDataModel.js │ │ ├── TimeSeriesAggregatedAlarmsDataModel.js │ │ ├── TreeMapAggregatedAlarmsDataModel.js │ │ └── WorldMapAggregatedAlarmsDataModel.js │ ├── networkName.js │ ├── query │ │ └── IhrQuery.js │ ├── report.js │ ├── tracerouteFunctions.js │ └── utils │ │ └── AggregatedAlarmsUtils.js ├── router │ └── index.js ├── styles │ ├── chart.css │ └── main.css └── views │ ├── Api.vue │ ├── BGPMonitor.vue │ ├── Contact.vue │ ├── Corona.vue │ ├── Countries.vue │ ├── Documentation.vue │ ├── GlobalReport.vue │ ├── Home.vue │ ├── HostNames.vue │ ├── MetisDeployment.vue │ ├── MetisHome.vue │ ├── MetisSelection.vue │ ├── NetworkTopology.vue │ ├── Networks.vue │ ├── Observable.vue │ ├── PageNotFound.vue │ ├── ROV.vue │ ├── Ranks.vue │ ├── Tags.vue │ └── TracerouteVisualizationTool.vue └── vite.config.js /.env: -------------------------------------------------------------------------------- 1 | VITE_DEFAULT_LOCALE=en 2 | VITE_FALLBACK_LOCALE=en 3 | VITE_SUPPORTED_LOCALES=en,jp 4 | VITE_BASE_URL=/ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## Description 5 | 6 | 7 | 8 | 9 | 10 | ## Related issue 11 | 12 | 13 | 14 | 15 | #000 16 | 17 | ## How Has This Been Tested? 18 | 19 | 20 | 21 | 22 | 23 | 24 | ## Screenshots (if appropriate): 25 | 26 | ## Types of changes 27 | 28 | 29 | 30 | - [ ] Bug fix (non-breaking change which fixes an issue) 31 | - [ ] New feature (non-breaking change which adds functionality) 32 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 33 | 34 | ## Checklist: 35 | 36 | 37 | 38 | 39 | - [ ] My code follows the code style of this project. 40 | - [ ] My change requires a change to the documentation. 41 | - [ ] I have updated the documentation accordingly. 42 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Docker image 17 | run: docker build -t internethealthreport/ihr-website:$(date +%s) . 18 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [20.x, 22.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - name: Installation 28 | run: npm install 29 | - name: Build (cold cache) 30 | run: npm run build 31 | timeout-minutes: 5 32 | - name: Build (warm cache) 33 | run: npm run build 34 | timeout-minutes: 2 -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push Docker Image 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: Log in to Docker Hub 16 | uses: docker/login-action@v2 17 | with: 18 | username: ${{ secrets.DOCKERHUB_USERNAME }} 19 | password: ${{ secrets.DOCKERHUB_TOKEN }} 20 | 21 | - name: Build Docker image 22 | run: | 23 | docker build -t internethealthreport/ihr-website:${{ github.ref_name }} . 24 | 25 | - name: Push Docker image 26 | run: | 27 | docker push internethealthreport/ihr-website:${{ github.ref_name }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | package-lock.json 17 | 18 | /cypress/videos/ 19 | /cypress/screenshots/ 20 | 21 | # Editor directories and files 22 | .vscode/* 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | npx lint-staged 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20.10.0 AS builder 2 | WORKDIR /app 3 | COPY . . 4 | RUN npm install && npm run build 5 | FROM nginx:stable-alpine-slim 6 | WORKDIR /app 7 | COPY --from=builder /app/dist /usr/share/nginx/html 8 | COPY --from=builder /app/default.conf /etc/nginx/conf.d/default.conf 9 | EXPOSE 80 -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | Thanks for helping make this application safe for everyone! ☺️ 2 | 3 | **Reporting Security Issues** 4 | 5 | If you believe you have found a security vulnerability in this repository, please report it to us through coordinated disclosure. 6 | 7 | **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** 8 | 9 | Instead, please send an email to admin@ihr.live. 10 | 11 | Please include as much of the information listed below as you can to help us better understand and resolve the issue: 12 | 13 | * The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) 14 | * Full paths of source file(s) related to the manifestation of the issue 15 | * The location of the affected source code (tag/branch/commit or direct URL) 16 | * Any special configuration required to reproduce the issue 17 | * Step-by-step instructions to reproduce the issue 18 | * Proof-of-concept or exploit code (if possible) 19 | * Impact of the issue, including how an attacker might exploit the issue 20 | 21 | This information will help us triage your report quicker. 22 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ] 11 | ], 12 | }; -------------------------------------------------------------------------------- /default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80; 4 | server_name localhost; 5 | 6 | #access_log /var/log/nginx/host.access.log main; 7 | 8 | location / { 9 | root /usr/share/nginx/html; 10 | index index.html index.htm; 11 | include /etc/nginx/mime.types; 12 | try_files $uri $uri/ /index.html; 13 | } 14 | 15 | #error_page 404 /404.html; 16 | 17 | # redirect server error pages to the static page /50x.html 18 | # 19 | error_page 500 502 503 504 /50x.html; 20 | location = /50x.html { 21 | root /usr/share/nginx/html; 22 | } 23 | 24 | # proxy the PHP scripts to Apache listening on 127.0.0.1:80 25 | # 26 | #location ~ \.php$ { 27 | # proxy_pass http://127.0.0.1; 28 | #} 29 | 30 | # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 31 | # 32 | #location ~ \.php$ { 33 | # root html; 34 | # fastcgi_pass 127.0.0.1:9000; 35 | # fastcgi_index index.php; 36 | # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; 37 | # include fastcgi_params; 38 | #} 39 | 40 | # deny access to .htaccess files, if Apache's document root 41 | # concurs with nginx's one 42 | # 43 | #location ~ /\.ht { 44 | # deny all; 45 | #} 46 | } 47 | 48 | -------------------------------------------------------------------------------- /eslint.config.cjs: -------------------------------------------------------------------------------- 1 | 2 | const globals = require("globals") 3 | const pluginVue = require("eslint-plugin-vue") 4 | const js = require("@eslint/js") 5 | 6 | 7 | module.exports = [ 8 | ...pluginVue.configs['flat/recommended'], 9 | { 10 | files: ["**/*.{vue,js,jsx,cjs,mjs}"], 11 | ignores: ["**/tests/*"], 12 | languageOptions: { 13 | ecmaVersion: 2022, 14 | sourceType: "module", 15 | globals: { 16 | ...globals.browser, 17 | ...globals.node, 18 | myCustomGlobal: "readonly" 19 | }, 20 | }, 21 | rules: { 22 | "vue/no-v-html": "off", 23 | "vue/multi-word-component-names": "off", 24 | "vue/require-prop-types": "off", 25 | "vue/require-default-prop": "off", 26 | "vue/no-template-shadow": "off", 27 | "vue/require-explicit-emits": "off", 28 | "vue/no-useless-template-attributes": "off", 29 | "vue/return-in-emits-validator": "off", 30 | "vue/no-side-effects-in-computed-properties": "off", 31 | "vue/no-v-text-v-html-on-component": "off", 32 | // ...js.configs.recommended.rules, 33 | 34 | }, 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ihr-website", 3 | "version": "1.6.3", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "lint": "npx eslint --format stylish --fix", 11 | "format": "prettier --write src/" 12 | }, 13 | "dependencies": { 14 | "@intlify/unplugin-vue-i18n": "^6.0.6", 15 | "@quasar/extras": "^1.16.17", 16 | "axios": "^1.8.4", 17 | "dagre": "^0.8.5", 18 | "grid-layout-plus": "^1.0.6", 19 | "highlight.js": "^11.11.1", 20 | "highlightjs-cypher": "^1.2.0", 21 | "idb-keyval": "^6.2.1", 22 | "ip-address": "^10.0.1", 23 | "plotly.js-dist": "^3.0.1", 24 | "quasar": "^2.18.1", 25 | "swagger-ui": "^5.21.0", 26 | "v-network-graph": "^0.9.21", 27 | "vue": "^3.5.13", 28 | "vue-i18n": "^11.1.3", 29 | "vue-router": "^4.5.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/preset-env": "^7.26.9", 33 | "@eslint/js": "^9.25.0", 34 | "@quasar/vite-plugin": "^1.9.0", 35 | "@vitejs/plugin-vue": "^5.2.3", 36 | "@vue/eslint-config-prettier": "^10.2.0", 37 | "eslint": "^9.25.0", 38 | "eslint-plugin-vue": "^10.0.0", 39 | "globals": "^16.0.0", 40 | "prettier": "^3.5.3", 41 | "sass-embedded": "^1.86.3", 42 | "vite": "^6.3.2", 43 | "vite-plugin-vue-devtools": "^7.7.5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/favicon.png -------------------------------------------------------------------------------- /public/imgs/AS-interdependece.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/AS-interdependece.png -------------------------------------------------------------------------------- /public/imgs/AS-page-AS2497.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/AS-page-AS2497.png -------------------------------------------------------------------------------- /public/imgs/DF-anomalies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/DF-anomalies.png -------------------------------------------------------------------------------- /public/imgs/Net-disconnections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/Net-disconnections.png -------------------------------------------------------------------------------- /public/imgs/aggregated-alarms-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/aggregated-alarms-architecture.png -------------------------------------------------------------------------------- /public/imgs/apnic-foundation-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/apnic-foundation-logo.png -------------------------------------------------------------------------------- /public/imgs/cloudflare-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/cloudflare-logo.png -------------------------------------------------------------------------------- /public/imgs/disco_AS16322.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/disco_AS16322.png -------------------------------------------------------------------------------- /public/imgs/dns-anomaly-alarm-type-integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/dns-anomaly-alarm-type-integration.png -------------------------------------------------------------------------------- /public/imgs/fastly-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/imgs/forwarding_AS174.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/forwarding_AS174.png -------------------------------------------------------------------------------- /public/imgs/global-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/global-banner.png -------------------------------------------------------------------------------- /public/imgs/hegemony_AS2497.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/hegemony_AS2497.png -------------------------------------------------------------------------------- /public/imgs/ihr_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/ihr_logo.png -------------------------------------------------------------------------------- /public/imgs/isoc-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/isoc-logo.png -------------------------------------------------------------------------------- /public/imgs/linkdelay_AS7922.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/linkdelay_AS7922.png -------------------------------------------------------------------------------- /public/imgs/manrs-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/manrs-logo.jpg -------------------------------------------------------------------------------- /public/imgs/netdelay_AS24482.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/netdelay_AS24482.png -------------------------------------------------------------------------------- /public/imgs/originated-prefixes-chart-AS2497.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/originated-prefixes-chart-AS2497.png -------------------------------------------------------------------------------- /public/imgs/ripe-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/ripe-logo.png -------------------------------------------------------------------------------- /public/imgs/rv-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InternetHealthReport/ihr-website/233d947c5c8cbdbfcaa4cb9be2f71bd54bcf4720/public/imgs/rv-logo.png -------------------------------------------------------------------------------- /src/components/DateTimePicker.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 88 | 89 | 103 | -------------------------------------------------------------------------------- /src/components/Feedback.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 53 | 54 | 68 | -------------------------------------------------------------------------------- /src/components/LanguageSwitcher.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/LocalStorageBanner.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 51 | 52 | 67 | -------------------------------------------------------------------------------- /src/components/charts/IodaChart.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 79 | -------------------------------------------------------------------------------- /src/components/charts/IypGenericBoxPlotChart.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 121 | -------------------------------------------------------------------------------- /src/components/charts/IypGenericHeatmapChart.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 110 | -------------------------------------------------------------------------------- /src/components/charts/IypGenericPieChart.vue: -------------------------------------------------------------------------------- 1 | 110 | 111 | 118 | -------------------------------------------------------------------------------- /src/components/charts/IypGenericRadarChart.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 83 | -------------------------------------------------------------------------------- /src/components/charts/NetworkDelayAlarmsChart.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 107 | -------------------------------------------------------------------------------- /src/components/charts/RirCountrySunburstChart.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 69 | -------------------------------------------------------------------------------- /src/components/charts/WorldMapAggregatedAlarmsChart.vue: -------------------------------------------------------------------------------- 1 | 64 | 78 | -------------------------------------------------------------------------------- /src/components/controllers/GenericCardController.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 96 | 97 | 110 | -------------------------------------------------------------------------------- /src/components/iyp/as/ASAuthoritativeNameservers.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 94 | -------------------------------------------------------------------------------- /src/components/iyp/as/ASCoLocatedASes.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 71 | -------------------------------------------------------------------------------- /src/components/iyp/as/ASConnectedASes.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 95 | -------------------------------------------------------------------------------- /src/components/iyp/as/ASIXPs.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 86 | -------------------------------------------------------------------------------- /src/components/iyp/as/ASPopularDomains.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 109 | -------------------------------------------------------------------------------- /src/components/iyp/as/ASRPKIRouteOriginAuthorization.vue: -------------------------------------------------------------------------------- 1 | 92 | 93 | 105 | -------------------------------------------------------------------------------- /src/components/iyp/as/ASRankings.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 102 | -------------------------------------------------------------------------------- /src/components/iyp/as/ASRipeAtlas.vue: -------------------------------------------------------------------------------- 1 | 100 | 101 | 122 | -------------------------------------------------------------------------------- /src/components/iyp/as/ASSiblingASes.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 80 | -------------------------------------------------------------------------------- /src/components/iyp/country/CountryAutonomousSystems.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 87 | -------------------------------------------------------------------------------- /src/components/iyp/country/CountryInternetExchangePoints.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 106 | -------------------------------------------------------------------------------- /src/components/iyp/hostName/HostNameQueryingASes.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 103 | -------------------------------------------------------------------------------- /src/components/iyp/hostName/HostNameQueryingCountries.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 102 | -------------------------------------------------------------------------------- /src/components/iyp/hostName/HostNameRankings.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 79 | -------------------------------------------------------------------------------- /src/components/iyp/ixp/IXPCoLocationFacilities.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 78 | -------------------------------------------------------------------------------- /src/components/iyp/ixp/IXPMembers.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 96 | -------------------------------------------------------------------------------- /src/components/iyp/ixp/IXPPeeringLANs.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 81 | -------------------------------------------------------------------------------- /src/components/iyp/prefix/PrefixAuthoritativeNameservers.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 104 | -------------------------------------------------------------------------------- /src/components/iyp/prefix/PrefixPopularDomains.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 105 | -------------------------------------------------------------------------------- /src/components/iyp/prefix/PrefixPopularHostNames.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 104 | -------------------------------------------------------------------------------- /src/components/iyp/prefix/PrefixRPKIRouteOriginAuthorization.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 97 | -------------------------------------------------------------------------------- /src/components/iyp/prefix/PrefixUpstreamASes.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 88 | -------------------------------------------------------------------------------- /src/components/iyp/rank/RankASRankings.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 90 | -------------------------------------------------------------------------------- /src/components/iyp/rank/RankHostNameRankings.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 78 | -------------------------------------------------------------------------------- /src/components/iyp/tag/TagPopularHostNames.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 98 | -------------------------------------------------------------------------------- /src/components/maps/WorldMapAggregatedAlarmsMap.vue: -------------------------------------------------------------------------------- 1 | 116 | 117 | 128 | -------------------------------------------------------------------------------- /src/components/networks/Rank.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 117 | 118 | 123 | -------------------------------------------------------------------------------- /src/components/networks/Tag.vue: -------------------------------------------------------------------------------- 1 | 89 | 90 | 124 | 125 | 130 | -------------------------------------------------------------------------------- /src/components/networks/as/ASDNS.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | 39 | 44 | -------------------------------------------------------------------------------- /src/components/networks/as/ASPeering.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 58 | 59 | 64 | -------------------------------------------------------------------------------- /src/components/networks/as/ASRankings.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /src/components/networks/as/ASRegistration.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /src/components/networks/as/ASRouting.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 58 | 59 | 64 | -------------------------------------------------------------------------------- /src/components/networks/country/CountryPeering.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 44 | 45 | 50 | -------------------------------------------------------------------------------- /src/components/networks/country/CountryRankings.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | 20 | 25 | -------------------------------------------------------------------------------- /src/components/networks/country/CountryRouting.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /src/components/networks/hostName/HostNameDNS.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | 39 | 44 | -------------------------------------------------------------------------------- /src/components/networks/hostName/HostNameRankings.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /src/components/networks/hostName/HostNameRouting.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /src/components/networks/ixp/IXPMonitoring.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 45 | 46 | 51 | -------------------------------------------------------------------------------- /src/components/networks/ixp/IXPPeering.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | 39 | 44 | -------------------------------------------------------------------------------- /src/components/networks/ixp/IXPRouting.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /src/components/networks/prefix/PrefixDNS.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | 39 | 44 | -------------------------------------------------------------------------------- /src/components/networks/prefix/PrefixRouting.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 48 | 49 | 54 | -------------------------------------------------------------------------------- /src/components/networks/tag/TagOverview.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 78 | 79 | 95 | -------------------------------------------------------------------------------- /src/components/ripe/Bgplay.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/components/ripe/Latencymon.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 87 | 88 | 97 | -------------------------------------------------------------------------------- /src/components/ripe/ReverseDnsIp.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/components/ripe/Tracemon.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 47 | -------------------------------------------------------------------------------- /src/components/tables/MetisTable.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import axios from 'axios' 3 | 4 | const i18n = async () => { 5 | const messages = (await axios.get('/locales/en.json')).data 6 | return createI18n({ 7 | locale: import.meta.env.VITE_DEFAULT_LOCALE, 8 | fallbackLocale: import.meta.env.VITE_FALLBACK_LOCALE, 9 | legacy: false, 10 | globalInjection: true, 11 | warnHtmlMessage: false, 12 | messages: { en: messages } 13 | }) 14 | } 15 | 16 | export default await i18n() 17 | -------------------------------------------------------------------------------- /src/i18n/translation.js: -------------------------------------------------------------------------------- 1 | import i18n from '@/i18n' 2 | import { nextTick } from 'vue' 3 | import { get, set } from 'idb-keyval' 4 | import axios from 'axios' 5 | 6 | const Trans = { 7 | set currentLocale(newLocale) { 8 | i18n.global.locale.value = newLocale 9 | }, 10 | 11 | isLocaleSupported(locale) { 12 | return Trans.supportedLocales.includes(locale) 13 | }, 14 | 15 | getUserLocale() { 16 | const locale = window.navigator.language || window.navigator.userLanguage || Trans.defaultLocale 17 | return { 18 | locale: locale, 19 | localeNoRegion: locale.split('-')[0] 20 | } 21 | }, 22 | 23 | async getPersistedLocale() { 24 | const persistedLocale = await get('user-locale') 25 | if (Trans.isLocaleSupported(persistedLocale)) { 26 | return persistedLocale 27 | } else { 28 | return null 29 | } 30 | }, 31 | 32 | guessDefaultLocale() { 33 | const userPersistedLocale = Trans.getPersistedLocale() 34 | if (userPersistedLocale) { 35 | return userPersistedLocale 36 | } 37 | const userPreferredLocale = Trans.getUserLocale() 38 | if (Trans.isLocaleSupported(userPreferredLocale.locale)) { 39 | return userPreferredLocale.locale 40 | } 41 | if (Trans.isLocaleSupported(userPreferredLocale.localeNoRegion)) { 42 | return userPreferredLocale.localeNoRegion 43 | } 44 | return Trans.defaultLocale 45 | }, 46 | 47 | get defaultLocale() { 48 | return import.meta.env.VITE_DEFAULT_LOCALE 49 | }, 50 | 51 | get supportedLocales() { 52 | return import.meta.env.VITE_SUPPORTED_LOCALES.split(',') 53 | }, 54 | 55 | async routeMiddleware(to, _from, next) { 56 | let paramLocale = to.params.locale 57 | if (!Trans.isLocaleSupported(paramLocale)) { 58 | return next(await Trans.guessDefaultLocale()) 59 | } 60 | await Trans.switchLanguage(paramLocale) 61 | return next() 62 | }, 63 | 64 | get currentLocale() { 65 | return i18n.global.locale.value 66 | }, 67 | 68 | i18nRoute(to) { 69 | return { 70 | ...to, 71 | params: { 72 | locale: Trans.currentLocale, 73 | ...to.params 74 | } 75 | } 76 | }, 77 | 78 | async switchLanguage(newLocale) { 79 | await Trans.loadLocaleMessages(newLocale) 80 | Trans.currentLocale = newLocale 81 | document.querySelector('html').setAttribute('lang', newLocale) 82 | if (JSON.parse(await get('storage-allowed'))) { 83 | await set('user-locale', newLocale) 84 | } 85 | }, 86 | 87 | async loadLocaleMessages(locale) { 88 | if (!i18n.global.availableLocales.includes(locale)) { 89 | const messages = (await axios.get(`/locales/${locale}.json`)).data 90 | i18n.global.setLocaleMessage(locale, messages) 91 | } 92 | return nextTick() 93 | } 94 | } 95 | 96 | export default Trans 97 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import './styles/main.css' 2 | 3 | import { createApp } from 'vue' 4 | import App from './App.vue' 5 | import router from './router' 6 | import { Quasar } from 'quasar' 7 | import '@quasar/extras/roboto-font/roboto-font.css' 8 | import '@quasar/extras/material-icons/material-icons.css' 9 | import '@quasar/extras/fontawesome-v6/fontawesome-v6.css' 10 | import 'quasar/dist/quasar.css' 11 | import i18n from './i18n' 12 | import { IhrApi } from '@/plugins/IhrApi' 13 | import { LibraryDelayer } from '@/plugins/LibraryDelayer' 14 | import { IypApi } from '@/plugins/IypApi' 15 | import { AtlasApi } from '@/plugins/RipeAtlasApi' 16 | import VNetworkGraph from 'v-network-graph' 17 | import 'v-network-graph/lib/style.css' 18 | 19 | const app = createApp(App) 20 | 21 | app.use(router) 22 | app.use(Quasar, { 23 | plugins: {}, 24 | config: { 25 | brand: { 26 | primary: '#263238', 27 | secondary: '#1976d2', 28 | accent: '#405057', 29 | positive: '#21ba45', 30 | negative: '#c10015', 31 | info: '#4f5b62', 32 | warning: '#ffee58' 33 | } 34 | } 35 | }) 36 | app.use(i18n) 37 | app.use(IhrApi) 38 | app.use(LibraryDelayer, { 39 | libraries: { 40 | bgplay_api: 41 | 'https://cdn.jsdelivr.net/gh/InternetHealthReport/bgplay/widget/bgplayjs-main-widget.js', 42 | ripe_widget_api: 'https://stat.ripe.net/widget-api/widget_api.js', 43 | latencymon_widget: [ 44 | 'https://www-static.ripe.net/static/rnd-ui/atlas/static/measurements/widgets/latencymon/dev/libs/require.min.js', 45 | 'https://atlas.ripe.net/resource/latencymon/latencymon-widget-main.js' 46 | ], 47 | tracemon_widget: [ 48 | 'https://www-static.ripe.net/static/rnd-ui/atlas/static/measurements/widgets/tracemon/dev/libs/require.min.js', 49 | // 'https://atlas.ripe.net/resource/tracemon/tracemon-widget-main.js', 50 | 'https://www-static.ripe.net/static/rnd-ui/atlas/static/measurements/widgets/tracemon/tracemon-widget-main.js' 51 | ] 52 | } 53 | }) 54 | app.use(IypApi) 55 | app.use(AtlasApi) 56 | app.use(VNetworkGraph) 57 | 58 | app.mount('#app') 59 | -------------------------------------------------------------------------------- /src/plugins/AsNames.js: -------------------------------------------------------------------------------- 1 | import { runIyp } from './IypApi' 2 | 3 | export function getASNamesCountryMappings() { 4 | const cypher = ` 5 | MATCH (a:AS) 6 | OPTIONAL MATCH (a)-[:NAME {reference_org:'PeeringDB'}]->(pdbn:Name) 7 | OPTIONAL MATCH (a)-[:NAME {reference_org:'BGP.Tools'}]->(btn:Name) 8 | OPTIONAL MATCH (a)-[:NAME {reference_org:'RIPE NCC'}]->(ripen:Name) 9 | OPTIONAL MATCH (a)-[:COUNTRY {reference_name: 'nro.delegated_stats'}]-(cc:Country) 10 | RETURN DISTINCT a.asn AS asn, COALESCE(pdbn.name, btn.name, ripen.name) AS name, cc.country_code AS country_code 11 | ` 12 | const request = () => { 13 | return new Promise((resolve, reject) => { 14 | const parsedData = {} 15 | runIyp([{ statement: cypher }]) 16 | .then((response) => { 17 | response[0].forEach(obj => { 18 | parsedData[obj.asn] = { 19 | asn_name: obj.name, 20 | country_iso_code2: obj.country_code 21 | } 22 | }) 23 | resolve(parsedData) 24 | }) 25 | .catch((error) => reject(error)) 26 | }) 27 | } 28 | return request() 29 | } 30 | -------------------------------------------------------------------------------- /src/plugins/GripApi.js: -------------------------------------------------------------------------------- 1 | import * as AggregatedAlarmsUtils from './utils/AggregatedAlarmsUtils' 2 | import axios from 'axios' 3 | 4 | export function getGripAlarms( 5 | startTime, 6 | endTime, 7 | timezone = '', 8 | minSuspicionLevel = 80, 9 | maxSuspicionLevel = 100, 10 | eventType = 'all', 11 | onePage = false 12 | ) { 13 | const request = () => { 14 | return new Promise((resolve, _) => { 15 | getGripAlarmsHelper( 16 | startTime, 17 | endTime, 18 | timezone, 19 | minSuspicionLevel, 20 | maxSuspicionLevel, 21 | eventType, 22 | onePage 23 | ) 24 | .then((gripAlarmsData) => resolve(gripAlarmsData)) 25 | .catch((error) => { 26 | console.error(error) 27 | resolve([]) 28 | }) 29 | }) 30 | } 31 | return request() 32 | } 33 | 34 | function getGripAlarmsHelper( 35 | startTime, 36 | endTime, 37 | timezone = '', 38 | minSuspicionLevel = 80, 39 | maxSuspicionLevel = 100, 40 | eventType = 'all', 41 | onePage 42 | ) { 43 | const API_URL = 'https://ihr.iijlab.net/proxy/grip/events' 44 | 45 | const chunkSize = 100 46 | const startUTCTimeFormatted = AggregatedAlarmsUtils.formatUTCTime(startTime, timezone) 47 | const endUTCTimeFormatted = AggregatedAlarmsUtils.formatUTCTime(endTime, timezone) 48 | 49 | const params = { 50 | length: chunkSize, 51 | start: 0, 52 | ts_start: startUTCTimeFormatted, 53 | ts_end: endUTCTimeFormatted, 54 | min_susp: minSuspicionLevel, 55 | max_susp: maxSuspicionLevel, 56 | event_type: eventType 57 | } 58 | 59 | const request = () => { 60 | return axios 61 | .get(API_URL, { params }) 62 | .then(handleResponse) 63 | .catch((error) => Promise.reject(error)) 64 | } 65 | 66 | const handleResponse = (response) => { 67 | const data = response.data 68 | const totalRecords = parseInt(data.recordsTotal) 69 | const bgpAlertsData = [...data.data] 70 | if (onePage) { 71 | return bgpAlertsData 72 | } 73 | const getPageDataPromises = createGetPageDataPromises(totalRecords, bgpAlertsData, params) 74 | 75 | return Promise.all(getPageDataPromises) 76 | .then(() => bgpAlertsData) 77 | .catch((error) => Promise.reject(error)) 78 | } 79 | 80 | const createGetPageDataPromises = (totalRecords, bgpAlertsData, params) => { 81 | const getPageDataPromises = [] 82 | 83 | for (let i = 100; i < totalRecords; i += 100) { 84 | params.start = i 85 | const getPromise = getPageData(API_URL, params) 86 | .then((pageData) => { 87 | bgpAlertsData.push(...pageData) 88 | return delay(0.5) 89 | }) 90 | .catch((_) => { 91 | return delay(1000) 92 | }) 93 | 94 | getPageDataPromises.push(getPromise) 95 | } 96 | 97 | return getPageDataPromises 98 | } 99 | 100 | const getPageData = async (url, params) => { 101 | return axios.get(url, { params }).then((response) => response.data.data) 102 | } 103 | 104 | const delay = (ms) => { 105 | return new Promise((resolve) => setTimeout(resolve, ms)) 106 | } 107 | 108 | return request() 109 | } 110 | -------------------------------------------------------------------------------- /src/plugins/IodaApi.js: -------------------------------------------------------------------------------- 1 | import * as AggregatedAlarmsUtils from './utils/AggregatedAlarmsUtils' 2 | import axios from 'axios' 3 | 4 | export function getIodaAlarms( 5 | startTime, 6 | endTime, 7 | timezone = ':00Z', 8 | entityType = 'asn', 9 | ignoreMethods = '*.sarima' 10 | ) { 11 | const request = () => { 12 | return new Promise((resolve, _) => { 13 | getIodaAlarmsHelper(startTime, endTime, timezone, entityType, ignoreMethods) 14 | .then((iodaAlarmsData) => resolve(iodaAlarmsData)) 15 | .catch((error) => { 16 | console.error(error) 17 | resolve([]) 18 | }) 19 | }) 20 | } 21 | return request() 22 | } 23 | 24 | function getIodaAlarmsHelper(startTime, endTime, timezone, entityType, ignoreMethods) { 25 | const API_URL = 'https://ihr.iijlab.net/proxy/ioda/alerts' 26 | 27 | const startUTCTimeFormatted = AggregatedAlarmsUtils.formatUTCTime(startTime, timezone) 28 | const endUTCTimeFormatted = AggregatedAlarmsUtils.formatUTCTime(endTime, timezone) 29 | 30 | const startUnixTime = Date.parse(startUTCTimeFormatted) / 1000 31 | const endUnixTime = Date.parse(endUTCTimeFormatted) / 1000 32 | 33 | const params = { 34 | from: startUnixTime, 35 | until: endUnixTime, 36 | entityType: entityType, 37 | ignoreMethods: ignoreMethods 38 | } 39 | 40 | const request = async () => { 41 | return axios 42 | .get(API_URL, { params }) 43 | .then((response) => response.data.data) 44 | .catch((error) => Promise.reject(error)) 45 | } 46 | 47 | return request() 48 | } 49 | 50 | export function getIodaEntityInfo( 51 | entityType, 52 | entityValue, 53 | startUnixTime, 54 | endUnixTime, 55 | sourceParams 56 | ) { 57 | const API_URL = `https://api.ioda.inetintel.cc.gatech.edu/v2/signals/raw/${entityType}/${entityValue}?from=${startUnixTime}&until=${endUnixTime}&sourceParams=${sourceParams}` 58 | const request = () => { 59 | return new Promise((resolve, reject) => { 60 | axios 61 | .get(API_URL) 62 | .then((response) => { 63 | const data = response.data.data.length ? response.data.data[0] : [] 64 | resolve(data) 65 | }) 66 | .catch((error) => reject(error)) 67 | }) 68 | } 69 | return request() 70 | } 71 | -------------------------------------------------------------------------------- /src/plugins/IypApi.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import cache from './cache.js' 3 | import { get } from 'idb-keyval' 4 | 5 | /// Base url for api 6 | const IYP_API_BASE = 'https://iyp.iijlab.net/iyp/db/neo4j/query/v2' 7 | 8 | /// Default timeout before api call are considered failed 9 | const DEFAULT_TIMEOUT = 180000 10 | 11 | const axios_base = axios.create({ 12 | baseURL: IYP_API_BASE, 13 | timeout: DEFAULT_TIMEOUT 14 | }) 15 | 16 | const runIyp = async (queries) => { 17 | const storageAllowed = JSON.parse(await get('storage-allowed')) 18 | let response = await Promise.all( 19 | queries.map((query) => { 20 | return cache( 21 | JSON.stringify(query), 22 | () => { 23 | return axios_base.post('', query) 24 | }, 25 | { 26 | storageAllowed: storageAllowed ? storageAllowed : false 27 | } 28 | ) 29 | }) 30 | ) 31 | return response.map((res) => { 32 | return formatResponse(res.data.data) 33 | }) 34 | } 35 | 36 | const formatResponse = (results) => { 37 | const list = [] 38 | const keys = results.fields 39 | results.values.forEach((row) => { 40 | let obj = {} 41 | let countElementsInRow = 0 42 | row.forEach((rowVal) => { 43 | if (typeof rowVal === 'object') { 44 | if (rowVal?.properties) { 45 | obj[keys[countElementsInRow]] = rowVal.properties 46 | } else { 47 | obj[keys[countElementsInRow]] = rowVal?.map((val) => { 48 | if (val?.properties !== undefined) { 49 | return val.properties 50 | } 51 | return val 52 | }) 53 | } 54 | } else { 55 | obj[keys[countElementsInRow]] = rowVal 56 | } 57 | countElementsInRow += 1 58 | }) 59 | obj = JSON.parse(JSON.stringify(obj, (_, value) => value === undefined ? null : value)) 60 | list.push(obj) 61 | }) 62 | return list 63 | } 64 | 65 | const IypApi = { 66 | install: (app, options) => { 67 | const run = async (queries) => { 68 | return runIyp(queries) 69 | } 70 | 71 | const iyp_api = { 72 | run 73 | } 74 | app.provide('iyp_api', iyp_api) 75 | } 76 | } 77 | 78 | export { IypApi, runIyp } 79 | -------------------------------------------------------------------------------- /src/plugins/IypGenericTreemapChart.js: -------------------------------------------------------------------------------- 1 | import Tr from '@/i18n/translation' 2 | 3 | function isLeafNode(nodeLabel, data) { 4 | for (let i = 0; i < data.labels.length; i++) { 5 | if (data.parents[i].includes(nodeLabel)) { 6 | return false 7 | } 8 | } 9 | return true 10 | } 11 | 12 | export default function treemapClicked(event) { 13 | if (event.points && event.points.length) { 14 | const network = event.points[0].label 15 | if ( 16 | typeof network === 'string' && 17 | event.leafKey === 'nameserver' && 18 | isLeafNode(network, event.points[0].data) 19 | ) { 20 | } else if ( 21 | typeof network === 'number' && 22 | event.leafKey === 'asn' && 23 | isLeafNode(network, event.points[0].data) 24 | ) { 25 | const asId = `AS${network}` 26 | event.router.push( 27 | Tr.i18nRoute({ 28 | name: 'network', 29 | params: { id: asId } 30 | }) 31 | ) 32 | } else if ( 33 | typeof network === 'string' && 34 | event.leafKey === 'ixpName' && 35 | isLeafNode(network, event.points[0].data) 36 | ) { 37 | } else if ( 38 | typeof network === 'string' && 39 | event.leafKey === 'prefix' && 40 | isLeafNode(network, event.points[0].data) 41 | ) { 42 | const [host, prefixLength] = network.split('/') 43 | if (prefixLength) { 44 | event.router.push( 45 | Tr.i18nRoute({ 46 | name: 'prefix', 47 | params: { ip: host, length: prefixLength } 48 | }) 49 | ) 50 | } 51 | } else if ( 52 | typeof network === 'string' && 53 | (event.leafKey === 'hostname') & isLeafNode(network, event.points[0].data) 54 | ) { 55 | event.router.push( 56 | Tr.i18nRoute({ 57 | name: 'hostname', 58 | params: { hostname: network } 59 | }) 60 | ) 61 | } else if ( 62 | typeof network === 'number' && 63 | event.leafKey === 'atlasId' && 64 | isLeafNode(network, event.points[0].data) 65 | ) { 66 | } else if ( 67 | typeof network === 'string' && 68 | event.leafKey === 'rankName' && 69 | isLeafNode(network, event.points[0].data) 70 | ) { 71 | event.router.push( 72 | Tr.i18nRoute({ 73 | name: 'rank', 74 | params: { rank: network } 75 | }) 76 | ) 77 | } else if ( 78 | typeof network === 'string' && 79 | event.leafKey === 'asn' && 80 | isLeafNode(network.split(' ')[0], event.points[0].data) 81 | ) { 82 | event.router.push( 83 | Tr.i18nRoute({ 84 | name: 'network', 85 | params: { id: network } 86 | }) 87 | ) 88 | } else if ( 89 | typeof network === 'string' && 90 | event.leafKey === 'ip' && 91 | isLeafNode(network, event.points[0].data) 92 | ) { 93 | } else if ( 94 | typeof network === 'string' && 95 | event.leafKey === 'country' && 96 | isLeafNode(network, event.points[0].data) 97 | ) { 98 | event.router.push( 99 | Tr.i18nRoute({ 100 | name: 'country', 101 | params: { cc: network } 102 | }) 103 | ) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/plugins/RipeApi.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import cache from './cache.js' 3 | import { get, set } from 'idb-keyval' 4 | 5 | // Base URL for RIPE stat API 6 | const RIPE_API_BASE = 'https://stat.ripe.net/data/' 7 | const DEFAULT_TIMEOUT = 180000 8 | 9 | const axios_base = axios.create({ 10 | baseURL: RIPE_API_BASE, 11 | timeout: DEFAULT_TIMEOUT 12 | }) 13 | 14 | // Utility function to get data with caching 15 | const getCachedData = async (url, params) => { 16 | const key = `${url}_${JSON.stringify(params)}` 17 | const cachedData = await get(key) 18 | 19 | if (cachedData) { 20 | return JSON.parse(cachedData) 21 | } 22 | 23 | const response = await ripe_axios.get(url, { params }) 24 | try { 25 | await set(key, JSON.stringify(response.data)) 26 | } catch (error) { 27 | if ( 28 | error instanceof DOMException && 29 | (error.code === 22 || 30 | error.code === 1014 || 31 | error.name === 'QuotaExceededError' || 32 | error.name === 'NS_ERROR_DOM_QUOTA_REACHED') 33 | ) { 34 | return { error: 'LOCAL_STORAGE_FULL' } 35 | } 36 | } 37 | return response.data 38 | } 39 | 40 | export default { 41 | async asnNeighbours(asn) { 42 | let queryarg = { 43 | params: { 44 | resource: asn 45 | } 46 | } 47 | const storageAllowed = false //await get('storage-allowed')) 48 | const url = 'asn-neighbours/data.json' 49 | return await cache( 50 | `${url}_${JSON.stringify(queryarg)}`, 51 | () => { 52 | return axios_base.get(url, queryarg) 53 | }, 54 | { 55 | storageAllowed: storageAllowed ? storageAllowed : false 56 | } 57 | ) 58 | }, 59 | async userIP() { 60 | const storageAllowed = false //JSON.parse(await get('storage-allowed')) 61 | const url = 'whats-my-ip/data.json' 62 | return await cache( 63 | `${url}_${JSON.stringify({})}`, 64 | () => { 65 | return axios_base.get(url) 66 | }, 67 | { 68 | storageAllowed: storageAllowed ? storageAllowed : false 69 | } 70 | ) 71 | }, 72 | async userASN(ip) { 73 | let queryarg = { 74 | params: { 75 | resource: ip 76 | } 77 | } 78 | const storageAllowed = false //JSON.parse(await get('storage-allowed')) 79 | const url = 'network-info/data.json' 80 | return await cache( 81 | `${url}_${JSON.stringify(queryarg)}`, 82 | () => { 83 | return axios_base.get(url, queryarg) 84 | }, 85 | { 86 | storageAllowed: storageAllowed ? storageAllowed : false 87 | } 88 | ) 89 | }, 90 | async prefixOverview(ip) { 91 | let queryarg = { 92 | params: { 93 | resource: ip 94 | } 95 | } 96 | const storageAllowed = false //JSON.parse(await get('storage-allowed')) 97 | const url = 'prefix-overview/data.json' 98 | return await cache( 99 | `${url}_${JSON.stringify(queryarg)}`, 100 | () => { 101 | return axios_base.get(url, queryarg) 102 | }, 103 | { 104 | storageAllowed: storageAllowed ? storageAllowed : false 105 | } 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/plugins/RipeAtlasApi.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import cache from './cache.js' 3 | import { get, set } from 'idb-keyval' 4 | 5 | // Base URL for RIPE Atlas API 6 | const RIPE_ATLAS_API_BASE = 'https://atlas.ripe.net/api/v2/' 7 | const DEFAULT_TIMEOUT = 180000 8 | 9 | const axios_base = axios.create({ 10 | baseURL: RIPE_ATLAS_API_BASE, 11 | timeout: DEFAULT_TIMEOUT 12 | }) 13 | 14 | const AtlasApi = { 15 | install: (app, options) => { 16 | const getMeasurementById = async (measurementId) => { 17 | const storageAllowed = JSON.parse(await get('storage-allowed')) 18 | const url = `measurements/${measurementId}` 19 | return await cache( 20 | `${url}`, 21 | () => { 22 | return axios_base.get(url) 23 | }, 24 | { 25 | storageAllowed: storageAllowed ? storageAllowed : false 26 | } 27 | ) 28 | } 29 | 30 | const getMeasurementData = async (measurementId, params = {}) => { 31 | const storageAllowed = JSON.parse(await get('storage-allowed')) 32 | const url = `measurements/${measurementId}/results` 33 | return await cache( 34 | `${url}_${JSON.stringify(params)}`, 35 | () => { 36 | return axios_base.get(url, { 37 | params 38 | }) 39 | }, 40 | { 41 | storageAllowed: storageAllowed ? storageAllowed : false 42 | } 43 | ) 44 | } 45 | 46 | const getProbeById = async (probeId) => { 47 | const storageAllowed = JSON.parse(await get('storage-allowed')) 48 | const url = `probes/${probeId}` 49 | return await cache( 50 | `${url}`, 51 | () => { 52 | return axios_base.get(url) 53 | }, 54 | { 55 | storageAllowed: storageAllowed ? storageAllowed : false 56 | } 57 | ) 58 | } 59 | 60 | const atlas_api = { 61 | getMeasurementById, 62 | getMeasurementData, 63 | getProbeById 64 | } 65 | 66 | app.provide('atlas_api', atlas_api) 67 | } 68 | } 69 | 70 | export { AtlasApi } 71 | -------------------------------------------------------------------------------- /src/plugins/cache.js: -------------------------------------------------------------------------------- 1 | import { get, set, del, clear, keys, getMany, delMany } from 'idb-keyval' 2 | 3 | const cache = async (key, fetcher, options) => { 4 | if (!options) { 5 | options = defaultOptions 6 | } else { 7 | options = Object.assign(defaultOptions, options) 8 | } 9 | let item = await getItem(key) 10 | if (item) { 11 | item = JSON.parse(item).data 12 | } else { 13 | try { 14 | item = await fetcher() 15 | const sessionObj = { 16 | ...options, 17 | data: item 18 | } 19 | if (options.storageAllowed) { 20 | await set(key, JSON.stringify(sessionObj)) 21 | } 22 | } catch (error) { 23 | if ( 24 | error instanceof DOMException && 25 | (error.code === 22 || 26 | error.code === 1014 || 27 | error.name === 'QuotaExceededError' || 28 | error.name === 'NS_ERROR_DOM_QUOTA_REACHED') 29 | ) { 30 | await deleteExpiredItemsAndReduceSpace() 31 | item = await cache(key, fetcher, options) 32 | } 33 | } 34 | } 35 | return item 36 | } 37 | 38 | const getItem = async (key) => { 39 | let item = await get(key) 40 | if (item) { 41 | if (JSON.parse(item).expiresAt < new Date().getTime()) { 42 | await del(key) 43 | item = null 44 | } 45 | } 46 | return item 47 | } 48 | 49 | const deleteExpiredItemsAndReduceSpace = async () => { 50 | const allKeys = await keys() 51 | const allItems = await getMany(allKeys) 52 | const keysToDel = [] 53 | const sortedKeys = [] 54 | allItems.forEach((item, index) => { 55 | const parsedItem = JSON.parse(item) 56 | if (parsedItem.expiresAt !== undefined) { 57 | const getKey = allKeys.at(index) 58 | if (parsedItem.expiresAt < new Date().getTime()) { 59 | keysToDel.push(getKey) 60 | } else { 61 | sortedKeys.push([getKey, parsedItem.expiresAt]) 62 | } 63 | } 64 | }) 65 | if (keysToDel.length) { 66 | await delMany(keysToDel) 67 | } 68 | if (sortedKeys.length) { 69 | sortedKeys.sort(([, valueA], [, valueB]) => valueA - valueB) 70 | const howManyToDel = Math.floor(sortedKeys.length * 0.5) 71 | const sortedKeysToDel = sortedKeys.map(([key]) => key).slice(0, howManyToDel) 72 | if (sortedKeysToDel.length) { 73 | await delMany(sortedKeysToDel) 74 | } 75 | } 76 | } 77 | 78 | const setDefaultExpireDate = () => { 79 | const expireAt = new Date() 80 | return new Date( 81 | expireAt.getFullYear(), 82 | expireAt.getMonth(), 83 | expireAt.getDate(), 84 | 23, 85 | 59, 86 | 59 87 | ).getTime() 88 | } 89 | 90 | const defaultOptions = { 91 | expiresAt: setDefaultExpireDate(), 92 | storageAllowed: true 93 | } 94 | 95 | export default cache 96 | -------------------------------------------------------------------------------- /src/plugins/commonTable.js: -------------------------------------------------------------------------------- 1 | import { toRefs, ref, watch } from 'vue' 2 | import networkName from './networkName' 3 | 4 | export default function commonTable(props, ctx) { 5 | const { data, loading, filter } = toRefs(props) 6 | const filteredRows = ref([]) 7 | const filterTable = ref('') 8 | const rows = ref(data.value) 9 | 10 | const prettyName = (shortname) => { 11 | return networkName(shortname) 12 | } 13 | 14 | const getCellValue = (props, columnName) => { 15 | let col = props.colsMap[columnName] 16 | return col.format(col.field(props.row)) 17 | } 18 | 19 | const filterFct = (rows, terms, cols, cellValue) => { 20 | const lowerTerms = terms ? terms.toLowerCase() : '' 21 | filteredRows.value = rows.filter((row) => 22 | cols.some((col) => (cellValue(col, row) + '').toLowerCase().indexOf(lowerTerms) !== -1) 23 | ) 24 | return filteredRows.value 25 | } 26 | 27 | const dateHourShift = (datetime, shift) => { 28 | var res = new Date(datetime) 29 | var sign = shift < 0 ? -1 : 1 30 | res.setHours(res.getHours() + sign * Math.max(1, Math.abs(shift))) 31 | return res 32 | } 33 | 34 | const setRows = (newData) => { 35 | rows.value = newData 36 | } 37 | 38 | watch(filteredRows, (newValue) => { 39 | if (ctx.emit && ctx.emit.filteredRows) { 40 | ctx.emit('filteredRows', [filterTable.value, newValue]) 41 | } 42 | }) 43 | watch(filterTable, (newValue) => { 44 | if (newValue == '') filteredRows.value = rows.value 45 | }) 46 | watch(data, (newValue) => { 47 | rows.value = newValue 48 | }) 49 | watch(rows, (newValue) => { 50 | filteredRows.value = newValue 51 | }) 52 | watch(filter, (newValue) => { 53 | filterTable.value = newValue 54 | }) 55 | return { 56 | filteredRows, 57 | filterTable, 58 | rows, 59 | prettyName, 60 | getCellValue, 61 | filterFct, 62 | dateHourShift, 63 | setRows 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/plugins/delay.js: -------------------------------------------------------------------------------- 1 | import { AS_FAMILY } from './IhrApi' 2 | 3 | const DEFAULT_MIN_NPROBES = 10 4 | const DEFAULT_MIN_DEVIATION = 150 5 | const DEFAULT_MIN_DIFFMEDIAN = 15 6 | const DEFAULT_MAX_DIFFMEDIAN = 300 7 | const DEFAULT_AS_FAMILY = AS_FAMILY.v4 8 | 9 | export { 10 | DEFAULT_MIN_NPROBES, 11 | DEFAULT_MIN_DEVIATION, 12 | DEFAULT_MIN_DIFFMEDIAN, 13 | DEFAULT_MAX_DIFFMEDIAN, 14 | DEFAULT_AS_FAMILY 15 | } 16 | -------------------------------------------------------------------------------- /src/plugins/disco.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_DISCO_AVG_LEVEL = 10 2 | const DEFAULT_MIN_DISCO_DURATION = 5 3 | 4 | export { DEFAULT_DISCO_AVG_LEVEL, DEFAULT_MIN_DISCO_DURATION } 5 | -------------------------------------------------------------------------------- /src/plugins/networkName.js: -------------------------------------------------------------------------------- 1 | export default function networkName(shortname) { 2 | switch (shortname) { 3 | case 'IX23': 4 | return 'AMS-IX (Amsterdam)' 5 | case 'IX208': 6 | return 'DE-CIX (Frankfurt)' 7 | case 'IX438': 8 | return 'LINX (London)' 9 | case 'IX382': 10 | return 'INEX (Dublin)' 11 | case 'AS15169': 12 | return `Google (${shortname})` 13 | case 'AS21556': 14 | return `E-root (${shortname})` 15 | case 'AS25152': 16 | return `K-root (${shortname})` 17 | default: 18 | return shortname 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/plugins/report.js: -------------------------------------------------------------------------------- 1 | import { ref, computed } from 'vue' 2 | import { PROJECT_START_DATE } from './IhrApi' 3 | 4 | class DateInterval { 5 | constructor(begin, end) { 6 | this.begin = this.createDateAsUTC(begin) 7 | this.end = this.createDateAsUTC(end) 8 | } 9 | 10 | dayDiff() { 11 | return Math.ceil((this.end - this.begin) / 1000 / 60 / 60 / 24) 12 | } 13 | 14 | createDateAsUTC(date) { 15 | return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59)) 16 | } 17 | 18 | setHours() { 19 | this.begin.setUTCHours(0, 0, 0, 0) 20 | this.end.setUTCHours(23, 59, 59, 0) 21 | return this 22 | } 23 | } 24 | 25 | export default function report(defaultTimeRange = null) { 26 | const defaultTimeRangeVal = computed(() => { 27 | return defaultTimeRange ? defaultTimeRange : 3 28 | }) 29 | 30 | const getDateInterval = (dateObj, nDays) => { 31 | const end = new Date(dateObj) 32 | const begin = new Date(end.getTime()) 33 | begin.setUTCDate(begin.getUTCDate() - nDays) 34 | if (isNaN(begin.getTime()) || isNaN(end.getTime())) throw RangeError('invalid start or end') 35 | const newInterval = new DateInterval(begin, end) 36 | // newInterval.setHours() 37 | return newInterval 38 | } 39 | 40 | const interval = ref(getDateInterval(new Date(), defaultTimeRangeVal.value)) 41 | 42 | let fetch = ref(false) 43 | 44 | const minDate = computed(() => { 45 | return PROJECT_START_DATE 46 | }) 47 | 48 | const maxDate = computed(() => { 49 | return new Date() 50 | }) 51 | 52 | const reportDateFmt = computed(() => { 53 | var options = { 54 | year: 'numeric', 55 | month: 'long', 56 | day: '2-digit', 57 | timeZone: 'UTC' 58 | } 59 | return interval.value.end.toLocaleDateString(undefined, options) 60 | }) 61 | 62 | const startTime = computed(() => { 63 | return interval.value.begin 64 | }) 65 | 66 | const endTime = computed(() => { 67 | return interval.value.end 68 | }) 69 | 70 | const setReportDate = (event) => { 71 | interval.value = getDateInterval(event, defaultTimeRangeVal.value) 72 | } 73 | 74 | const utcString = (date) => { 75 | return date.toISOString() 76 | } 77 | 78 | return { 79 | interval, 80 | fetch, 81 | minDate, 82 | maxDate, 83 | reportDateFmt, 84 | startTime, 85 | endTime, 86 | defaultTimeRangeVal, 87 | setReportDate, 88 | getDateInterval, 89 | utcString 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/plugins/tracerouteFunctions.js: -------------------------------------------------------------------------------- 1 | const convertUnixTimestamp = (unixTimestamp) => { 2 | const date = new Date(unixTimestamp * 1000) 3 | 4 | const year = String(date.getFullYear()) 5 | const day = String(date.getDate()).padStart(2, '0') 6 | const monthNames = [ 7 | 'January', 8 | 'February', 9 | 'March', 10 | 'April', 11 | 'May', 12 | 'June', 13 | 'July', 14 | 'August', 15 | 'September', 16 | 'October', 17 | 'November', 18 | 'December' 19 | ] 20 | const month = monthNames[date.getMonth()] 21 | const hours = String(date.getHours()).padStart(2, '0') 22 | const minutes = String(date.getMinutes()).padStart(2, '0') 23 | 24 | return `${year} - ${day} ${month}, ${hours}:${minutes}` 25 | } 26 | 27 | const isPrivateIP = (ip) => { 28 | const privateRangesIPv4 = [ 29 | { start: '10.0.0.0', end: '10.255.255.255' }, 30 | { start: '172.16.0.0', end: '172.31.255.255' }, 31 | { start: '192.168.0.0', end: '192.168.255.255' } 32 | ] 33 | 34 | const privateRangesIPv6 = [ 35 | { start: 'fc00::', end: 'fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' }, 36 | { start: 'fe80::', end: 'febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff' } 37 | ] 38 | 39 | const ipToLong = (ip) => { 40 | return ip.split('.').reduce((ipInt, octet) => (ipInt << 8) + parseInt(octet, 10), 0) >>> 0 41 | } 42 | 43 | const ipToBigInt = (ip) => { 44 | return BigInt( 45 | `0x${ip 46 | .split(':') 47 | .map((part) => part.padStart(4, '0')) 48 | .join('')}` 49 | ) 50 | } 51 | 52 | const ipLong = ip.includes(':') ? ipToBigInt(ip) : ipToLong(ip) 53 | 54 | const isPrivateIPv4 = privateRangesIPv4.some((range) => { 55 | const start = ipToLong(range.start) 56 | const end = ipToLong(range.end) 57 | return ipLong >= start && ipLong <= end 58 | }) 59 | 60 | const isPrivateIPv6 = privateRangesIPv6.some((range) => { 61 | const start = ipToBigInt(range.start) 62 | const end = ipToBigInt(range.end) 63 | return ipLong >= start && ipLong <= end 64 | }) 65 | 66 | return isPrivateIPv4 || isPrivateIPv6 67 | } 68 | 69 | const calculateMedian = (values) => { 70 | if (!values || !values.length) return null 71 | const sorted = [...values].sort((a, b) => a - b) 72 | const mid = Math.floor(sorted.length / 2) 73 | return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2 74 | } 75 | 76 | export { convertUnixTimestamp, isPrivateIP, calculateMedian } 77 | -------------------------------------------------------------------------------- /src/styles/chart.css: -------------------------------------------------------------------------------- 1 | .IHR_chart { 2 | text-align: center; 3 | position: relative; 4 | } 5 | 6 | .IHR_chart h1 { 7 | font-size: 2rem; 8 | margin-bottom: 0px; 9 | font-weight: 400; 10 | line-height: 1; 11 | } 12 | 13 | .IHR_char-container { 14 | width: 90%; 15 | margin: 0 auto; 16 | } 17 | 18 | .IHR_loading-spinner { 19 | position: absolute; 20 | top: 35%; 21 | left: 49%; 22 | } 23 | .IHR_loading-spinner > * { 24 | width: 25%; 25 | height: 25%; 26 | } 27 | 28 | .IHR_description { 29 | max-width: 900px; 30 | font-size: 1.1875rem; 31 | } 32 | 33 | .IHR_api-table table { 34 | display: inline-block; 35 | margin: 0 auto; 36 | text-align: left; 37 | } 38 | 39 | .IHR_api-table p { 40 | text-align: left; 41 | text-transform: capitalize; 42 | color: #000; 43 | font-weight: 200; 44 | } 45 | 46 | .IHR_api-table h3 { 47 | color: #000; 48 | text-align: left; 49 | font-weight: 300; 50 | } 51 | 52 | .IHR_table-card { 53 | padding: 0px; 54 | background-color: #475057; 55 | color: #475057; 56 | } 57 | 58 | .IHR_table-close-button { 59 | position: absolute; 60 | right: 20px; 61 | z-index: 4; 62 | color: #808080; 63 | box-shadow: none; 64 | font-size: 1.1875rem; 65 | font-family: monospace; 66 | font-weight: 600; 67 | cursor: pointer; 68 | } 69 | 70 | .IHR_table-row { 71 | cursor: pointer; 72 | } 73 | 74 | .IHR_color-increment { 75 | font-weight: 600; 76 | } 77 | 78 | .IHR_important-cell { 79 | font-weight: bold; 80 | } 81 | 82 | .IHR_color-deviation-mid-threshold { 83 | color: #eb9329; 84 | } 85 | 86 | .IHR_color-deviation-high-threshold { 87 | color: #c41c00; 88 | } 89 | 90 | .q-td { 91 | white-space: normal !important; 92 | } 93 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | h1, 2 | .text-h1 { 3 | margin: 0; 4 | font-size: 2.6875rem !important; 5 | font-weight: 400; 6 | } 7 | 8 | h2, 9 | .text-h2 { 10 | margin: 0; 11 | font-size: 2rem !important; 12 | font-weight: 300; 13 | } 14 | 15 | h3, 16 | .text-h3 { 17 | margin: 0; 18 | font-size: 1.375rem !important; 19 | font-weight: 300; 20 | } 21 | 22 | a { 23 | color: #405057; 24 | } 25 | 26 | p:first-letter { 27 | text-transform: uppercase; 28 | } 29 | 30 | .IHR_label { 31 | text-align: right; 32 | font-weight: 500; 33 | } 34 | 35 | .IHR_errors-banner { 36 | background-color: #c10015; 37 | width: 100%; 38 | margin-top: -1em; 39 | } 40 | .IHR_errors-banner p { 41 | font-size: 1.1875rem; 42 | font-weight: 500; 43 | color: #fff; 44 | } 45 | .IHR_errors-banner p:first-letter { 46 | text-transform: capitalize; 47 | } 48 | .IHR_errors-banner p:first-child { 49 | margin-top: 0.4375rem; 50 | } 51 | .IHR_errors-banner .q-btn__content > div { 52 | font-weight: 600; 53 | } 54 | 55 | .IHR_errors-banner-animation-enter-active, 56 | .IHR_errors-banner-animation-leave-active { 57 | transition: all 0.5s; 58 | } 59 | 60 | .IHR_errors-banner-animation-enter, 61 | .IHR_errors-banner-animation-leave-to { 62 | height: 0em; 63 | } 64 | 65 | .IHR_delikify { 66 | color: #000; 67 | text-decoration: none; 68 | font-weight: 400; 69 | } 70 | -------------------------------------------------------------------------------- /src/views/Api.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/views/Contact.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 90 | -------------------------------------------------------------------------------- /src/views/MetisDeployment.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/views/PageNotFound.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 18 | 19 | 120 | -------------------------------------------------------------------------------- /src/views/TracerouteVisualizationTool.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 80 | 81 | 86 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import { quasar, transformAssetUrls } from '@quasar/vite-plugin' 5 | import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' 6 | import { existsSync } from 'fs' 7 | import VueDevTools from 'vite-plugin-vue-devtools' 8 | 9 | const dotPathFixPlugin = () => ({ 10 | name: "dot-path-fix-plugin", 11 | configureServer: (server) => { 12 | server.middlewares.use((req, _, next) => { 13 | const reqPath = req.url.split("?", 2)[0] 14 | if ( 15 | !req.url.startsWith("/@") && // virtual files provided by vite plugins 16 | !req.url.startsWith("/api/") && // api proxy, configured below 17 | !existsSync(`./public${reqPath}`) && // files served directly from public folder 18 | !existsSync(`.${reqPath}`) // actual files 19 | ) { 20 | req.url = "/" 21 | } 22 | next() 23 | }) 24 | }, 25 | }) 26 | 27 | // https://vitejs.dev/config/ 28 | export default defineConfig({ 29 | plugins: [ 30 | vue({ 31 | template: { transformAssetUrls } 32 | }), 33 | quasar(), 34 | VueI18nPlugin({ 35 | strictMessage: false, 36 | }), 37 | dotPathFixPlugin(), 38 | VueDevTools(), 39 | ], 40 | resolve: { 41 | alias: { 42 | '@': fileURLToPath(new URL('./src', import.meta.url)) 43 | } 44 | }, 45 | build: { 46 | target: ['es2022'] 47 | } 48 | }) 49 | --------------------------------------------------------------------------------