Update
14
.babelrc
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false
|
||||
}
|
||||
],
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"react-hot-loader/babel"
|
||||
]
|
||||
}
|
||||
71
.gitignore
vendored
|
|
@ -1,24 +1,65 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
env/
|
||||
|
||||
.vscode/
|
||||
|
||||
# backened #
|
||||
|
||||
/backend/secret_settings.py
|
||||
|
||||
*.log
|
||||
*.pot
|
||||
*.pyc
|
||||
__pycache__
|
||||
db.sqlite3
|
||||
media
|
||||
|
||||
# Backup files #
|
||||
*.bak
|
||||
|
||||
# Distribution / packaging
|
||||
.Python build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# frontend
|
||||
/frontend/.env
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
/frontend/node_modules
|
||||
/frontend/.pnp
|
||||
/frontend/.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
/frontend/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
/frontend/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
/frontend/.DS_Store
|
||||
/frontend/.env.local
|
||||
/frontend/.env.development.local
|
||||
/frontend/.env.test.local
|
||||
/frontend/.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
/frontend/npm-debug.log*
|
||||
/frontend/yarn-debug.log*
|
||||
/frontend/yarn-error.log*
|
||||
0
backend/backend/__init__.py
Normal file
16
backend/backend/asgi.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
ASGI config for backend project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
132
backend/backend/settings.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
"""
|
||||
Django settings for backend project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.2.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.2/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from secret_settings import *
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'corsheaders',
|
||||
'rest_framework',
|
||||
'django_filters',
|
||||
'vytal',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'backend.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'backend.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
CORS_ORIGIN_WHITELIST = [
|
||||
'http://localhost:3000'
|
||||
]
|
||||
12
backend/backend/urls.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from rest_framework import routers
|
||||
from vytal import views
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'fingerprint', views.FingerprintView, 'fingerprint')
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include(router.urls)),
|
||||
]
|
||||
16
backend/backend/wsgi.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for backend project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
22
backend/manage.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
backend/vytal/__init__.py
Normal file
11
backend/vytal/admin.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from django.contrib import admin
|
||||
from .models import Fingerprint
|
||||
|
||||
|
||||
class FingerprintAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'hash')
|
||||
|
||||
# Register your models here.
|
||||
|
||||
|
||||
admin.site.register(Fingerprint, FingerprintAdmin)
|
||||
6
backend/vytal/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class VytalConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'vytal'
|
||||
22
backend/vytal/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 3.2.5 on 2021-07-18 04:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Fingerprint',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('hash', models.CharField(max_length=32)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
backend/vytal/migrations/__init__.py
Normal file
11
backend/vytal/models.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
||||
|
||||
class Fingerprint(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
hash = models.CharField(max_length=32)
|
||||
|
||||
def _str_(self):
|
||||
return self.name
|
||||
8
backend/vytal/serializers.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from rest_framework import serializers
|
||||
from .models import Fingerprint
|
||||
|
||||
|
||||
class FingerprintSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Fingerprint
|
||||
fields = ('id', 'name', 'hash')
|
||||
3
backend/vytal/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
14
backend/vytal/views.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from django.shortcuts import render
|
||||
from rest_framework import viewsets
|
||||
from .serializers import FingerprintSerializer
|
||||
from .models import Fingerprint
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
||||
# Create your views here.
|
||||
|
||||
|
||||
class FingerprintView(viewsets.ModelViewSet):
|
||||
serializer_class = FingerprintSerializer
|
||||
queryset = Fingerprint.objects.all()
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['hash']
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"name": "Vytal",
|
||||
"description": "An Extension To Show You What Trackers See",
|
||||
"manifest_version": 2,
|
||||
"version": "1.0.0",
|
||||
"permissions": ["storage"],
|
||||
"icons": {
|
||||
"16": "icon_16.png",
|
||||
"32": "icon_32.png",
|
||||
"48": "icon_48.png",
|
||||
"128": "icon_128.png"
|
||||
},
|
||||
"browser_action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
"16": "icon_16.png",
|
||||
"32": "icon_32.png",
|
||||
"48": "icon_48.png",
|
||||
"128": "icon_128.png"
|
||||
}
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": false
|
||||
},
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "vytal@example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,5 +32,7 @@ module.exports = {
|
|||
],
|
||||
'react/jsx-one-expression-per-line': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'no-bitwise': 'off'
|
||||
},
|
||||
};
|
||||
|
|
@ -12,10 +12,12 @@ Vytal contains no ads and signup is not required.
|
|||
|
||||
Download for Chrome:
|
||||
|
||||
https://chrome.google.com/webstore/detail/vytal/ncbknoohfjmcfneopnfkapmkblaenokb
|
||||
https://chrome.google.com/webstore/detail/Vytal/ncbknoohfjmcfneopnfkapmkblaenokb
|
||||
|
||||
Download for Firefox:
|
||||
|
||||
https://addons.mozilla.org/en-US/firefox/addon/vytal
|
||||
https://addons.mozilla.org/en-US/firefox/addon/Vytal
|
||||
|
||||

|
||||
Github for Extension:
|
||||
|
||||
https://github.com/z0ccc/Vytal-extension
|
||||
51
frontend/package.json
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"name": "vytal",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"proxy": "http://localhost:8000",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-pro": "^5.15.3",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||
"@fortawesome/pro-light-svg-icons": "^5.15.3",
|
||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||
"axios": "0.21.1",
|
||||
"bowser": "^2.11.0",
|
||||
"crypto-js": "^4.0.0",
|
||||
"emailjs-com": "^3.1.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-tsparticles": "^1.28.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 786 B After Width: | Height: | Size: 786 B |
42
frontend/public/index.html
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Vytal</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
15
frontend/public/manifest.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"short_name": "Vytal",
|
||||
"name": "Vytal",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
frontend/public/robots.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
9
frontend/src/App.test.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
const { getByText } = render(<App />);
|
||||
const linkElement = getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
13
frontend/src/components/App.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import Particles from 'react-tsparticles';
|
||||
import particlesOptions from '../particles.json';
|
||||
import MainColumn from './MainColumn';
|
||||
import '../styles/App.css';
|
||||
|
||||
const App = () => (
|
||||
<div className="App">
|
||||
<Particles options={particlesOptions} />
|
||||
<MainColumn />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default App;
|
||||
70
frontend/src/components/ConnectBlock.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import ScanBlock from './ScanBlock';
|
||||
import Table from './Table';
|
||||
|
||||
const ConnectBlock = () => {
|
||||
const [connectData, setConnectData] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://ip-api.com/json')
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setConnectData(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const detectTor = () => {
|
||||
const date = new Date();
|
||||
if (
|
||||
navigator.plugins.length === 0 &&
|
||||
date.getTimezoneOffset() === 0 &&
|
||||
window.outerWidth === window.screen.availWidth &&
|
||||
window.outerHeight === window.screen.availHeight
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const data = [
|
||||
{
|
||||
key: 'ipAddress',
|
||||
title: 'IP address',
|
||||
value: connectData.query,
|
||||
},
|
||||
{
|
||||
key: 'isp',
|
||||
title: 'ISP',
|
||||
value: connectData.isp,
|
||||
},
|
||||
{
|
||||
key: 'org',
|
||||
title: 'Organization',
|
||||
value: connectData.org,
|
||||
},
|
||||
{
|
||||
key: 'asn',
|
||||
title: 'ASN',
|
||||
value: connectData.as,
|
||||
},
|
||||
{
|
||||
key: 'tor',
|
||||
title: 'Tor browser detected',
|
||||
value: detectTor() ? 'True' : 'False',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ScanBlock>
|
||||
<h1>Connection</h1>
|
||||
<Table data={data} />
|
||||
<p>
|
||||
<b>Explanation:</b> JavaScript can be used to read various information
|
||||
about your software. This information can be used to create a
|
||||
fingerprint.
|
||||
</p>
|
||||
</ScanBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectBlock;
|
||||
25
frontend/src/components/ContentList.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { ReactComponent as WifiIcon } from '../images/wifi.svg';
|
||||
import { ReactComponent as BrowserIcon } from '../images/browser.svg';
|
||||
import { ReactComponent as DesktopIcon } from '../images/desktop.svg';
|
||||
|
||||
const Icons = {
|
||||
wifi: <WifiIcon />,
|
||||
browser: <BrowserIcon />,
|
||||
desktop: <DesktopIcon />,
|
||||
};
|
||||
|
||||
const ContentList = ({ items }) => (
|
||||
<div className="contentList">
|
||||
{items.map((item) => (
|
||||
<div className="contentItem" key={item.title}>
|
||||
<div className="contentIcon">{Icons[item.icon]}</div>
|
||||
<div className="contentText">
|
||||
<h2>{item.title}</h2>
|
||||
<div className="contentBody">{item.body}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ContentList;
|
||||
39
frontend/src/components/FiltersBlock.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import ScanBlock from './ScanBlock';
|
||||
import Table from './Table';
|
||||
|
||||
const FiltersBlock = () => {
|
||||
const [adBlockDetected, setAdBlockDetected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://www3.doubleclick.net', {
|
||||
method: 'HEAD',
|
||||
mode: 'no-cors',
|
||||
cache: 'no-store',
|
||||
}).catch(() => {
|
||||
setAdBlockDetected(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const data = [
|
||||
{
|
||||
key: 'adBlock',
|
||||
title: 'Adblock detected',
|
||||
value: adBlockDetected ? 'True' : 'False',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ScanBlock>
|
||||
<h1>Content Filters</h1>
|
||||
<Table data={data} />
|
||||
<p>
|
||||
<b>Explanation:</b> JavaScript can be used to read various information
|
||||
about your hardware. This information can be used to create a
|
||||
fingerprint.
|
||||
</p>
|
||||
</ScanBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default FiltersBlock;
|
||||
151
frontend/src/components/FingerprintBlock.js
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import md5 from 'crypto-js/md5';
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import ScanBlock from './ScanBlock';
|
||||
import Table from './Table';
|
||||
|
||||
const FingerprintBlock = () => {
|
||||
const [name, setName] = useState('');
|
||||
const [saved, setSaved] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
axios.get(`/api/fingerprint/?hash=${hash}`).then((response) => {
|
||||
if (response.data.length !== 0) {
|
||||
setName(response.data[response.data.length - 1].name);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSave = (e) => {
|
||||
e.preventDefault();
|
||||
axios.post('/api/fingerprint/', {
|
||||
name: e.target[0].value,
|
||||
hash,
|
||||
});
|
||||
setSaved(true);
|
||||
};
|
||||
|
||||
const gl = document.createElement('canvas').getContext('webgl');
|
||||
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
|
||||
const fingerprintData = [
|
||||
{
|
||||
key: 'screenResolution',
|
||||
value: `${window.screen.width}x${window.screen.height}`,
|
||||
},
|
||||
{
|
||||
key: 'colorResolution',
|
||||
value: window.screen.colorDepth,
|
||||
},
|
||||
{
|
||||
key: 'deviceMemory',
|
||||
value: navigator.deviceMemory ? `${navigator.deviceMemory}GB` : 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'cpuCores',
|
||||
value: navigator.hardwareConcurrency || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'maxTouchpoints',
|
||||
value: navigator.maxTouchPoints,
|
||||
},
|
||||
{
|
||||
key: 'webGLVendor',
|
||||
title: 'WebGL vendor',
|
||||
value: gl.getParameter(ext.UNMASKED_VENDOR_WEBGL),
|
||||
},
|
||||
{
|
||||
key: 'webglRenderer',
|
||||
title: 'WebGL renderer',
|
||||
value: gl.getParameter(ext.UNMASKED_RENDERER_WEBGL),
|
||||
},
|
||||
{
|
||||
key: 'platform',
|
||||
value: navigator.platform,
|
||||
},
|
||||
{
|
||||
key: 'userAgent',
|
||||
value: navigator.userAgent,
|
||||
},
|
||||
{
|
||||
key: 'preferredLanguage',
|
||||
value: navigator.language,
|
||||
},
|
||||
{
|
||||
key: 'languages',
|
||||
title: 'Languages',
|
||||
value: navigator.languages,
|
||||
},
|
||||
{
|
||||
key: 'timezone',
|
||||
value: Intl.DateTimeFormat().resolvedOptions().timeZone || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'cookiesEnabled',
|
||||
value: navigator.cookieEnabled,
|
||||
},
|
||||
{
|
||||
key: 'javaEnabled',
|
||||
value: navigator.javaEnabled(),
|
||||
},
|
||||
{
|
||||
key: 'dntHeader',
|
||||
value: navigator.doNotTrack,
|
||||
},
|
||||
{
|
||||
key: 'automatedBrowser',
|
||||
value: navigator.webdriver,
|
||||
},
|
||||
{
|
||||
key: 'plugins',
|
||||
value: navigator.plugins,
|
||||
},
|
||||
];
|
||||
|
||||
const hash = md5(JSON.stringify(fingerprintData)).toString();
|
||||
|
||||
const tableData = [
|
||||
{
|
||||
key: 'hash',
|
||||
title: 'Hash',
|
||||
value: hash,
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
title: 'Name',
|
||||
value: name,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ScanBlock>
|
||||
<h1>Fingerprint</h1>
|
||||
{name ? (
|
||||
<Table data={tableData} />
|
||||
) : (
|
||||
<div className="boxWrap">
|
||||
<div className="hash">{hash}</div>
|
||||
</div>
|
||||
)}
|
||||
<p>
|
||||
<b>Explanation:</b> JavaScript can be used to read various information
|
||||
about your software. This information can be used to create a
|
||||
fingerprint.
|
||||
</p>
|
||||
{saved ? (
|
||||
<p>Success! Re-scan browser.</p>
|
||||
) : (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
handleSave(e);
|
||||
}}
|
||||
>
|
||||
<input type="text" id="name" name="name" placeholder="Enter name" />
|
||||
<input type="submit" id="saveButton" value="Save" maxLength="100" />
|
||||
</form>
|
||||
)}
|
||||
</ScanBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default FingerprintBlock;
|
||||
60
frontend/src/components/FontsBlock.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { useEffect } from 'react';
|
||||
import ScanBlock from './ScanBlock';
|
||||
import fontList from '../fontList.json';
|
||||
|
||||
const FontsBlock = () => {
|
||||
useEffect(() => {
|
||||
const families = ['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy'];
|
||||
const famLen = families.length;
|
||||
const fontsEl = document.querySelector('.fonts');
|
||||
const width = [];
|
||||
const height = [];
|
||||
const span = document.createElement('span');
|
||||
|
||||
span.innerHTML = 'AaBbCcWwLl:/!@';
|
||||
span.style.fontSize = '100px';
|
||||
|
||||
for (let i = 0; i < famLen; i++) {
|
||||
span.style.fontFamily = families[i];
|
||||
fontsEl.appendChild(span);
|
||||
width[i] = span.offsetWidth;
|
||||
height[i] = span.offsetHeight;
|
||||
fontsEl.removeChild(span);
|
||||
}
|
||||
|
||||
function detect(font) {
|
||||
let detected = false;
|
||||
for (let i = 0; i < famLen; i++) {
|
||||
span.style.fontFamily = `"${font}" ,${families[i]}`;
|
||||
fontsEl.appendChild(span);
|
||||
if (span.offsetWidth !== width[i] || span.offsetHeight !== height[i]) {
|
||||
detected = true;
|
||||
}
|
||||
fontsEl.removeChild(span);
|
||||
}
|
||||
return detected;
|
||||
}
|
||||
|
||||
let fontStr = '';
|
||||
fontList.forEach((item) => {
|
||||
if (detect(item.font)) {
|
||||
fontStr += `${item.font}, `;
|
||||
}
|
||||
});
|
||||
fontsEl.textContent = fontStr.slice(0, -2);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ScanBlock>
|
||||
<h1>System Fonts</h1>
|
||||
<div className="fonts boxWrap" />
|
||||
<p>
|
||||
<b>Explanation:</b> JavaScript can be used to read various information
|
||||
about your hardware. This information can be used to create a
|
||||
fingerprint.
|
||||
</p>
|
||||
</ScanBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default FontsBlock;
|
||||
86
frontend/src/components/HardwareBlock.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import ScanBlock from './ScanBlock';
|
||||
import Table from './Table';
|
||||
|
||||
const HardwareBlock = () => {
|
||||
const [batLevel, setBatLevel] = useState('');
|
||||
const [batStatus, setBatStatus] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// waits for battery info to resolve and then updates
|
||||
if ('getBattery' in navigator) {
|
||||
navigator.getBattery().then((res) => {
|
||||
setBatLevel(`${Math.round(res.level * 100)}%`);
|
||||
setBatStatus(res.charging ? 'Charging' : 'Not charging');
|
||||
});
|
||||
} else {
|
||||
setBatLevel('N/A');
|
||||
setBatStatus('N/A');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const gl = document.createElement('canvas').getContext('webgl');
|
||||
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
|
||||
// Hardware table items
|
||||
const data = [
|
||||
{
|
||||
key: 'screenResolution',
|
||||
title: 'Screen resolution',
|
||||
value: `${window.screen.width}x${window.screen.height}`,
|
||||
},
|
||||
{
|
||||
key: 'colorResolution',
|
||||
title: 'Color Resolution',
|
||||
value: window.screen.colorDepth,
|
||||
},
|
||||
{
|
||||
key: 'batteryLevel',
|
||||
title: 'Battery level',
|
||||
value: batLevel,
|
||||
},
|
||||
{
|
||||
key: 'batteryStatus',
|
||||
title: 'Battery status',
|
||||
value: batStatus,
|
||||
},
|
||||
{
|
||||
key: 'deviceMemory',
|
||||
title: 'Device memory',
|
||||
value: navigator.deviceMemory ? `${navigator.deviceMemory}GB` : 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'cpuCores',
|
||||
title: '# of CPU cores',
|
||||
value: navigator.hardwareConcurrency || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'maxTouchpoints',
|
||||
title: 'Max touchpoints',
|
||||
value: navigator.maxTouchPoints,
|
||||
},
|
||||
{
|
||||
key: 'webGLVendor',
|
||||
title: 'WebGL vendor',
|
||||
value: gl.getParameter(ext.UNMASKED_VENDOR_WEBGL),
|
||||
},
|
||||
{
|
||||
key: 'webglRenderer',
|
||||
title: 'WebGL renderer',
|
||||
value: gl.getParameter(ext.UNMASKED_RENDERER_WEBGL),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<ScanBlock>
|
||||
<h1>Hardware</h1>
|
||||
<Table data={data} />
|
||||
<p>
|
||||
<b>Explanation:</b> JavaScript can be used to read various information
|
||||
about your hardware. This information can be used to create a
|
||||
fingerprint.
|
||||
</p>
|
||||
</ScanBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default HardwareBlock;
|
||||
65
frontend/src/components/LocationBlock.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import ScanBlock from './ScanBlock';
|
||||
import Table from './Table';
|
||||
|
||||
const LocationBlock = () => {
|
||||
const [locationData, setLocationData] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
fetch('http://ip-api.com/json')
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setLocationData(data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const mapUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${locationData.lat},${locationData.lon}&markers=color:red%7Clabel:%7C${locationData.lat},${locationData.lon}&size=500x200&zoom=10&key=AIzaSyB-YN-X8PGBSPd7NOaQu4csVhgJUnF3ZGk`;
|
||||
|
||||
const data = [
|
||||
{
|
||||
key: 'country',
|
||||
title: 'Country',
|
||||
value: locationData.country,
|
||||
},
|
||||
{
|
||||
key: 'regionName',
|
||||
title: 'Region',
|
||||
value: locationData.regionName,
|
||||
},
|
||||
{
|
||||
key: 'lat',
|
||||
title: 'City',
|
||||
value: locationData.city,
|
||||
},
|
||||
{
|
||||
key: 'zip',
|
||||
title: 'Zip code',
|
||||
value: locationData.zip,
|
||||
},
|
||||
{
|
||||
key: 'lat',
|
||||
title: 'Latitude',
|
||||
value: locationData.lat,
|
||||
},
|
||||
{
|
||||
key: 'lon',
|
||||
title: 'Longitude',
|
||||
value: locationData.lon,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ScanBlock>
|
||||
<h1>Location</h1>
|
||||
<img src={mapUrl} alt="Map of current location" />
|
||||
<Table data={data} />
|
||||
<p>
|
||||
<b>Explanation:</b> JavaScript can be used to read various information
|
||||
about your software. This information can be used to create a
|
||||
fingerprint.
|
||||
</p>
|
||||
</ScanBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocationBlock;
|
||||
11
frontend/src/components/Logo.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { ReactComponent as LogoImg } from '../images/logo.svg';
|
||||
|
||||
const Logo = () => (
|
||||
<div className="logoWrap">
|
||||
<a href="/" className="logo">
|
||||
<LogoImg />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Logo;
|
||||
21
frontend/src/components/MainColumn.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { useState } from 'react';
|
||||
import Logo from './Logo';
|
||||
import StartBlock from './StartBlock';
|
||||
import ScanBlocks from './ScanBlocks';
|
||||
|
||||
const MainColumn = () => {
|
||||
const [scan, setScan] = useState(false);
|
||||
return (
|
||||
<div className="centerBlockOuter">
|
||||
<div className="centerBlockInner">
|
||||
<Logo />
|
||||
{scan ? (
|
||||
<ScanBlocks />
|
||||
) : (
|
||||
<StartBlock scan={scan} onScanClick={setScan} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default MainColumn;
|
||||
9
frontend/src/components/ScanBlock.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const ContentBlock = ({ children }) => (
|
||||
<div className="contentBlock">
|
||||
<div className="contentItem">
|
||||
<div className="contentText">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ContentBlock;
|
||||
21
frontend/src/components/ScanBlocks.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import FingerprintBlock from './FingerprintBlock';
|
||||
import LocationBlock from './LocationBlock';
|
||||
import HardwareBlock from './HardwareBlock';
|
||||
import SoftwareBlock from './SoftwareBlock';
|
||||
import ConnectBlock from './ConnectBlock';
|
||||
import FiltersBlock from './FiltersBlock';
|
||||
import FontsBlock from './FontsBlock';
|
||||
|
||||
const ScanBlocks = () => (
|
||||
<div>
|
||||
<FingerprintBlock />
|
||||
<LocationBlock />
|
||||
<ConnectBlock />
|
||||
<HardwareBlock />
|
||||
<SoftwareBlock />
|
||||
<FiltersBlock />
|
||||
<FontsBlock />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ScanBlocks;
|
||||
133
frontend/src/components/SoftwareBlock.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import Bowser from 'bowser';
|
||||
import ScanBlock from './ScanBlock';
|
||||
import Table from './Table';
|
||||
|
||||
const HardwareBlock = () => {
|
||||
// sorts array into comma separated list
|
||||
const sortArr = (arr) => {
|
||||
const arrLength = arr.length;
|
||||
let list = '';
|
||||
for (let i = 0; i < arrLength; i++) {
|
||||
if (i !== 0) list += ', ';
|
||||
list += arr[i];
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
// sorts plugins object into comma separated list
|
||||
const sortPlugins = (data) => {
|
||||
const { length } = data;
|
||||
|
||||
let list = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i !== 0) list += ', ';
|
||||
list += data[i].name;
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
const uaResult = Bowser.parse(navigator.userAgent);
|
||||
const date = new Date();
|
||||
|
||||
// Software table items
|
||||
const data = [
|
||||
{
|
||||
key: 'browser',
|
||||
title: 'Browser',
|
||||
value: uaResult.browser.name,
|
||||
},
|
||||
{
|
||||
key: 'browserVersion',
|
||||
title: 'Browser version',
|
||||
value: uaResult.browser.version,
|
||||
},
|
||||
{
|
||||
key: 'browserEngine',
|
||||
title: 'Browser engine',
|
||||
value: uaResult.browser.engine || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'os',
|
||||
title: 'OS',
|
||||
value: `${uaResult.os.name} ${uaResult.os.versionName}`,
|
||||
},
|
||||
{
|
||||
key: 'osVersion',
|
||||
title: 'OS version',
|
||||
value: uaResult.os.version,
|
||||
},
|
||||
{
|
||||
key: 'platform',
|
||||
title: 'Platform',
|
||||
value: navigator.platform,
|
||||
},
|
||||
{
|
||||
key: 'systemType',
|
||||
title: 'System type',
|
||||
value: uaResult.platform.type,
|
||||
},
|
||||
{
|
||||
key: 'userAgent',
|
||||
title: 'User Agent',
|
||||
value: navigator.userAgent || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'preferredLanguage',
|
||||
title: 'Preferred language',
|
||||
value: navigator.language || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'languages',
|
||||
title: 'Languages',
|
||||
value: sortArr(navigator.languages) || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'timezone',
|
||||
title: 'Timezone',
|
||||
value: Intl.DateTimeFormat().resolvedOptions().timeZone || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'timezoneOffset',
|
||||
title: 'Timezone offset',
|
||||
value: date.getTimezoneOffset() || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'cookiesEnabled',
|
||||
title: 'Cookies enabled',
|
||||
value: navigator.cookieEnabled ? 'True' : 'False',
|
||||
},
|
||||
{
|
||||
key: 'javaEnabled',
|
||||
title: 'Java enabled',
|
||||
value: navigator.javaEnabled() ? 'True' : 'False',
|
||||
},
|
||||
{
|
||||
key: 'dntHeader',
|
||||
title: 'DNT header enabled',
|
||||
value: navigator.doNotTrack ? 'True' : 'False',
|
||||
},
|
||||
{
|
||||
key: 'automatedBrowser',
|
||||
title: 'Automated browser',
|
||||
value: navigator.webdriver ? 'True' : 'False',
|
||||
},
|
||||
{
|
||||
key: 'plugins',
|
||||
title: 'Plugins',
|
||||
value: sortPlugins(navigator.plugins) || 'N/A',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<ScanBlock>
|
||||
<h1>Software</h1>
|
||||
<Table data={data} />
|
||||
<p>
|
||||
<b>Explanation:</b> JavaScript can be used to read various information
|
||||
about your software. This information can be used to create a
|
||||
fingerprint.
|
||||
</p>
|
||||
</ScanBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default HardwareBlock;
|
||||
56
frontend/src/components/StartBlock.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { useCallback } from 'react';
|
||||
import ContentList from './ContentList';
|
||||
import ScanBlock from './ScanBlock';
|
||||
|
||||
const contentItems = [
|
||||
{
|
||||
title: 'Hardware',
|
||||
icon: 'desktop',
|
||||
body: 'Browsers reveal bits of identifiable information. This data can be combined into a digital fingerprint which can be used to follow you around the web.',
|
||||
},
|
||||
{
|
||||
title: 'Software',
|
||||
icon: 'browser',
|
||||
body: 'Browsers reveal bits of identifiable information. This data can be combined into a digital fingerprint which can be used to follow you around the web.',
|
||||
},
|
||||
{
|
||||
title: 'Connection',
|
||||
icon: 'wifi',
|
||||
body: 'Browsers reveal bits of identifiable information. This data can be combined into a digital fingerprint which can be used to follow you around the web.',
|
||||
},
|
||||
];
|
||||
|
||||
const StartBlock = ({ onScanClick }) => {
|
||||
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
||||
|
||||
const handleInputClick = async () => {
|
||||
document.getElementById('scanButton').value = 'Loading...';
|
||||
await delay(2000);
|
||||
startScan();
|
||||
};
|
||||
|
||||
const startScan = useCallback(() => {
|
||||
onScanClick(true);
|
||||
}, [onScanClick]);
|
||||
|
||||
return (
|
||||
<ScanBlock>
|
||||
<h2>About</h2>
|
||||
<div className="contentBody">
|
||||
With the Vytal Browser Privacy Check, you can determine which traces you
|
||||
or your browser leave while surfing. Our test is intended to raise
|
||||
awareness of which data can be used by websites and advertisers to
|
||||
create a profile of you or to track your activities online.
|
||||
</div>
|
||||
<ContentList items={contentItems} />
|
||||
<input
|
||||
type="submit"
|
||||
onClick={handleInputClick}
|
||||
id="scanButton"
|
||||
value="Scan Browser"
|
||||
/>
|
||||
</ScanBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default StartBlock;
|
||||
16
frontend/src/components/Table.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
const Table = ({ data }) => (
|
||||
<div className="tableWrap">
|
||||
<table>
|
||||
{data.map((item) => (
|
||||
<tbody key={item.title}>
|
||||
<tr>
|
||||
<td>{item.title}</td>
|
||||
<td>{item.value}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
))}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Table;
|
||||
1
frontend/src/fontList.json
Normal file
5
frontend/src/images/browser.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<g fill="#9fa6b2">
|
||||
<path d="M464 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h416c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM32 80c0-8.8 7.2-16 16-16h48v64H32V80zm448 352c0 8.8-7.2 16-16 16H48c-8.8 0-16-7.2-16-16V160h448v272zm0-304H128V64h336c8.8 0 16 7.2 16 16v48z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 360 B |
5
frontend/src/images/desktop.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
|
||||
<g fill="#9fa6b2">
|
||||
<path d="M528 0H48C21.5 0 0 21.5 0 48v288c0 26.5 21.5 48 48 48h192l-24 96h-72c-8.8 0-16 7.2-16 16s7.2 16 16 16h288c8.8 0 16-7.2 16-16s-7.2-16-16-16h-72l-24-96h192c26.5 0 48-21.5 48-48V48c0-26.5-21.5-48-48-48zM249 480l16-64h46l16 64h-78zm295-144c0 8.8-7.2 16-16 16H48c-8.8 0-16-7.2-16-16V48c0-8.8 7.2-16 16-16h480c8.8 0 16 7.2 16 16v288z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 451 B |
|
|
@ -4,7 +4,7 @@
|
|||
version="1"
|
||||
viewBox="0 0 4389 1056"
|
||||
>
|
||||
<g fill="#943ec5" stroke="none" transform="matrix(.1 0 0 -.1 0 1056)">
|
||||
<g fill="#fff" stroke="none" transform="matrix(.1 0 0 -.1 0 1056)">
|
||||
<path d="M4890 10554c-19-2-102-11-185-20-814-84-1627-377-2332-841-666-437-1242-1047-1652-1748-345-590-582-1279-670-1950-34-261-44-422-44-710 0-388 28-679 99-1035C523 2160 2153 526 4240 106c360-73 640-99 1040-99 488 0 847 44 1305 160 665 168 1310 477 1860 891 1004 754 1703 1833 1983 3062 146 636 168 1351 61 2010-273 1699-1362 3160-2913 3911-540 261-1091 420-1716 495-100 12-240 17-535 19-220 2-416 1-435-1zm870-978c947-115 1772-494 2460-1130 303-281 615-672 815-1025 491-866 669-1875 504-2861-113-681-390-1322-814-1885-233-310-552-627-869-862-1529-1139-3637-1136-5161 7-391 294-746 669-1012 1070-367 553-599 1170-683 1814-113 869 22 1705 399 2472 103 210 151 294 287 499 316 476 726 886 1199 1199 340 225 629 368 999 495 349 120 681 187 1106 225 112 10 641-3 770-18z"></path>
|
||||
<path d="M5045 8784c-729-59-1353-298-1925-740-149-114-480-445-594-594-405-524-638-1090-722-1755-22-172-25-595-6-760 86-742 352-1362 822-1916 176-207 457-462 565-513 153-73 342-56 483 44 140 98 210 245 199 420-9 156-62 251-211 377-479 408-770 907-876 1504-31 174-39 528-16 719 93 759 503 1435 1121 1847 275 183 609 315 946 372 442 76 912 34 1329-116 593-214 1089-664 1375-1248 194-396 270-726 270-1165-1-320-34-522-135-815-143-414-360-722-786-1116-311-287-141-788 284-834 71-8 180 13 256 51 97 47 353 285 519 482 474 563 751 1223 818 1954 17 188 6 625-19 798-99 656-347 1220-760 1730-118 144-361 385-514 507-520 416-1102 662-1770 748-118 16-543 28-653 19z"></path>
|
||||
<path d="M5175 6514c-92-14-198-39-260-61-458-165-766-607-768-1103-1-309 89-544 306-792 141-160 207-284 259-481 21-80 22-101 27-972l6-890 24-60c79-199 237-332 440-372 282-56 561 117 657 407 17 51 19 120 24 935 6 867 6 881 28 958 51 183 137 343 254 474 207 230 308 486 308 782 0 328-118 612-349 842-171 170-340 263-576 314-69 16-321 28-380 19z"></path>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
5
frontend/src/images/wifi.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512">
|
||||
<g fill="#9fa6b2">
|
||||
<path d="M320 320c-44.18 0-80 35.82-80 80 0 44.19 35.83 80 80 80 44.19 0 80-35.84 80-80 0-44.18-35.82-80-80-80zm0 128c-26.47 0-48-21.53-48-48s21.53-48 48-48 48 21.53 48 48-21.53 48-48 48zm316.21-290.05C459.22-9.9 180.95-10.06 3.79 157.95c-4.94 4.69-5.08 12.51-.26 17.32l5.69 5.69c4.61 4.61 12.07 4.74 16.8.25 164.99-156.39 423.64-155.76 587.97 0 4.73 4.48 12.19 4.35 16.8-.25l5.69-5.69c4.81-4.81 4.67-12.63-.27-17.32zM526.02 270.31c-117.34-104.48-294.86-104.34-412.04 0-5.05 4.5-5.32 12.31-.65 17.2l5.53 5.79c4.46 4.67 11.82 4.96 16.66.67 105.17-93.38 264-93.21 368.98 0 4.83 4.29 12.19 4.01 16.66-.67l5.53-5.79c4.65-4.89 4.38-12.7-.67-17.2z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 746 B |
17
frontend/src/index.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './styles/index.css';
|
||||
import App from './components/App';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorker.unregister();
|
||||
73
frontend/src/particles.json
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"fpsLimit": 60,
|
||||
"interactivity": {
|
||||
"detectsOn": "window",
|
||||
"events": {
|
||||
"onClick": {
|
||||
"enable": false,
|
||||
"mode": "push"
|
||||
},
|
||||
"onHover": {
|
||||
"enable": true,
|
||||
"mode": "bubble"
|
||||
},
|
||||
"resize": true
|
||||
},
|
||||
"modes": {
|
||||
"bubble": {
|
||||
"distance": 150,
|
||||
"duration": 2,
|
||||
"opacity": 0.5,
|
||||
"size": 15
|
||||
},
|
||||
"push": {
|
||||
"quantity": 4
|
||||
},
|
||||
"repulse": {
|
||||
"distance": 200,
|
||||
"duration": 0.4
|
||||
}
|
||||
}
|
||||
},
|
||||
"particles": {
|
||||
"color": {
|
||||
"value": "#ffffff"
|
||||
},
|
||||
"links": {
|
||||
"color": "#ffffff",
|
||||
"distance": 150,
|
||||
"enable": true,
|
||||
"opacity": 0.2,
|
||||
"width": 1
|
||||
},
|
||||
"collisions": {
|
||||
"enable": true
|
||||
},
|
||||
"move": {
|
||||
"direction": "none",
|
||||
"enable": true,
|
||||
"outMode": "bounce",
|
||||
"random": false,
|
||||
"speed": 0.2,
|
||||
"straight": false
|
||||
},
|
||||
"number": {
|
||||
"density": {
|
||||
"enable": true,
|
||||
"value_area": 800
|
||||
},
|
||||
"value": 70
|
||||
},
|
||||
"opacity": {
|
||||
"value": 0.4
|
||||
},
|
||||
"shape": {
|
||||
"type": "circle"
|
||||
},
|
||||
"size": {
|
||||
"random": true,
|
||||
"value": 5
|
||||
}
|
||||
},
|
||||
"detectRetina": true
|
||||
}
|
||||
142
frontend/src/serviceWorker.js
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/* eslint-disable */
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' },
|
||||
})
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then(registration => {
|
||||
registration.unregister();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
5
frontend/src/setupTests.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
194
frontend/src/styles/App.css
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
:root {
|
||||
--main: #943ec5;
|
||||
--grey: #9fa6b2;
|
||||
--text: #4b5563;
|
||||
--border: #ddd;
|
||||
}
|
||||
|
||||
.App {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#tsparticles {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
background: rgb(87, 35, 117);
|
||||
background: linear-gradient(
|
||||
165deg,
|
||||
rgba(87, 35, 117, 1) 0%,
|
||||
rgba(148, 62, 197, 1) 55%,
|
||||
rgba(211, 176, 231, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.centerBlockOuter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.centerBlockInner {
|
||||
width: 650px;
|
||||
margin: 24px 0 0 0;
|
||||
}
|
||||
|
||||
.logoWrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
width: 270px;
|
||||
height: auto;
|
||||
margin: 0 0 18px 0;
|
||||
}
|
||||
|
||||
.contentBlock {
|
||||
color: var(--text);
|
||||
background-color: #fff;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
padding: 24px;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.contentItem {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.contentItem:not(:last-child) {
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.contentIcon {
|
||||
flex: none;
|
||||
margin: 0 24px 0 0;
|
||||
width: 32px !important;
|
||||
}
|
||||
|
||||
.contentText {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 12px 0;
|
||||
font-weight: 500;
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 4px 0;
|
||||
font-weight: 500;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.contentList {
|
||||
margin: 24px 0 0 0;
|
||||
}
|
||||
|
||||
#scanButton {
|
||||
display: block;
|
||||
background-color: var(--main);
|
||||
color: #fff;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
margin: 24px 0 0 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
#scanButton:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.boxWrap {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.tableWrap {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
tbody:not(:last-child) {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
color: var(--main);
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: top;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 180px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 12px 0 0 0;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.hash {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 12px 0 0 0;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
border: 1px solid var(--grey);
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
margin: 0 6px 0 0;
|
||||
width: 30%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#saveButton {
|
||||
border: 1px solid var(--grey);
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#saveButton:hover {
|
||||
background-color: var(--border);
|
||||
}
|
||||
14
frontend/src/styles/index.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
11438
frontend/yarn.lock
Normal file
50
package.json
|
|
@ -1,50 +0,0 @@
|
|||
{
|
||||
"name": "vytal",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "webpack",
|
||||
"start": "webpack --watch"
|
||||
},
|
||||
"keywords": [],
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.3",
|
||||
"@babel/preset-env": "^7.12.1",
|
||||
"@babel/preset-react": "^7.12.1",
|
||||
"@hot-loader/react-dom": "^17.0.0-rc.2",
|
||||
"@types/chrome": "0.0.143",
|
||||
"@types/react": "^16.9.53",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@typescript-eslint/eslint-plugin": "^4.26.0",
|
||||
"@typescript-eslint/parser": "^4.26.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"copy-webpack-plugin": "^6.2.1",
|
||||
"css-loader": "^5.0.0",
|
||||
"eslint": "^7.27.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"file-loader": "^6.1.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"ts-loader": "^8.0.5",
|
||||
"typescript": "^4.0.3",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.1.3",
|
||||
"webpack-cli": "^4.0.0",
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.3",
|
||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||
"bowser": "^2.11.0",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"react-world-flags": "^1.4.0"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 288 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 533 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"name": "Vytal",
|
||||
"description": "An Extension To Show You What Trackers See",
|
||||
"manifest_version": 3,
|
||||
"version": "1.0.0",
|
||||
"permissions": ["storage"],
|
||||
"icons": {
|
||||
"16": "icon_16.png",
|
||||
"32": "icon_32.png",
|
||||
"48": "icon_48.png",
|
||||
"128": "icon_128.png"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
"16": "icon_16.png",
|
||||
"32": "icon_32.png",
|
||||
"48": "icon_48.png",
|
||||
"128": "icon_128.png"
|
||||
}
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
.checkBoxWrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0079d3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.optionText {
|
||||
margin: 3px 3px 3px 4px;
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="options.css" />
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="checkBoxWrap">
|
||||
<input type="checkbox" id="sendData" name="sendData"/>
|
||||
<label for="sendData">Do not send anonymous data that improves fingerprint accuracy</label>
|
||||
</div>
|
||||
<div class="optionText">Github: <a target="_blank" href="https://github.com/z0ccc/vytal-extension">https://github.com/z0ccc/vytal-extension</a></div>
|
||||
</body>
|
||||
</body>
|
||||
<script src="options.js"></script>
|
||||
</html>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
chrome.storage.sync.get('sendData', ({ sendData }) => {
|
||||
document.getElementById('sendData').checked = sendData;
|
||||
});
|
||||
|
||||
window.onchange = function change(event) {
|
||||
if (event.target.matches('#sendData')) {
|
||||
chrome.storage.sync.get('sendData', ({ sendData }) => {
|
||||
const value = !sendData;
|
||||
chrome.storage.sync.set({ sendData: value });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="popup"></div>
|
||||
</body>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</html>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import Navbar from './Navbar';
|
||||
import TableBox from './TableBox';
|
||||
import '../styles/App.css';
|
||||
|
||||
const App = () => (
|
||||
<div className="App">
|
||||
<Navbar />
|
||||
<TableBox />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExternalLinkAlt, faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import Logo from '../images/logo.svg';
|
||||
|
||||
const openOptions = () => {
|
||||
chrome.runtime.openOptionsPage();
|
||||
};
|
||||
|
||||
const Navbar = () => (
|
||||
<div className="navbar">
|
||||
<div className="logo">
|
||||
<img src={Logo} alt="Vytal logo" />
|
||||
</div>
|
||||
<div className="menu">
|
||||
<a href="https://vytal.io" target="_blank" rel="noreferrer">
|
||||
<FontAwesomeIcon
|
||||
icon={faExternalLinkAlt}
|
||||
size="lg"
|
||||
className="navIcon"
|
||||
/>
|
||||
</a>
|
||||
<FontAwesomeIcon
|
||||
icon={faCog}
|
||||
size="lg"
|
||||
className="navIcon"
|
||||
onClick={openOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Navbar;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import * as React from 'react';
|
||||
|
||||
const Table = ({ title, data }) => (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{title}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{data.map((item) => (
|
||||
<tbody key={item.title}>
|
||||
<tr>
|
||||
<td>{item.title}:</td>
|
||||
<td>{item.value}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
))}
|
||||
</table>
|
||||
);
|
||||
export default Table;
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import Bowser from 'bowser';
|
||||
import Table from './Table';
|
||||
|
||||
// sorts array into comma separated list
|
||||
const sortArr = (arr) => {
|
||||
const arrLength = arr.length;
|
||||
let list = '';
|
||||
for (let i = 0; i < arrLength; i++) {
|
||||
if (i !== 0) list += ', ';
|
||||
list += arr[i];
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
// sorts plugins object into comma separated list
|
||||
const sortPlugins = (data) => {
|
||||
const { length } = data;
|
||||
|
||||
let list = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i !== 0) list += ', ';
|
||||
list += data[i].name;
|
||||
}
|
||||
return list;
|
||||
};
|
||||
|
||||
const fetchData = (data) => {
|
||||
fetch('https://server.vytal.io/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
};
|
||||
|
||||
const TableBox = () => {
|
||||
const [batLevel, setBatLevel] = useState('');
|
||||
const [batStatus, setBatStatus] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// waits for battery info to resolve and then updates
|
||||
if ('getBattery' in navigator) {
|
||||
navigator.getBattery().then((res) => {
|
||||
setBatLevel(`${Math.round(res.level * 100)}%`);
|
||||
setBatStatus(res.charging ? 'Charging' : 'Not charging');
|
||||
});
|
||||
} else {
|
||||
setBatLevel('N/A');
|
||||
setBatStatus('N/A');
|
||||
}
|
||||
|
||||
// checks if user is okay with sending anonymous data
|
||||
chrome.storage.sync.get('sendData', ({ sendData }) => {
|
||||
if (!sendData) {
|
||||
fetchData(software.concat(hardware));
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const uaResult = Bowser.parse(navigator.userAgent);
|
||||
const date = new Date();
|
||||
const gl = document.createElement('canvas').getContext('webgl');
|
||||
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
|
||||
// Software table items
|
||||
const software = [
|
||||
{
|
||||
key: 'browser',
|
||||
title: 'Browser',
|
||||
value: uaResult.browser.name,
|
||||
},
|
||||
{
|
||||
key: 'browserVersion',
|
||||
title: 'Browser version',
|
||||
value: uaResult.browser.version,
|
||||
},
|
||||
{
|
||||
key: 'browserEngine',
|
||||
title: 'Browser engine',
|
||||
value: uaResult.browser.engine || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'os',
|
||||
title: 'OS',
|
||||
value: `${uaResult.os.name} ${uaResult.os.versionName}`,
|
||||
},
|
||||
{
|
||||
key: 'osVersion',
|
||||
title: 'OS version',
|
||||
value: uaResult.os.version,
|
||||
},
|
||||
{
|
||||
key: 'platform',
|
||||
title: 'Platform',
|
||||
value: navigator.platform,
|
||||
},
|
||||
{
|
||||
key: 'systemType',
|
||||
title: 'System type',
|
||||
value: uaResult.platform.type,
|
||||
},
|
||||
{
|
||||
key: 'userAgent',
|
||||
title: 'User Agent',
|
||||
value: navigator.userAgent || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'preferredLanguage',
|
||||
title: 'Preferred language',
|
||||
value: navigator.language || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'languages',
|
||||
title: 'Languages',
|
||||
value: sortArr(navigator.languages) || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'timezone',
|
||||
title: 'Timezone',
|
||||
value: Intl.DateTimeFormat().resolvedOptions().timeZone || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'timezoneOffset',
|
||||
title: 'Timezone offset',
|
||||
value: date.getTimezoneOffset() || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'cookiesEnabled',
|
||||
title: 'Cookies enabled',
|
||||
value: navigator.cookieEnabled ? 'True' : 'False',
|
||||
},
|
||||
{
|
||||
key: 'javaEnabled',
|
||||
title: 'Java enabled',
|
||||
value: navigator.javaEnabled() ? 'True' : 'False',
|
||||
},
|
||||
{
|
||||
key: 'dntHeader',
|
||||
title: 'DNT header enabled',
|
||||
value: navigator.doNotTrack ? 'True' : 'False',
|
||||
},
|
||||
{
|
||||
key: 'automatedBrowser',
|
||||
title: 'Automated browser',
|
||||
value: navigator.webdriver ? 'True' : 'False',
|
||||
},
|
||||
{
|
||||
key: 'plugins',
|
||||
title: 'Plugins',
|
||||
value: sortPlugins(navigator.plugins) || 'N/A',
|
||||
},
|
||||
];
|
||||
|
||||
// Hardware table items
|
||||
const hardware = [
|
||||
{
|
||||
key: 'screenResolution',
|
||||
title: 'Screen resolution',
|
||||
value: `${window.screen.width}x${window.screen.height}`,
|
||||
},
|
||||
{
|
||||
key: 'colorResolution',
|
||||
title: 'Color Resolution',
|
||||
value: window.screen.colorDepth,
|
||||
},
|
||||
{
|
||||
key: 'batteryLevel',
|
||||
title: 'Battery level',
|
||||
value: batLevel,
|
||||
},
|
||||
{
|
||||
key: 'batteryStatus',
|
||||
title: 'Battery status',
|
||||
value: batStatus,
|
||||
},
|
||||
{
|
||||
key: 'deviceMemory',
|
||||
title: 'Device memory',
|
||||
value: navigator.deviceMemory ? `${navigator.deviceMemory}GB` : 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'cpuCores',
|
||||
title: '# of CPU cores',
|
||||
value: navigator.hardwareConcurrency || 'N/A',
|
||||
},
|
||||
{
|
||||
key: 'maxTouchpoints',
|
||||
title: 'Max touchpoints',
|
||||
value: navigator.maxTouchPoints,
|
||||
},
|
||||
{
|
||||
key: 'webGLVendor',
|
||||
title: 'WebGL vendor',
|
||||
value: gl.getParameter(ext.UNMASKED_VENDOR_WEBGL),
|
||||
},
|
||||
{
|
||||
key: 'webglRenderer',
|
||||
title: 'WebGL renderer',
|
||||
value: gl.getParameter(ext.UNMASKED_RENDERER_WEBGL),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="tableBox">
|
||||
<Table title="Software" data={software} />
|
||||
<Table title="Hardware" data={hardware} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableBox;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
|
||||
import App from './App';
|
||||
|
||||
const mountNode = document.getElementById('popup');
|
||||
ReactDOM.render(<App />, mountNode);
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
:root {
|
||||
--main: #943EC5;
|
||||
--text: #212121;
|
||||
--background: #fff;
|
||||
--scrollbar: #ccc;
|
||||
--navbar: #FBFCFC;
|
||||
--icon: #AAB7B8;
|
||||
--border: #F0F3F4;
|
||||
scrollbar-color: var(--scrollbar) !important;
|
||||
scrollbar-width: thin !important;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--text);
|
||||
background-color: var(--background);
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
width: 400px;
|
||||
margin: 0;
|
||||
overflow: overlay;
|
||||
overflow-x: hidden;
|
||||
font-family: "Segoe UI", Tahoma, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
.navIcon {
|
||||
color: var(--icon);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navIcon:hover {
|
||||
color: var( --main);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
background-color: var(--navbar);
|
||||
border-bottom: var(--border) solid 1px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 52px;
|
||||
margin: 0 8px 0 0;
|
||||
}
|
||||
|
||||
.tableBox {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-spacing: 0 6px;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
color: var(--main);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
color: var(--main);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
const webpack = require("webpack");
|
||||
const path = require("path");
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
|
||||
const config = {
|
||||
entry: {
|
||||
popup: path.join(__dirname, 'src/components/popup.js'),
|
||||
},
|
||||
output: { path: path.join(__dirname, 'dist'), filename: '[name].js' },
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
use: 'babel-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
exclude: /\.module\.css$/,
|
||||
},
|
||||
{
|
||||
test: /\.ts(x)?$/,
|
||||
loader: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
include: /\.module\.css$/,
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: 'file-loader',
|
||||
},
|
||||
{
|
||||
test: /\.png$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
mimetype: 'image/png',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx', '.tsx', '.ts'],
|
||||
alias: {
|
||||
'react-dom': '@hot-loader/react-dom',
|
||||
},
|
||||
},
|
||||
devServer: {
|
||||
contentBase: './dist',
|
||||
},
|
||||
plugins: [
|
||||
new CopyPlugin({
|
||||
patterns: [{ from: 'public', to: '.' }],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||