This commit is contained in:
z0ccc 2021-07-20 21:09:25 -04:00
parent 9e8146bf1c
commit 099654d2ee
77 changed files with 13132 additions and 6777 deletions

View file

@ -1,14 +0,0 @@
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
],
"@babel/preset-react"
],
"plugins": [
"react-hot-loader/babel"
]
}

71
.gitignore vendored
View file

@ -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*

View file

16
backend/backend/asgi.py Normal file
View 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
View 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
View 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
View 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
View 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()

View file

11
backend/vytal/admin.py Normal file
View 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
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class VytalConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'vytal'

View 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)),
],
),
]

View file

11
backend/vytal/models.py Normal file
View 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

View 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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

14
backend/vytal/views.py Normal file
View 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']

View file

@ -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"
}
}
}

View file

@ -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'
},
};

View file

@ -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
![Screen Shot](https://raw.githubusercontent.com/z0ccc/Vytal-extension/master/promo_images/screenshot-1.png)
Github for Extension:
https://github.com/z0ccc/Vytal-extension

51
frontend/package.json Normal file
View 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"
}
}

View file

Before

Width:  |  Height:  |  Size: 786 B

After

Width:  |  Height:  |  Size: 786 B

View 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>

View 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"
}

View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

9
frontend/src/App.test.js Normal file
View 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();
});

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View file

@ -0,0 +1,9 @@
const ContentBlock = ({ children }) => (
<div className="contentBlock">
<div className="contentItem">
<div className="contentText">{children}</div>
</div>
</div>
);
export default ContentBlock;

View 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;

View 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;

View 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;

View 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;

File diff suppressed because one or more lines are too long

View 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

View 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

View file

@ -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

View 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
View 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();

View 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
}

View 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);
});
}
}

View 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
View 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);
}

View 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

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -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
}
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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 });
});
}
};

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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;
}

View file

@ -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;

6120
yarn.lock

File diff suppressed because it is too large Load diff