Compare commits

...
Sign in to create a new pull request.

45 commits

Author SHA1 Message Date
z0ccc
68e8297aa2 read me update 2022-06-02 20:13:47 -04:00
z0ccc
24644bef87 Read me update 2022-06-02 20:06:35 -04:00
z0ccc
302e66552d Read me update 2022-06-02 19:59:38 -04:00
z0ccc
87c7f91fd0 Read me update 2022-06-02 19:42:33 -04:00
z0ccc
68352f6776 Added screen shots for webstore 2022-05-29 22:29:45 -04:00
z0ccc
c39e55f55b Updated manifest version and description 2022-05-29 22:29:31 -04:00
z0ccc
6defd8e801 Small ui tweaks 2022-05-29 22:13:19 -04:00
z0ccc
39233de2d8 userAgent not getting cleared 2022-05-25 22:15:01 -04:00
z0ccc
2db9eabf78 Added more useragents 2022-05-13 01:26:16 -04:00
z0ccc
b9b8523eb0 Add key to profiles map 2022-05-13 01:01:26 -04:00
z0ccc
10cd4dedce Style text inputs 2022-05-12 22:39:37 -04:00
z0ccc
247e4edfe7 Fixed 'none' profile 2022-05-12 20:14:41 -04:00
z0ccc
7ba33fb6c6 Removed unnecessary files from boiler plate 2022-05-12 19:43:52 -04:00
z0ccc
14774f2648 Switched profile list for loop 2022-05-12 18:32:02 -04:00
z0ccc
73ea2ede74 Adding city profiles 2022-05-12 00:31:37 -04:00
z0ccc
b25d62dd61 Added component for profile select 2022-05-10 18:59:57 -04:00
z0ccc
c321921f65 No alarm when interval is 0 of empty 2022-05-10 16:41:53 -04:00
z0ccc
7dc60c2157 Added timer feature to user agent option 2022-05-09 23:56:42 -04:00
z0ccc
431d51fd0c Formatted spacing of popup ui 2022-05-08 22:56:03 -04:00
z0ccc
506fad5c41 Added locale to new profile selection for match ip 2022-05-08 20:42:38 -04:00
z0ccc
89f1faffa0 Added more profiles and changed bottom text 2022-05-07 16:15:57 -04:00
z0ccc
62bac340de Save and load custom values 2022-05-06 13:28:24 -04:00
z0ccc
c350da3cc3 Added profile options 2022-05-06 13:01:20 -04:00
z0ccc
b264e5f1bb Fixed overriding timezone bug 2022-05-05 14:30:45 -04:00
z0ccc
27e0e414f9 Only attach if options enabled 2022-05-02 17:31:39 -04:00
z0ccc
ebf2e47b9c Fixed controlled component error for user agent settings 2022-05-02 01:02:32 -04:00
z0ccc
867b9ef33d Fixed bugs with user agent 2022-05-01 20:52:33 -04:00
z0ccc
6f202aeae5 Correctly saving user agent options in storage 2022-04-28 18:58:53 -04:00
z0ccc
83af9c1c7c Added functionality to randomize options 2022-04-28 13:40:32 -04:00
z0ccc
b17865c13f user agent randomization 2022-04-27 00:18:47 -04:00
z0ccc
bec416ae74 Added useragent masking 2022-04-20 21:44:42 -04:00
z0ccc
2117156138 Fixed controlled component issues 2022-04-16 20:22:09 -04:00
z0ccc
8fc026b348 Added custom locales to debugger 2022-04-16 13:39:09 -04:00
z0ccc
3a76962921 Added locale settings for popup 2022-04-16 13:10:53 -04:00
z0ccc
1c4ee91a1c use custom data for debugger 2022-04-15 23:53:21 -04:00
z0ccc
9f7754641e Fixed issues with reloading match ip's 2022-04-15 17:50:01 -04:00
z0ccc
6e2372b28d Synced up reload button with match ip and geo data 2022-04-15 16:08:37 -04:00
z0ccc
8653004136 Added match ip checkboxes 2022-04-15 14:39:42 -04:00
z0ccc
d2860e202c Adding popup code to reload and display ip 2022-04-15 01:27:03 -04:00
z0ccc
f635a63a1e Added new tab page 2022-04-14 01:42:38 -04:00
z0ccc
5fe9b413e3 Added popup button to refresh ip 2022-04-14 00:50:04 -04:00
z0ccc
62a1fc10d2 Added locale to match ip 2022-04-13 20:57:12 -04:00
z0ccc
1da219b110 Better attachemnt method and using ip data 2022-04-13 19:09:56 -04:00
z0ccc
61d52ca16d Initial setup 2022-04-10 14:17:38 -04:00
z0ccc
ad3a42d040 Extension boiler plant 2022-04-10 13:27:05 -04:00
109 changed files with 27475 additions and 13965 deletions

11
.babelrc Normal file
View file

@ -0,0 +1,11 @@
{
"presets": [
// "@babel/preset-env"
"@babel/preset-react"
// "react-app"
],
"plugins": [
// "@babel/plugin-proposal-class-properties",
"react-hot-loader/babel"
]
}

6
.eslintrc Normal file
View file

@ -0,0 +1,6 @@
{
"extends": "react-app",
"globals": {
"chrome": "readonly"
}
}

70
.gitignore vendored
View file

@ -1,67 +1,21 @@
env/
.vscode/
./frontend/App.test.js
# 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
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/frontend/node_modules
/frontend/.pnp
/frontend/.pnp.js
/node_modules
# testing
/frontend/coverage
/coverage
# production
/frontend/build
/build
# misc
/frontend/.DS_Store
/frontend/.env.local
/frontend/.env.development.local
/frontend/.env.test.local
/frontend/.env.production.local
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.history
/frontend/npm-debug.log*
/frontend/yarn-debug.log*
/frontend/yarn-error.log*
# secrets
secrets.*.js

8
.prettierrc Normal file
View file

@ -0,0 +1,8 @@
{
"singleQuote": true,
"trailingComma": "es5",
"requirePragma": false,
"arrowParens": "always",
"semi": false,
"editor.formatOnSave": true
}

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

View file

@ -1,6 +1,6 @@
MIT License
The MIT License (MIT)
Copyright (c) 2022 z0ccc
Copyright (c) 2019 Michael Xieyang Liu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

62
README.md Normal file → Executable file
View file

@ -1,20 +1,32 @@
# Vytal
Check it out here: https://vytal.io.
<a href="https://chrome.google.com/webstore/detail/vytal/ncbknoohfjmcfneopnfkapmkblaenokb"><img src="https://raw.githubusercontent.com/z0ccc/Upvote-Anywhere/master/promo/chrome.png" alt="Get Vytal for Chromium"></a>
Protect your privacy by mocking your personal data.
## About
Vytal shows you what traces your browser leaves behind while surfing the web. This scan allows you to understand how easy it is to identify and track your browser even while using a VPN or private mode.
Vytal can mock your timezone, locale, geolocation and user agent. This data can be used to track you online or reveal your location.
A device fingerprint will be generated from your data in the form of a hash. You can sign your hash by entering and saving a signature. You can clear cookies, change your IP or use private mode and reload the page to see if your signature remains the same.
Vytal is utilizes the chrome.debugger API to mock this data. This allows the data to be changed in frames, web workers and during the initial loading of a website. It also makes the spoofing completely undetectable.
You can test and compare Vytal and other extensions on https://vytal.io
Vytal contains no ads and signup is not required.
## Limitations
### Tab initialization
There is a slight delay between when a new tab is opened and the debugger starts mocking the data. This allows for websites to get the original value of the data before it is changed. After the initial loading of a tab, this will no longer be an issue.
### Locale override does not mock language data
Unlike the Chrome devtools location sensor, overriding the locale does not change language data (such as navigator.language or navigator.languages). There is an open ticket about this here: https://bugs.chromium.org/p/chromium/issues/detail?id=1320419
## Data Tampering
The data used to create device fingerprints can be spoofed or tampered with to prevent tracking. There are a variety of methods used to do this including VPNs, browser extensions and built in browser options. Some methods of data tampering can be detected.
If data tampering is detected then a red circle with an x will be displayed next to the data value. If the legitimate value cannot be identified then the data will be discarded and will not be used to generate a fingerprint. Clicking on the table row of the tampered value will bring up a dialog box showing the types of tampering.
Data spoofed with Vytal can not be detected. Although other extensions which spoof's data can be detected. have If data tampering is detected then a red circle with an x will be displayed next to the data value. If the legitimate value cannot be identified then the data will be discarded and will not be used to generate a fingerprint. Clicking on the table row of the tampered value will bring up a dialog box showing the types of tampering.
## Types of Tampering
@ -30,18 +42,6 @@ If data tampering is detected then a red circle with an x will be displaye
`Object.getOwnPropertyDescriptor(Navigator.prototype, [DataType]).value` returns an error if the data object was tampered with. Otherwise returns undefined.
### Did not match web worker (\_\_\_)
Value does not match the value found in a [web worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers). The web worker value is in the brackets.
### Location data doesn't match system data
Occurs when the location data from ip address does not match your location data from your system (such as your timezone).
### VPN/proxy has been detected
Your ip address is known to be used by proxies or VPNs.
### Failed Date.prototype.setDate.toString()
`Failed Date.prototype.setDate.toString()` returns 'function setDate() { [native code] }' if the data object was NOT tampered with.
@ -50,28 +50,26 @@ Your ip address is known to be used by proxies or VPNs.
`Screen.prototype[DataType]` returns a value if the data object was tampered with. Otherwise returns an error.
### Avail width is greater than width
## Screenshots
Happens when the avail width is greater than the normal width (which is impossible).
![Screenshot of extension popup](https://raw.githubusercontent.com/z0ccc/Vytal/extension/promo/screenshot-1.png)
### Avail height is greater than height
Happens when the height width is greater than the normal height (which is impossible).
![Screenshot of extension popup and vytal.io](https://raw.githubusercontent.com/z0ccc/Vytal/extension/promo/screenshot-2.png)
## Dev
This application uses a React frontend and a Django backend that communicates using the Django REST framework.
This application is built with Javascript and React.
Backend Django setup:
Clone this repo and run these commands to start the development server.
```
cd backend
python manage.py runserver
yarn
yarn start
```
Frontend React setup:
Load your extension on Chrome following:
```
cd frontend
yarn run start
```
- Access chrome://extensions/
- Check Developer mode
- Click on Load unpacked extension
- Select the build folder.

View file

@ -1,16 +0,0 @@
"""
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()

View file

@ -1,146 +0,0 @@
"""
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',
},
]
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle'
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day'
}
}
# 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 = [
'https://vytal.io',
'https://z0ccc.github.io',
'http://localhost:3000'
]

View file

@ -1,14 +0,0 @@
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from vytal import views
from secret_settings import *
router = routers.DefaultRouter()
router.register(r'fingerprint', views.FingerprintView, 'fingerprint')
urlpatterns = [
path(ADMIN_URL + '/', admin.site.urls),
path('', include(router.urls)),
path('', include('vytal.urls')),
]

View file

@ -1,16 +0,0 @@
"""
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()

View file

@ -1,22 +0,0 @@
#!/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()

Binary file not shown.

View file

@ -1,11 +0,0 @@
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)

View file

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

View file

@ -1,22 +0,0 @@
# 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

@ -1,11 +0,0 @@
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

@ -1,8 +0,0 @@
from rest_framework import serializers
from .models import Fingerprint
class FingerprintSerializer(serializers.ModelSerializer):
class Meta:
model = Fingerprint
fields = ('id', 'name', 'hash')

View file

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

View file

@ -1,6 +0,0 @@
from django.urls import path
from . import views
urlpatterns = [
path('ip/', views.IPView, name='ip'),
]

View file

@ -1,23 +0,0 @@
from rest_framework import viewsets
from .serializers import FingerprintSerializer
from .models import Fingerprint
from django_filters.rest_framework import DjangoFilterBackend
from django.http import JsonResponse
from ipware import get_client_ip
import urllib.request
import json
from secret_settings import *
class FingerprintView(viewsets.ModelViewSet):
serializer_class = FingerprintSerializer
queryset = Fingerprint.objects.all()
filter_backends = [DjangoFilterBackend]
filterset_fields = ['hash']
def IPView(request):
ip = get_client_ip(request)
with urllib.request.urlopen("https://pro.ip-api.com/json/" + ip[0] + "?key=" + API_KEY) as url:
data = json.loads(url.read().decode())
return JsonResponse(data)

View file

@ -1,44 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: true,
webextensions: true,
},
extends: ['plugin:react/recommended', 'airbnb'],
plugins: ['react'],
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx'],
moduleDirectory: ['node_modules', 'src/'],
},
},
},
rules: {
'no-plusplus': 'off',
'comma-dangle': 'off',
'operator-linebreak': 'off',
'no-use-before-define': 'off',
'linebreak-style': 'off',
'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.js'] }],
'jsx-a11y/label-has-associated-control': 'off',
'one-var': 'off',
'one-var-declaration-per-line': 'off',
'object-curly-newline': 'off',
'implicit-arrow-linebreak': 'off',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
},
],
'react/jsx-one-expression-per-line': 'off',
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'no-bitwise': 'off',
'react/no-array-index-key': 'off',
'dot-notation': 'off',
},
};

View file

@ -1,57 +0,0 @@
{
"homepage": "https://z0ccc.github.io",
"name": "vytal",
"version": "0.1.0",
"private": true,
"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",
"gh-pages": "^3.2.3",
"html-react-parser": "^1.2.8",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-github-btn": "^1.2.1",
"react-modal": "^3.14.3",
"react-scripts": "4.0.3",
"react-webworker": "^2.1.0",
"tslib": "^2.2.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"predeploy": "yarn run build",
"deploy": "echo vytal.io > ./build/CNAME && gh-pages -d build"
},
"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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 786 B

View file

@ -1,34 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Vytal shows you what traces your browser leaves behind while surfing the web."
/>
<meta name="author" content="z0ccc" />
<meta property="og:title" content="Vytal" />
<meta property="og:url" content="https://vytal.io" />
<meta property="og:img" content="https://vytal.io/vytal.png" />
<meta
property="og:description"
content="Vytal shows you what traces your browser leaves behind while surfing the web."
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Vytal" />
<meta
name="twitter:description"
content="Vytal shows you what traces your browser leaves behind while surfing the web."
/>
<meta name="twitter:image" content="https://vytal.io/vytal.png" />
<link rel="manifest" href="/manifest.json" />
<title>Vytal Privacy Scan</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View file

@ -1,15 +0,0 @@
{
"short_name": "Vytal",
"name": "Vytal Privacy Scan",
"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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

View file

@ -1,13 +0,0 @@
const data = {
locale: Intl.DateTimeFormat().resolvedOptions().locale,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timezoneOffset: new Date().getTimezoneOffset(),
deviceMemory: navigator.deviceMemory,
hardwareConcurrency: navigator.hardwareConcurrency,
platform: navigator.platform,
userAgent: navigator.userAgent,
appVersion: navigator.appVersion,
language: navigator.language,
};
postMessage(data);

View file

@ -1,9 +0,0 @@
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

@ -1,92 +0,0 @@
:root {
--main: #943ec5;
--grey: #9fa6b2;
--text: #4b5563;
--border: #ddd;
--issueBackground: #f8d7da;
--issueText: #721c24;
--link: #943ec5;
}
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;
}
.App {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.background {
position: fixed;
width: 100%;
height: 100%;
background: var(--grey);
background: linear-gradient(
165deg,
rgba(87, 35, 117, 1) 0%,
rgba(148, 62, 197, 1) 55%,
rgba(211, 176, 231, 1) 100%
);
z-index: -1;
}
h1 {
margin: 0 0 12px 0;
font-weight: 600;
font-size: 19px;
}
h2 {
margin: 0 0 4px 0;
font-weight: 600;
font-size: 17px;
}
p {
margin: 12px 0 0 0;
}
b {
font-weight: 600;
}
img {
width: 100%;
border-radius: 6px;
border: 1px solid var(--border);
box-sizing: border-box;
display: block;
margin: 0 0 12px 0;
}
.link {
color: var(--link);
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
@media screen and (max-width: 500px) {
body {
font-size: 14px;
}
h1 {
font-size: 16px;
}
h2 {
margin: 0 0 3px 0;
font-size: 15px;
}
}

View file

@ -1,13 +0,0 @@
import Github from './Github';
import MainColumn from './MainColumn';
import './App.css';
const App = () => (
<div className="App">
<Github />
<div className="background" />
<MainColumn />
</div>
);
export default App;

View file

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

View file

@ -1,47 +0,0 @@
.centerBlockInner {
width: 500px;
}
.centerBlockMobile {
display: none;
}
.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;
min-width: 500px;
}
@media screen and (max-width: 1200px) {
.centerBlockInner {
display: none;
}
.centerBlockMobile {
display: block;
max-width: 650px;
padding: 0 12px;
}
.contentBlock {
padding: 18px;
margin: 0 0 12px 0;
min-width: 400px;
}
.loadBlock {
margin: 0 12px;
}
}
@media screen and (max-width: 500px) {
.contentBlock {
width: 100%;
min-width: 0;
}
}

View file

@ -1,43 +0,0 @@
import { useState, useEffect } from 'react';
import DataContext from './Context';
import BlocksOne from './BlocksOne';
import BlocksTwo from './BlocksTwo';
// import FontsBlock from './FontsBlock';
import { fetchAPI, getWebWorker } from '../utils/common';
import './Blocks.css';
const Blocks = () => {
const [workerData, setWorkerData] = useState();
const [connectionData, setConnectionData] = useState('');
useEffect(() => {
getWebWorker().onmessage = (event) => {
setWorkerData(event.data);
fetchAPI(setConnectionData);
};
}, []);
return (
<>
{connectionData ? (
<DataContext.Provider value={{ workerData, connectionData }}>
<div className="centerBlockInner">
<BlocksOne />
</div>
<div className="centerBlockInner">
<BlocksTwo />
</div>
<div className="centerBlockMobile">
<BlocksOne />
<BlocksTwo />
</div>
</DataContext.Provider>
) : (
<div className="contentBlock loadBlock">
<center>Loading...</center>
</div>
)}
</>
);
};
export default Blocks;

View file

@ -1,15 +0,0 @@
import UserAgentBlock from './UserAgentBlock';
import IntlBlock from './IntlBlock';
import NavigatorBlock from './NavigatorBlock';
import FingerprintBlock from './FingerprintBlock';
const BlocksOne = () => (
<>
<FingerprintBlock />
<NavigatorBlock />
<UserAgentBlock />
<IntlBlock />
</>
);
export default BlocksOne;

View file

@ -1,15 +0,0 @@
import OtherBlock from './OtherBlock';
import ScreenBlock from './ScreenBlock';
import LocationBlock from './LocationBlock';
import ConnectionBlock from './ConnectionBlock';
const BlocksTwo = () => (
<>
<LocationBlock />
<ConnectionBlock />
<ScreenBlock />
<OtherBlock />
</>
);
export default BlocksTwo;

View file

@ -1,30 +0,0 @@
import { useContext } from 'react';
import DataContext from './Context';
import Block from './Block';
import Table from './Table';
import { getConnection } from '../utils/connection';
const LocationBlock = () => {
const { connectionData } = useContext(DataContext);
return (
<Block>
<h1>Connection</h1>
<Table data={getConnection(connectionData)} />
<p>
<b>Explanation:</b> Your IP address reveals information about your
connection.{' '}
<a
className="link"
target="_blank"
rel="noreferrer"
alt="Read more about ip connection"
href="https://en.wikipedia.org/wiki/IP_address"
>
Read more
</a>
</p>
</Block>
);
};
export default LocationBlock;

View file

@ -1,5 +0,0 @@
import { createContext } from 'react';
const DataContext = createContext();
export default DataContext;

View file

@ -1,53 +0,0 @@
.fingerprintTable td:first-child {
width: 80px;
font-weight: 600;
}
.boxWrap {
border: 1px solid var(--border);
border-radius: 6px;
padding: 12px;
}
.hash {
text-align: center;
font-weight: 600;
}
form {
margin: 12px 0 0 0;
}
.saveButton {
border: 1px solid var(--grey);
border-radius: 6px;
padding: 6px;
background-color: transparent;
cursor: pointer;
color: var(--text);
margin: 0 0 0 6px;
-webkit-appearance: none;
}
.saveButton:hover {
background-color: var(--border);
}
input[type='text'] {
border: 1px solid var(--grey);
border-radius: 6px;
padding: 6px;
width: 200px;
outline: none;
-webkit-appearance: none;
}
@media screen and (max-width: 500px) {
.boxWrap {
padding: 8px;
}
input[type='text'] {
width: calc(100% - 70px);
}
}

View file

@ -1,62 +0,0 @@
import './FingerprintBlock.css';
import { useState, useEffect, useContext } from 'react';
import DataContext from './Context';
import Block from './Block';
import Table from './Table';
import {
getSignature,
postSignature,
getHash,
getFingerprint,
} from '../utils/fingerprint';
const FingerprintBlock = () => {
const [signature, setSignature] = useState();
const [load, setload] = useState(false);
const { workerData } = useContext(DataContext);
const hash = getHash(workerData);
useEffect(() => {
getSignature(hash, setSignature, setload);
}, []);
return (
<Block>
<h1>Fingerprint</h1>
{load && (
<>
{signature !== undefined ? (
<div className="fingerprintTable">
<Table data={getFingerprint(signature, hash)} />
</div>
) : (
<div className="boxWrap">
<div className="hash">{hash}</div>
</div>
)}
</>
)}
<p>
<b>Explanation:</b> A device fingerprint will be generated from your
data in the form of a hash. Sign your hash, change your IP or use
private mode and reload to see if your signature remains the same.
</p>
<form onSubmit={(e) => postSignature(hash, e, setSignature)}>
<input
type="text"
id="signature"
name="signature"
placeholder="Enter signature"
/>
<input
type="submit"
className="saveButton"
value="Save"
maxLength="100"
/>
</form>
</Block>
);
};
export default FingerprintBlock;

View file

@ -1,61 +0,0 @@
import { useEffect } from 'react';
import Block from './Block';
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 (
<Block>
<h1>System Fonts</h1>
<div className="fonts boxWrap" />
<p>
<b>Explanation:</b> The fonts you have installed on your computer are
generally linked to operating systems, language and system age. Any
fonts that you have installed that aren&apos;t common for your system
can make you easily identifiable.
</p>
</Block>
);
};
export default FontsBlock;

View file

@ -1,18 +0,0 @@
.gitHubButton {
position: fixed;
top: 12px;
right: 12px;
}
@media screen and (max-width: 900px) {
.gitHubButton {
position: absolute;
}
}
@media screen and (max-width: 500px) {
.gitHubButton {
top: 8px;
right: 8px;
}
}

View file

@ -1,16 +0,0 @@
import './GitHub.css';
import GitHubButton from 'react-github-btn';
const Github = () => (
<div className="gitHubButton">
<GitHubButton
href="https://github.com/z0ccc/Vytal"
data-color-scheme="no-preference: light; light: light; dark: light;"
aria-label="Star z0ccc/Vytal on GitHub"
>
Star
</GitHubButton>
</div>
);
export default Github;

View file

@ -1,29 +0,0 @@
import { useContext } from 'react';
import DataContext from './Context';
import Block from './Block';
import Table from './Table';
import getIntl from '../utils/intl';
const IntlBlock = () => {
const { workerData } = useContext(DataContext);
return (
<Block>
<h1>Intl</h1>
<Table data={getIntl(workerData)} />
<p>
<b>Explanation:</b> The Intl object exposes info about your computer.{' '}
<a
className="link"
target="_blank"
rel="noreferrer"
alt="Read more about intl"
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl"
>
Read more
</a>
</p>
</Block>
);
};
export default IntlBlock;

View file

@ -1,31 +0,0 @@
import { useContext } from 'react';
import DataContext from './Context';
import Block from './Block';
import Table from './Table';
import { getMap, getLocation } from '../utils/connection';
const LocationBlock = () => {
const { workerData, connectionData } = useContext(DataContext);
return (
<Block>
<h1>Location</h1>
<img src={getMap(connectionData)} alt="Map of current location" />
<Table data={getLocation(connectionData, workerData)} />
<p>
<b>Explanation:</b> Your IP address can be used to determine your
location.{' '}
<a
className="link"
target="_blank"
rel="noreferrer"
alt="Read more about ip location"
href="https://en.wikipedia.org/wiki/IP_address"
>
Read more
</a>
</p>
</Block>
);
};
export default LocationBlock;

View file

@ -1,17 +0,0 @@
.logoWrap {
display: flex;
align-items: center;
justify-content: center;
}
.logo {
margin: 20px 0 16px 0;
width: 250px;
}
@media screen and (max-width: 500px) {
.logo {
width: 160px;
margin: 12px 0 8px 0;
}
}

View file

@ -1,12 +0,0 @@
import './Logo.css';
import { ReactComponent as LogoImg } from '../images/logo.svg';
const Logo = () => (
<div className="logoWrap">
<a href="/" className="logo" alt="Vytal logo">
<LogoImg />
</a>
</div>
);
export default Logo;

View file

@ -1,5 +0,0 @@
.centerBlockOuter {
display: flex;
justify-content: center;
gap: 24px;
}

View file

@ -1,13 +0,0 @@
import './MainColumn.css';
import Logo from './Logo';
import Blocks from './Blocks';
const MainColumn = () => (
<>
<Logo />
<div className="centerBlockOuter">
<Blocks />
</div>
</>
);
export default MainColumn;

View file

@ -1,30 +0,0 @@
import { useContext } from 'react';
import DataContext from './Context';
import Block from './Block';
import Table from './Table';
import getNavigator from '../utils/navigator';
const NavigatorBlock = () => {
const { workerData } = useContext(DataContext);
return (
<Block>
<h1>Navigator</h1>
<Table data={getNavigator(workerData)} />
<p>
<b>Explanation:</b> The Navigator interface exposes info about your
computer.{' '}
<a
className="link"
target="_blank"
rel="noreferrer"
alt="Read more about navigator"
href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator"
>
Read more
</a>
</p>
</Block>
);
};
export default NavigatorBlock;

View file

@ -1,43 +0,0 @@
import { useState, useEffect, useContext } from 'react';
import DataContext from './Context';
import Block from './Block';
import Table from './Table';
import getOther from '../utils/other';
const OtherBlock = () => {
const [adBlock, setAdBlock] = useState();
const [battery, setBattery] = useState();
const { workerData } = useContext(DataContext);
useEffect(() => {
fetch('https://www3.doubleclick.net', {
method: 'HEAD',
mode: 'no-cors',
cache: 'no-store',
})
.then(() => {
setAdBlock(false);
})
.catch(() => {
setAdBlock(true);
});
if ('getBattery' in navigator) {
navigator.getBattery().then((res) => {
setBattery(res);
});
} else {
setBattery('N/A');
}
}, []);
return (
<Block>
<h1>Other</h1>
{battery && adBlock !== undefined && (
<Table data={getOther(battery, adBlock, workerData)} />
)}
</Block>
);
};
export default OtherBlock;

View file

@ -1,24 +0,0 @@
import Block from './Block';
import Table from './Table';
import getScreen from '../utils/screen';
const ScreenBlock = () => (
<Block>
<h1>Screen</h1>
<Table data={getScreen()} />
<p>
<b>Explanation:</b> The Screen interface exposes info about your computer.{' '}
<a
className="link"
target="_blank"
rel="noreferrer"
alt="Read more about screen"
href="https://developer.mozilla.org/en-US/docs/Web/API/Screen"
>
Read more
</a>
</p>
</Block>
);
export default ScreenBlock;

View file

@ -1,14 +0,0 @@
table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.tableWrap {
border: 1px solid var(--border);
border-radius: 6px;
}
tbody:not(:last-child) {
border-bottom: 1px solid var(--border);
}

View file

@ -1,16 +0,0 @@
import './Table.css';
import TableRow from './TableRow';
const Table = ({ data }) => (
<div className="tableWrap">
<table>
{data.map((item) => (
<tbody key={item.key} title={item.code}>
<TableRow item={item} />
</tbody>
))}
</table>
</div>
);
export default Table;

View file

@ -1,73 +0,0 @@
td {
padding: 12px;
word-break: break-all;
}
td:first-child {
width: 150px;
font-weight: 600;
word-break: normal;
}
td:nth-child(3) {
width: 40px;
font-weight: 600;
}
.circleButton {
display: flex;
width: 20px;
}
.issue {
cursor: pointer;
}
.issue:hover {
background-color: var(--issueBackground);
color: var(--issueText);
}
.modalHeader {
display: flex;
justify-content: space-between;
margin: 0 0 6px 0;
height: 20px;
}
.modalTitle {
font-weight: 600;
}
.closeButton {
fill: var(--border);
display: flex;
width: 12px;
cursor: pointer;
margin: 0 0 0 12px;
}
.closeButton:hover {
fill: var(--grey);
}
ul {
padding-left: 20px;
margin: 0px;
}
@media screen and (max-width: 500px) {
td {
padding: 8px;
}
td:first-child {
width: 75px;
}
td:nth-child(3) {
width: 20px;
font-weight: 600;
word-break: normal;
}
}

View file

@ -1,68 +0,0 @@
import './TableRow.css';
import Modal from 'react-modal';
import { useState } from 'react';
import { ReactComponent as XCircle } from '../images/xCircle.svg';
import { ReactComponent as CheckCircle } from '../images/checkCircle.svg';
import { ReactComponent as X } from '../images/x.svg';
const modalStyles = {
content: {
top: '50%',
left: '50%',
right: 'auto',
bottom: 'auto',
marginRight: '-50%',
transform: 'translate(-50%, -50%)',
padding: '18px',
border: '1px solid var(--border)',
borderRadius: '6px',
},
};
Modal.setAppElement('#root');
const TableRow = ({ item }) => {
const issues = item.issues.filter(Boolean).length !== 0;
const [modalIsOpen, setIsOpen] = useState(false);
const openModal = () => {
if (issues) setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
return (
<>
<tr className={issues ? 'issue' : ''} onClick={openModal}>
<td>{item.key}</td>
<td>{item.value || 'N/A'}</td>
<td>
{issues ? (
<XCircle className="circleButton" />
) : (
<CheckCircle className="circleButton" />
)}
</td>
</tr>
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
style={modalStyles}
contentLabel="Issues Modal"
>
<div className="modalHeader">
<div className="modalTitle">{item.key} issues</div>
<X className="closeButton" onClick={closeModal} />
</div>
<ul>
{item.issues.filter(Boolean).map((ele, index) => (
<li key={index}>{ele}</li>
))}
</ul>
</Modal>
</>
);
};
export default TableRow;

View file

@ -1,30 +0,0 @@
import { useContext } from 'react';
import DataContext from './Context';
import Block from './Block';
import Table from './Table';
import getUserAgent from '../utils/userAgent';
const UserAgentBlock = () => {
const { workerData } = useContext(DataContext);
return (
<Block>
<h1>User Agent</h1>
<Table data={getUserAgent(workerData.userAgent)} />
<p>
<b>Explanation:</b> Your user agent can be parsed to determine
information about your browser or operating system.{' '}
<a
className="link"
target="_blank"
rel="noreferrer"
alt="Read more about user agent"
href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent"
>
Read more
</a>
</p>
</Block>
);
};
export default UserAgentBlock;

File diff suppressed because one or more lines are too long

View file

@ -1,5 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 360 B

View file

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<g fill="#c3e6cb">
<path d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 470 B

View file

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<g fill="#9fa6b2">
<path d="M256 169.92c-28.28.41-52.84 9.62-71.37 28.17-18 18-27.69 41.94-27.28 67.44.78 50.23-3.91 100.5-14 149.39-1.81 8.66 3.78 17.12 12.41 18.91 8.5 1.66 17.09-3.77 18.91-12.44 10.56-51.17 15.5-103.78 14.69-156.35-.25-16.77 6.09-32.5 17.91-44.31 18.66-18.66 42.27-18.8 48.75-18.8 37.12.55 66.41 30.19 66.97 66.06.78 50.37-2.97 100.86-11.12 150.06-1.44 8.72 4.44 16.95 13.16 18.39.91.16 1.78.22 2.66.22 7.69 0 14.47-5.55 15.75-13.39 8.47-51.08 12.34-103.48 11.56-155.79-.87-53-44.81-96.76-99-97.56zm-.09 86.09c-8.84.14-15.87 7.41-15.75 16.25 1.12 73.39-8.22 144.99-27.78 215.07l-1.22 4.33c-1.85 6.63 1.74 20.34 15.41 20.34 7 0 13.41-4.61 15.41-11.66l1.22-4.37c20.41-73.08 30.16-147.74 28.97-224.21-.14-8.84-8.23-15.9-16.26-15.75zM112.66 149.79c-25.19 30.98-38.72 70.11-38.09 110.15.62 39.56-2.62 79.14-9.59 117.61-1.56 8.7 4.19 17.03 12.91 18.61.97.17 1.91.25 2.87.25 7.56 0 14.31-5.42 15.72-13.14 7.34-40.53 10.72-82.18 10.09-123.82-.53-33.01 10.19-63.95 30.91-89.47 5.59-6.86 4.53-16.94-2.31-22.51-6.85-5.55-16.91-4.57-22.51 2.32zm399.22 103.03c-.25-16.5-2.19-33.03-5.75-49.14-1.91-8.61-10.34-14-19.06-12.17-8.62 1.91-14.09 10.44-12.19 19.08 3.09 14 4.78 28.39 5 42.73.12 7.66.16 15.31.09 22.97-.06 8.83 7.03 16.05 15.87 16.12h.12c8.78 0 15.94-7.08 16-15.87.07-7.91.04-15.81-.08-23.72zM252.6.05C182.63-1.29 118.32 24.88 70.32 72.91 24.04 119.22-.87 180.76.16 246.2c.12 7.55.06 15.09-.16 22.62-.25 8.83 6.72 16.2 15.56 16.45H16c8.62 0 15.75-6.87 16-15.55.22-8 .28-16.02.16-24.03-.9-56.69 20.69-110.04 60.78-150.16 41.78-41.84 98.9-64.37 159.15-63.48 74.69 1.09 144.87 38.23 187.75 99.32 5.12 7.23 15.12 8.97 22.28 3.91 7.25-5.08 9-15.05 3.91-22.28C417.28 43.52 337.5 1.3 252.6.05zm1.28 84.93a182.36 182.36 0 0 0-45.19 4.91c-8.59 2.03-13.91 10.66-11.87 19.26 2.03 8.58 10.75 13.87 19.25 11.86 12.12-2.86 25.09-4.25 37.31-4.03 82.78 1.22 151.12 67.56 152.37 147.89.62 39.65-1.34 79.59-5.81 118.7-1 8.78 5.31 16.7 14.09 17.7.62.08 1.22.11 1.84.11 8 0 14.94-6.02 15.87-14.19 4.62-40.47 6.62-81.79 6-122.82-1.49-97.44-83.99-177.92-183.86-179.39z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -1,5 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 746 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"/></svg>

Before

Width:  |  Height:  |  Size: 468 B

View file

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<g fill="#f4c1c6">
<path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm121.6 313.1c4.7 4.7 4.7 12.3 0 17L338 377.6c-4.7 4.7-12.3 4.7-17 0L256 312l-65.1 65.6c-4.7 4.7-12.3 4.7-17 0L134.4 338c-4.7-4.7-4.7-12.3 0-17l65.6-65-65.6-65.1c-4.7-4.7-4.7-12.3 0-17l39.6-39.6c4.7-4.7 12.3-4.7 17 0l65 65.7 65.1-65.6c4.7-4.7 12.3-4.7 17 0l39.6 39.6c4.7 4.7 4.7 12.3 0 17L312 256l65.6 65.1z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 489 B

View file

@ -1,16 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
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

@ -1,142 +0,0 @@
/* 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

@ -1,5 +0,0 @@
// 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';

View file

@ -1,25 +0,0 @@
// Gets location values
const fetchAPI = (setData) => {
fetch('https://api.vytal.io/ip/')
.then((response) => response.json())
.then((json) => {
setData(json);
});
};
const checkWebWorker = (key, worker) => {
if (`${key}` !== `${worker}`) {
return `Did not match web worker (${worker})`;
}
return null;
};
const getWebWorker = () => {
let w;
if (typeof w === 'undefined') {
w = new Worker('worker.js');
}
return w;
};
export { fetchAPI, checkWebWorker, getWebWorker };

View file

@ -1,88 +0,0 @@
const getMap = (data) =>
`https://maps.googleapis.com/maps/api/staticmap?center=${data.lat},${data.lon}&markers=color:red%7Clabel:%7C${data.lat},${data.lon}&size=500x200&zoom=10&key=AIzaSyB-YN-X8PGBSPd7NOaQu4csVhgJUnF3ZGk`;
const compareTimeZone = (locationTimeZone, workerTimeZone) => {
if (locationTimeZone !== workerTimeZone) {
return "Location data doesn't match system data";
}
return null;
};
const checkProxy = (proxy) => {
if (proxy) {
return 'VPN/proxy has been detected';
}
return null;
};
// Returns object with location data
const getLocation = (data, workerData) => {
const timeZoneIssue = compareTimeZone(data.timezone, workerData.timeZone);
const isProxy = checkProxy(data.proxy);
return [
{
key: 'Country',
value: data.country,
issues: [timeZoneIssue, isProxy],
},
{
key: 'Region',
value: data.regionName,
issues: [timeZoneIssue, isProxy],
},
{
key: 'City',
value: data.city,
issues: [timeZoneIssue, isProxy],
},
{
key: 'Time zone',
value: data.timezone,
issues: [timeZoneIssue, isProxy],
},
{
key: 'Zip code',
value: data.zip,
issues: [timeZoneIssue, isProxy],
},
{
key: 'Latitude',
value: data.lat,
issues: [timeZoneIssue, isProxy],
},
{
key: 'Longitude',
value: data.lon,
issues: [timeZoneIssue, isProxy],
},
];
};
// Returns object with location data
const getConnection = (data) => {
const isProxy = checkProxy(data.proxy);
return [
{
key: 'IP address',
value: data.query,
issues: [isProxy],
},
{
key: 'ISP',
value: data.isp,
issues: [isProxy],
},
{
key: 'Org',
value: data.org,
issues: [isProxy],
},
{
key: 'ASN',
value: data.as,
issues: [isProxy],
},
];
};
export { getMap, getConnection, getLocation };

View file

@ -1,39 +0,0 @@
import axios from 'axios';
import md5 from 'crypto-js/md5';
const getSignature = (hash, setSignature, setload) => {
axios
.get(`https://api.vytal.io/fingerprint/?hash=${hash}`)
.then((response) => {
if (response.data.length !== 0) {
setSignature(response.data[response.data.length - 1].name);
}
setload(true);
});
};
const postSignature = (hash, e, setSignature) => {
e.preventDefault();
axios.post('https://api.vytal.io/fingerprint/', {
name: e.target[0].value,
hash,
});
setSignature(e.target[0].value);
};
const getHash = (data) => md5(JSON.stringify(data)).toString();
const getFingerprint = (signature, hash) => [
{
key: 'Signature',
value: signature,
issues: [],
},
{
key: 'Hash',
value: hash,
issues: [],
},
];
export { getSignature, postSignature, getHash, getFingerprint };

View file

@ -1,26 +0,0 @@
import { checkWebWorker } from './common';
const getLocale = (locale) => ({
key: 'Locale',
code: 'Intl.DateTimeFormat().resolvedOptions().locale',
value: Intl.DateTimeFormat().resolvedOptions().locale,
issues: [
checkWebWorker(Intl.DateTimeFormat().resolvedOptions().locale, locale),
],
});
const getTimezone = (timeZone) => ({
key: 'Timezone',
code: 'Intl.DateTimeFormat().resolvedOptions().timeZone',
value: Intl.DateTimeFormat().resolvedOptions().timeZone,
issues: [
checkWebWorker(Intl.DateTimeFormat().resolvedOptions().timeZone, timeZone),
],
});
const getIntl = (workerData) => [
getLocale(workerData.locale),
getTimezone(workerData.timeZone),
];
export default getIntl;

View file

@ -1,262 +0,0 @@
import { checkWebWorker } from './common';
const getDeviceMemory = (worker) => {
const name = 'deviceMemory';
return {
key: 'Device memory',
code: `navigator.${name}`,
value: navigator[name],
issues: [
checkNavigatorProperties(name),
checkNavigatorValue(name),
checkNavigatorPrototype(name),
checkWebWorker(navigator[name], worker),
],
};
};
const getHardwareConcurrency = (worker) => {
const name = 'hardwareConcurrency';
return {
key: 'Hardware concurrency',
code: `navigator.${name}`,
value: navigator[name],
issues: [
checkNavigatorProperties(name),
checkNavigatorValue(name),
checkNavigatorPrototype(name),
checkWebWorker(navigator[name], worker),
],
};
};
const getMaxTouchPoints = () => {
const name = 'maxTouchPoints';
return {
key: 'Max touch points',
code: `navigator.${name}`,
value: navigator[name],
issues: [
checkNavigatorProperties(name),
checkNavigatorValue(name),
checkNavigatorPrototype(name),
],
};
};
const getPlatform = (worker) => {
const name = 'platform';
return {
key: 'Platform',
code: `navigator.${name}`,
value: navigator[name],
issues: [
checkNavigatorProperties(name),
checkNavigatorValue(name),
checkNavigatorPrototype(name),
checkWebWorker(navigator[name], worker),
],
};
};
const getUserAgent = (worker) => {
const name = 'userAgent';
return {
key: 'User agent',
code: `navigator.${name}`,
value: navigator[name],
issues: [
checkNavigatorProperties(name),
checkNavigatorValue(name),
checkNavigatorPrototype(name),
checkWebWorker(navigator[name], worker),
],
};
};
const getAppVersion = (worker) => {
const name = 'appVersion';
return {
key: 'App version',
code: `navigator.${name}`,
value: navigator[name],
issues: [
checkNavigatorProperties(name),
checkNavigatorValue(name),
checkNavigatorPrototype(name),
checkWebWorker(navigator[name], worker),
],
};
};
const getLanguage = (worker) => {
const name = 'language';
return {
key: 'Language',
code: `navigator.${name}`,
value: navigator[name],
issues: [
checkNavigatorProperties(name),
checkNavigatorValue(name),
checkNavigatorPrototype(name),
checkWebWorker(navigator[name], worker),
],
};
};
const getLanguages = () => {
const name = 'languages';
return {
key: 'Languages',
code: `navigator.${name}`,
value: sortArr(navigator[name]),
issues: [
checkNavigatorProperties(name),
checkNavigatorValue(name),
checkNavigatorPrototype(name),
],
};
};
const getCookieEnabled = () => {
const name = 'cookieEnabled';
return {
key: 'Cookie enabled',
code: `navigator.${name}`,
value: navigator[name] ? 'True' : 'False',
issues: [
checkNavigatorProperties(name),
checkNavigatorValue(name),
checkNavigatorPrototype(name),
],
};
};
const getDoNotTrack = () => {
const name = 'doNotTrack';
return {
key: 'Do not track',
code: `navigator.${name}`,
value: navigator[name] ? 'True' : 'False',
issues: [
checkNavigatorProperties(name),
checkNavigatorValue(name),
checkNavigatorPrototype(name),
],
};
};
const getWebDriver = () => {
const name = 'webdriver';
return {
key: 'Web driver',
code: `navigator.${name}`,
value: navigator[name] ? 'True' : 'False',
issues: [
checkNavigatorProperties(name),
checkNavigatorValue(name),
checkNavigatorPrototype(name),
],
};
};
const getPlugins = () => {
const name = 'plugins';
return {
key: 'Plugins',
code: `navigator.${name}`,
value: sortPlugins(navigator[name]),
issues: [
checkNavigatorProperties(name),
checkNavigatorValue(name),
checkNavigatorPrototype(name),
],
};
};
const getVendor = () => {
const name = 'vendor';
return {
key: 'Vendor',
code: `navigator.${name}`,
value: navigator[name],
issues: [
checkNavigatorProperties(name),
checkNavigatorValue(name),
checkNavigatorPrototype(name),
],
};
};
// 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 checkNavigatorProperties = (name) => {
if (Object.getOwnPropertyDescriptor(navigator, name) !== undefined) {
return 'Failed undefined properties';
}
return null;
};
const checkNavigatorValue = (name) => {
try {
// eslint-disable-next-line no-unused-vars
const { value } = Object.getOwnPropertyDescriptor(
Navigator.prototype,
name
);
} catch (err) {
return 'Failed Navigator property value';
}
return null;
};
const checkNavigatorPrototype = (name) => {
try {
// eslint-disable-next-line no-unused-vars
const check = Navigator.prototype[name];
return 'Failed Navigator.prototype';
} catch (err) {
// eslint-disable-next-line no-unused-vars
const check = '';
}
return null;
};
const getNavigator = (workerData) => [
getDeviceMemory(workerData.deviceMemory),
getHardwareConcurrency(workerData.hardwareConcurrency),
getMaxTouchPoints(),
getPlatform(workerData.platform),
getUserAgent(workerData.userAgent),
getAppVersion(workerData.appVersion),
getLanguage(workerData.language),
getLanguages(),
getCookieEnabled(),
getDoNotTrack(),
getWebDriver(),
getPlugins(),
getVendor(),
];
export default getNavigator;

View file

@ -1,77 +0,0 @@
import { checkWebWorker } from './common';
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 checkDatePrototype = () => {
if (!Date.prototype.setDate.toString().includes('[native code]')) {
return 'Failed Date.prototype.setDate.toString()';
}
return null;
};
// Returns object with location data
const getOther = (battery, adBlock, workerData) => {
let batteryLevel, batteryStatus;
if (battery !== 'N/A') {
batteryLevel = `${Math.round(battery.level * 100)}%`;
batteryStatus = battery.charging ? 'Charging' : 'Not charging';
}
return [
{
key: 'Brave browser',
code: 'navigator.brave',
value: navigator.brave ? 'True' : 'False',
issues: [],
},
{
key: 'Tor browser',
value: detectTor() ? 'True' : 'False',
issues: [],
},
{
key: 'Adblock',
value: adBlock ? 'True' : 'False',
issues: [],
},
{
key: 'Date',
code: 'new Date().toString()',
value: new Date().toString(),
issues: [checkDatePrototype()],
},
{
key: 'Timezone offset',
code: 'new Date().getTimezoneOffset()',
value: new Date().getTimezoneOffset(),
issues: [
checkDatePrototype(),
checkWebWorker(
new Date().getTimezoneOffset(),
workerData.timezoneOffset
),
],
},
{
key: 'Battery level',
value: batteryLevel,
issues: [],
},
{
key: 'Battery status',
value: batteryStatus,
issues: [],
},
];
};
export default getOther;

View file

@ -1,161 +0,0 @@
const getWidth = () => {
const name = 'width';
return {
key: 'Width',
code: `window.screen.${name}`,
value: window.screen[name],
issues: [
checkScreenProperties('width'),
checkScreenValue('width'),
checkScreenPrototype('width'),
checkWidth(),
],
};
};
const getAvailWidth = () => {
const name = 'availWidth';
return {
key: 'Avail width',
code: `window.screen.${name}`,
value: window.screen[name],
issues: [
checkScreenProperties('availWidth'),
checkScreenValue('availWidth'),
checkScreenPrototype('availWidth'),
checkWidth(),
],
};
};
const getOuterWidth = () => {
const name = 'outerWidth';
return {
key: 'Outer width',
code: `window.${name}`,
value: window[name],
issues: [],
};
};
const getHeight = () => {
const name = 'height';
return {
key: 'Height',
code: `window.screen.${name}`,
value: window.screen[name],
issues: [
checkScreenProperties('height'),
checkScreenValue('height'),
checkScreenPrototype('height'),
],
};
};
const getAvailHeight = () => {
const name = 'availHeight';
return {
key: 'Avail height',
code: `window.screen.${name}`,
value: window.screen[name],
issues: [
checkScreenProperties('availHeight'),
checkScreenValue('availHeight'),
checkScreenPrototype('availHeight'),
checkHeight(),
],
};
};
const getOuterHeight = () => {
const name = 'outerHeight';
return {
key: 'Outer height',
code: `window.${name}`,
value: window[name],
issues: [],
};
};
const getPixelDepth = () => {
const name = 'pixelDepth';
return {
key: 'Pixel depth',
code: `window.screen.${name}`,
value: window.screen[name],
issues: [
checkScreenProperties('pixelDepth'),
checkScreenValue('pixelDepth'),
checkScreenPrototype('pixelDepth'),
],
};
};
const getColorDepth = () => {
const name = 'colorDepth';
return {
key: 'Color depth',
code: `window.screen.${name}`,
value: window.screen[name],
issues: [
checkScreenProperties('colorDepth'),
checkScreenValue('colorDepth'),
checkScreenPrototype('colorDepth'),
],
};
};
const checkScreenValue = (name) => {
if (
Object.getOwnPropertyDescriptor(Screen.prototype, name).value !== undefined
) {
return 'Failed descriptor.value undefined';
}
return null;
};
const checkScreenPrototype = (name) => {
try {
// eslint-disable-next-line no-unused-vars
const check = Screen.prototype[name];
return 'Failed Screen.prototype';
} catch (err) {
// eslint-disable-next-line no-unused-vars
const check = '';
}
return null;
};
const checkWidth = () => {
if (window.screen.availWidth > window.screen.width) {
return 'Avail width is greater than width';
}
return null;
};
const checkHeight = () => {
if (window.screen.availHeight > window.screen.height) {
return 'Avail height is greater than height';
}
return null;
};
const checkScreenProperties = (name) => {
if (Object.getOwnPropertyDescriptor(window.screen, name) !== undefined) {
return 'Failed undefined properties';
}
return null;
};
const getScreen = () => [
getWidth(),
getAvailWidth(),
getOuterWidth(),
getHeight(),
getAvailHeight(),
getOuterHeight(),
getPixelDepth(),
getColorDepth(),
];
export default getScreen;

View file

@ -1,49 +0,0 @@
import Bowser from 'bowser';
import { checkWebWorker } from './common';
const getUserAgentData = (key, userAgent, workerAgent) => ({
key,
value: userAgent || 'N/A',
issues: [checkWebWorker(userAgent, workerAgent)],
});
// Returns object with location data
const getUserAgent = (workerAgent) => {
const userAgentParsed = Bowser.parse(navigator.userAgent);
const workerAgentParsed = Bowser.parse(workerAgent);
return [
getUserAgentData(
'Browser',
userAgentParsed.browser.name,
workerAgentParsed.browser.name
),
getUserAgentData(
'Browser version',
userAgentParsed.browser.version,
workerAgentParsed.browser.version
),
getUserAgentData('OS', userAgentParsed.os.name, workerAgentParsed.os.name),
getUserAgentData(
'OS version',
userAgentParsed.os.versionName,
workerAgentParsed.os.versionName
),
getUserAgentData(
'Engine',
userAgentParsed.engine.name,
workerAgentParsed.engine.name
),
getUserAgentData(
'Engine version',
userAgentParsed.engine.version,
workerAgentParsed.engine.version
),
getUserAgentData(
'Platform type',
userAgentParsed.platform.type,
workerAgentParsed.platform.type
),
];
};
export default getUserAgent;

File diff suppressed because it is too large Load diff

19779
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

58
package.json Executable file
View file

@ -0,0 +1,58 @@
{
"name": "vytal",
"version": "1.0.0",
"description": "Vytal",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/z0ccc/Vytal.git"
},
"scripts": {
"build": "node utils/build.js",
"start": "node utils/webserver.js",
"prettier": "prettier --write '**/*.{js,jsx,ts,tsx,json,css,scss,md}'"
},
"dependencies": {
"@hot-loader/react-dom": "^17.0.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hot-loader": "^4.13.0"
},
"devDependencies": {
"@babel/core": "^7.17.0",
"@babel/plugin-proposal-class-properties": "^7.16.7",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@types/chrome": "^0.0.177",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.3",
"babel-preset-react-app": "^10.0.1",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^7.0.0",
"css-loader": "^6.6.0",
"eslint": "^8.8.0",
"eslint-config-react-app": "^7.0.0",
"eslint-plugin-flowtype": "^8.0.3",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"file-loader": "^6.2.0",
"fs-extra": "^10.0.0",
"html-loader": "^3.1.0",
"html-webpack-plugin": "^5.5.0",
"node-sass": "^6.0.1",
"prettier": "^2.5.1",
"sass-loader": "^12.4.0",
"source-map-loader": "^3.0.1",
"style-loader": "^3.3.1",
"terser-webpack-plugin": "^5.3.1",
"ts-loader": "^9.2.6",
"typescript": "^4.5.5",
"webpack": "^5.68.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.4"
}
}

BIN
promo/screenshot-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
promo/screenshot-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

BIN
src/assets/img/icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
src/assets/img/icon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

1
src/assets/img/link.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="#fff" d="M256 64C256 46.33 270.3 32 288 32H415.1C415.1 32 415.1 32 415.1 32C420.3 32 424.5 32.86 428.2 34.43C431.1 35.98 435.5 38.27 438.6 41.3C438.6 41.35 438.6 41.4 438.7 41.44C444.9 47.66 447.1 55.78 448 63.9C448 63.94 448 63.97 448 64V192C448 209.7 433.7 224 416 224C398.3 224 384 209.7 384 192V141.3L214.6 310.6C202.1 323.1 181.9 323.1 169.4 310.6C156.9 298.1 156.9 277.9 169.4 265.4L338.7 96H288C270.3 96 256 81.67 256 64V64zM0 128C0 92.65 28.65 64 64 64H160C177.7 64 192 78.33 192 96C192 113.7 177.7 128 160 128H64V416H352V320C352 302.3 366.3 288 384 288C401.7 288 416 302.3 416 320V416C416 451.3 387.3 480 352 480H64C28.65 480 0 451.3 0 416V128z"/></svg>

After

Width:  |  Height:  |  Size: 736 B

View file

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

21
src/manifest.json Executable file
View file

@ -0,0 +1,21 @@
{
"manifest_version": 3,
"name": "Vytal",
"version": "1.0.2",
"description": "Protect your privacy by mocking your personal data.",
"permissions": ["storage", "debugger", "activeTab", "alarms"],
"background": { "service_worker": "background.bundle.js" },
"action": {
"default_popup": "popup.html",
"default_icon": "icon-32.png"
},
"icons": {
"128": "icon-128.png"
},
"web_accessible_resources": [
{
"resources": ["icon-128.png", "icon-32.png"],
"matches": []
}
]
}

View file

@ -0,0 +1,115 @@
import userAgents from '../../utils/userAgents'
const attachTab = (tabId) => {
chrome.storage.sync.get(
[
'ipData',
'timezone',
'timezoneMatchIP',
'lat',
'latitudeMatchIP',
'lon',
'longitudeMatchIP',
'locale',
'localeMatchIP',
'userAgent',
],
(result) => {
if (
result.timezone ||
result.lat ||
result.lon ||
result.locale ||
result.userAgent
) {
chrome.debugger.attach({ tabId: tabId }, '1.3', () => {
if (!chrome.runtime.lastError) {
if (result.timezone) {
chrome.debugger.sendCommand(
{ tabId: tabId },
'Emulation.setTimezoneOverride',
{
timezoneId: result.timezone,
},
() => {
if (
chrome.runtime.lastError &&
chrome.runtime.lastError.message.includes(
'Timezone override is already in effect'
)
) {
chrome.debugger.detach({ tabId })
attachTab(tabId)
}
}
)
}
if (result.locale) {
chrome.debugger.sendCommand(
{ tabId: tabId },
'Emulation.setLocaleOverride',
{
locale: result.locale,
}
)
}
if (result.lat || result.lon) {
chrome.debugger.sendCommand(
{ tabId: tabId },
'Emulation.setGeolocationOverride',
{
latitude: result.lat
? parseFloat(result.lat)
: result.ipData.lat,
longitude: result.lon
? parseFloat(result.lon)
: result.ipData.lon,
accuracy: 1,
}
)
}
if (result.userAgent) {
chrome.debugger.sendCommand(
{ tabId: tabId },
'Emulation.setUserAgentOverride',
{
userAgent: result.userAgent,
}
// { acceptLanguage: "en-CA" },
// { platform: "WebTV OS" }
)
// 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.69',
}
}
})
}
}
)
}
chrome.tabs.onUpdated.addListener((tabId, change, tab) => {
chrome.debugger.getTargets((tabs) => {
const currentTab = tabs.find((obj) => obj.tabId === tabId)
if (!currentTab.attached) {
attachTab(tabId)
}
})
})
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'userAgentAlarm') {
chrome.storage.sync.get(['randomUA'], (result) => {
if (result.randomUA) {
console.log('userAgentAlarm')
const randomUserAgent =
userAgents[Math.floor(Math.random() * userAgents.length)]
chrome.storage.sync.set({
userAgent: randomUserAgent,
})
}
})
}
})

View file

@ -0,0 +1,59 @@
import React, { useState, useEffect } from 'react'
import profiles from '../../utils/profiles'
import countryLocales from '../../utils/countryLocales'
const DebugSettings = ({ type, title, ip, profile, setProfile }) => {
const [value, setValue] = useState('')
useEffect(() => {
if (profile === 'none') {
setValue('')
chrome.storage.sync.set({ [type]: '' })
} else if (profile === 'match') {
if (ip) {
const ipTypeValue =
type === 'locale' ? countryLocales[ip.countryCode].locale : ip[type]
setValue(ipTypeValue)
chrome.storage.sync.set({ [type]: ipTypeValue })
}
} else if (profile === 'custom') {
chrome.storage.sync.get([type], (result) => {
console.log(type)
console.log(result)
result[type] && setValue(result[type])
})
} else if (profile !== 'default') {
setValue(profiles[profile][type])
chrome.storage.sync.set({ [type]: profiles[profile][type] })
}
}, [ip, profile, type])
const changeTextValue = (e) => {
chrome.storage.sync.set({ timezone: e.target.value })
setValue(e.target.value)
chrome.storage.sync.set({ profile: 'custom' })
setProfile('custom')
}
return (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
margin: '12px 0 0 0',
}}
>
<input
type="text"
value={value}
onChange={changeTextValue}
style={{
width: '206px',
}}
/>
<label>{title}</label>
</div>
)
}
export default DebugSettings

View file

@ -0,0 +1,31 @@
import React from 'react'
import detachDebugger from '../../utils/detachDebugger'
const getFlagEmoji = (countryCode) => {
const codePoints = countryCode
.toUpperCase()
.split('')
.map((char) => 127397 + char.charCodeAt())
return String.fromCodePoint(...codePoints)
}
const IpSettings = ({ ip, getIP, setIP }) => {
return (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>
Current IP: {ip && `${ip.query} ${getFlagEmoji(ip.countryCode)}`}
</div>
<button
type="button"
onClick={() => {
Promise.resolve(getIP()).then((ipData) => setIP(ipData))
detachDebugger()
}}
>
Reload
</button>
</div>
)
}
export default IpSettings

45
src/pages/Popup/Navbar.js Normal file
View file

@ -0,0 +1,45 @@
import * as React from 'react'
import Logo from '../../assets/img/logo.svg'
import Link from '../../assets/img/link.svg'
const Navbar = () => (
<div
style={{
width: '100%',
boxSizing: 'border-box',
display: 'flex',
justifyContent: 'space-between',
padding: '8px 12px',
backgroundColor: 'var(--navbar)',
alignItems: 'center',
}}
>
<img
src={Logo}
style={{
width: '100px',
height: '24px',
}}
alt="Vytal logo"
/>
<a
href="https://vytal.io/scan.html"
target="_blank"
rel="noreferrer"
style={{
height: '18px',
}}
>
<img
src={Link}
alt="Test Vytal"
style={{
width: '18px',
height: '18px',
}}
/>
</a>
</div>
)
export default Navbar

83
src/pages/Popup/Popup.jsx Normal file
View file

@ -0,0 +1,83 @@
import React, { useState, useEffect } from 'react'
import Navbar from './Navbar'
import IpSettings from './IpSettings'
import ProfileSelect from './ProfileSelect'
import DebugSettings from './DebugSettings'
import UserAgentSettings from './UserAgentSettings'
const getIP = () =>
fetch('http://ip-api.com/json/')
.then((response) => response.json())
.then((ipData) => {
chrome.storage.sync.set({ ipData })
return ipData
})
const Popup = () => {
const [ip, setIP] = useState(null)
const [profile, setProfile] = useState('default')
useEffect(() => {
chrome.storage.sync.get(['profile', 'ipData'], (result) => {
result.profile && setProfile(result.profile)
if (result.ipData) {
setIP(result.ipData)
} else {
Promise.resolve(getIP()).then((ipData) => setIP(ipData))
}
})
}, [])
return (
<div className="App">
<Navbar />
<div
style={{
padding: '12px',
}}
>
<IpSettings ip={ip} getIP={getIP} setIP={setIP} />
<ProfileSelect profile={profile} setProfile={setProfile} />
<DebugSettings
type="timezone"
title="Timezone"
ip={ip}
profile={profile}
setProfile={setProfile}
/>
<DebugSettings
type="locale"
title="Locale"
ip={ip}
profile={profile}
setProfile={setProfile}
/>
<DebugSettings
type="lat"
title="Latitude"
ip={ip}
profile={profile}
setProfile={setProfile}
/>
<DebugSettings
type="lon"
title="Longitude"
ip={ip}
profile={profile}
setProfile={setProfile}
/>
<UserAgentSettings ip={ip} type="lat" title="Latitude" />
<div
style={{
margin: '8px 0 0 0',
fontSize: '10px',
}}
>
Tabs need to be initialized for full protection.
</div>
</div>
</div>
)
}
export default Popup

View file

@ -0,0 +1,47 @@
import React from 'react'
import profiles from '../../utils/profiles'
import detachDebugger from '../../utils/detachDebugger'
const ProfileSelect = ({ profile, setProfile }) => {
const changeProfile = (e) => {
detachDebugger()
chrome.storage.sync.set({
profile: e.target.value,
})
setProfile(e.target.value)
}
return (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
margin: '12px 0 0 0',
}}
>
<select
name="profile"
id="profile"
value={profile}
onChange={changeProfile}
style={{
width: '214px',
}}
>
<option value="none">None</option>
<option value="match">Match IP</option>
<option value="custom">Custom</option>
<optgroup label="Locations">
{Object.keys(profiles).map((key) => (
<option value={key} key={key}>
{profiles[key].name}
</option>
))}
</optgroup>
</select>
<label>Profile</label>
</div>
)
}
export default ProfileSelect

View file

@ -0,0 +1,106 @@
import React, { useState, useEffect } from 'react'
import userAgents from '../../utils/userAgents'
import detachDebugger from '../../utils/detachDebugger'
const UserAgentSettings = () => {
const [userAgent, setUserAgent] = useState('')
const [randomUA, setRandomUA] = useState(false)
const [interval, setInterval] = useState(60)
useEffect(() => {
chrome.storage.sync.get(['userAgent', 'randomUA', 'interval'], (result) => {
result.interval && setInterval(result.interval)
result.randomUA && setRandomUA(true)
if (result.userAgent) {
setUserAgent(result.userAgent)
}
})
}, [])
const changeUserAgent = (e) => {
detachDebugger()
chrome.storage.sync.set({ userAgent: e.target.value })
setUserAgent(e.target.value)
}
const randomize = (e) => {
detachDebugger()
const randomUserAgent =
userAgents[Math.floor(Math.random() * userAgents.length)]
chrome.storage.sync.set({
userAgent: e.target.checked ? randomUserAgent : null,
randomUA: e.target.checked,
})
e.target.checked ? setUserAgent(randomUserAgent) : setUserAgent('')
setRandomUA(e.target.checked)
if (parseInt(interval)) {
chrome.alarms.create('userAgentAlarm', {
delayInMinutes: parseInt(interval),
periodInMinutes: parseInt(interval),
})
}
}
const changeInterval = (e) => {
chrome.storage.sync.set({ interval: e.target.value })
setInterval(e.target.value)
if (parseInt(e.target.value)) {
chrome.alarms.create('userAgentAlarm', {
delayInMinutes: parseInt(e.target.value),
periodInMinutes: parseInt(e.target.value),
})
}
}
return (
<>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
margin: '12px 0 0 0',
}}
>
<input
type="text"
value={userAgent}
onChange={changeUserAgent}
style={{
width: '206px',
}}
/>
<label>User Agent</label>
</div>
<div
style={{
margin: '6px 0 0 0',
}}
>
<label
style={{
margin: '0 6px 0 0',
}}
>
<input type="checkbox" checked={randomUA} onChange={randomize} />
Randomize every
</label>
<label>
<input
type="number"
value={interval}
onChange={changeInterval}
style={{
width: '30px',
margin: '0 4px 0 0',
}}
/>
minutes
</label>
</div>
</>
)
}
export default UserAgentSettings

26
src/pages/Popup/index.css Normal file
View file

@ -0,0 +1,26 @@
:root {
--main: #943ec5;
--text: #212121;
--background: #fff;
--scrollbar: #ccc;
--navbar: #943ec5;
--icon: #aab7b8;
--border: #f0f3f4;
}
body {
color: var(--text);
background-color: var(--background);
font-size: 13px;
line-height: 22px;
width: 315px;
margin: 0;
font-family: 'Segoe UI', Tahoma, sans-serif;
}
input[type=checkbox] {
vertical-align: middle;
position: relative;
bottom: 1px;
margin: 0 4px 0 0
}

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title></title>
</head>
<body>
<div id="app-container"></div>
</body>
</html>

View file

@ -0,0 +1,9 @@
import React from 'react';
import { render } from 'react-dom';
import Popup from './Popup';
import './index.css';
render(<Popup />, window.document.querySelector('#app-container'));
if (module.hot) module.hot.accept();

207
src/utils/countryLocales.js Normal file
View file

@ -0,0 +1,207 @@
const countryLocales = {
AD: { locale: 'ca-AD' },
AE: { locale: 'ar-AE' },
AF: { locale: 'fa-AF' },
AG: { locale: 'en-AG' },
AL: { locale: 'sq-AL' },
AM: { locale: 'hy-AM' },
AO: { locale: 'ln-AO' },
AQ: { locale: 'en-US' },
AR: { locale: 'es-AR' },
AT: { locale: 'de-AT' },
AU: { locale: 'en-AU' },
AZ: { locale: 'az-AZ' },
BA: { locale: 'bs-BA' },
BB: { locale: 'en-BB' },
BD: { locale: 'bn-BD' },
BE: { locale: 'nl-BE' },
BF: { locale: 'fr-BF' },
BG: { locale: 'bg-BG' },
BH: { locale: 'ar-BH' },
BJ: { locale: 'fr-BJ' },
BI: { locale: 'fr-BI' },
BM: { locale: 'en-BM' },
BN: { locale: 'ms-BN' },
BR: { locale: 'pt-BR' },
BO: { locale: 'es-BO' },
BS: { locale: 'en-BS' },
BT: { locale: 'dz-BT' },
BW: { locale: 'en-BW' },
BY: { locale: 'be-BY' },
BZ: { locale: 'en-BZ' },
CA: { locale: 'en-CA' },
CD: { locale: 'fr-CD' },
CF: { locale: 'fr-CF' },
CG: { locale: 'fr-CG' },
CH: { locale: 'de-CH' },
CI: { locale: 'en-CA' },
CL: { locale: 'es-CL' },
CM: { locale: 'fr-CM' },
CN: { locale: 'zh-CN' },
CO: { locale: 'es-CO' },
CR: { locale: 'es-CR' },
CU: { locale: 'es-CU' },
CV: { locale: 'pt-CV' },
CY: { locale: 'el-CY' },
CZ: { locale: 'cs-CZ' },
DE: { locale: 'de-DE' },
DJ: { locale: 'fr-DJ' },
DK: { locale: 'da-DK' },
DM: { locale: 'en-DM' },
DO: { locale: 'es-DO' },
DZ: { locale: 'ar-DZ' },
EC: { locale: 'es-EC' },
EE: { locale: 'et-EE' },
EG: { locale: 'ar-EG' },
ES: { locale: 'es-ES' },
ER: { locale: 'ti-ER' },
ET: { locale: 'om-ET' },
FI: { locale: 'fi-FI' },
FJ: { locale: 'en-FJ' },
FM: { locale: 'en-FM' },
FR: { locale: 'fr-FR' },
GA: { locale: 'fr-GA' },
GB: { locale: 'en-GB' },
GD: { locale: 'en-GD' },
GE: { locale: 'ka-GE' },
GH: { locale: 'ak-GH' },
GM: { locale: 'en-GM' },
GN: { locale: 'fr-GN' },
GQ: { locale: 'es-GQ' },
GR: { locale: 'el-GR' },
GT: { locale: 'es-GT' },
GU: { locale: 'en-GU' },
GW: { locale: 'pt-GW' },
GY: { locale: 'en-GY' },
HK: { locale: 'zh-HK' },
HN: { locale: 'es-HN' },
HR: { locale: 'hr-HR' },
HT: { locale: 'fr-HT' },
HU: { locale: 'hu-HU' },
ID: { locale: 'id-ID' },
IE: { locale: 'en-IE' },
IL: { locale: 'he-IL' },
IN: { locale: 'hi-IN' },
IQ: { locale: 'ar-IQ' },
IR: { locale: 'fa-IR' },
IS: { locale: 'is-IS' },
IT: { locale: 'it-IT' },
JM: { locale: 'en-JM' },
JO: { locale: 'ar-JO' },
JP: { locale: 'ja-JP' },
KE: { locale: 'en-KE' },
KG: { locale: 'ky-KG' },
KI: { locale: 'en-KI' },
KH: { locale: 'km-KH' },
KM: { locale: 'fr-KM' },
KN: { locale: 'en-KN' },
KP: { locale: 'ko-KP' },
KW: { locale: 'ar-KW' },
KY: { locale: 'en-KY' },
KR: { locale: 'ko-KR' },
KZ: { locale: 'kk-KZ' },
LA: { locale: 'lo-LA' },
LB: { locale: 'ar-LB' },
LC: { locale: 'en-LC' },
LI: { locale: 'de-LI' },
LK: { locale: 'si-LK' },
LR: { locale: 'en-LR' },
LS: { locale: 'en-LS' },
LT: { locale: 'lt-LT' },
LU: { locale: 'fr-LU' },
LV: { locale: 'lv-LV' },
LY: { locale: 'ar-LY' },
MA: { locale: 'ar-MA' },
MC: { locale: 'fr-MC' },
MD: { locale: 'ro-MD' },
ME: { locale: 'sr-Latn-ME' },
MF: { locale: 'fr-MF' },
MG: { locale: 'fr-MG' },
MH: { locale: 'en-MH' },
MK: { locale: 'mk-MK' },
ML: { locale: 'bm-ML' },
MM: { locale: 'my-MM' },
MN: { locale: 'mn-MN' },
MO: { locale: 'zh-MO' },
MP: { locale: 'en-MP' },
MR: { locale: 'fr-MR' },
MT: { locale: 'mt-MT' },
MU: { locale: 'en-MU' },
MV: { locale: 'dv-MV' },
MW: { locale: 'en-MW' },
MX: { locale: 'es-MX' },
MY: { locale: 'ms-MY' },
MZ: { locale: 'pt-MZ' },
NA: { locale: 'af-NA' },
NL: { locale: 'nl-NL' },
NE: { locale: 'fr-NE' },
NG: { locale: 'en-NG' },
NC: { locale: 'nl-NL' },
NI: { locale: 'es-NI' },
NO: { locale: 'nb-NO' },
NP: { locale: 'ne-NP' },
NR: { locale: 'en-NR' },
NZ: { locale: 'en-NZ' },
OM: { locale: 'ar-OM' },
PA: { locale: 'es-PA' },
PE: { locale: 'es-PE' },
PH: { locale: 'en-PH' },
PG: { locale: 'en-PG' },
PK: { locale: 'ur-PK' },
PS: { locale: 'ar-PS' },
PL: { locale: 'pl-PL' },
PT: { locale: 'pt-PT' },
PW: { locale: 'en-PW' },
PY: { locale: 'es-PY' },
QA: { locale: 'ar-QA' },
RO: { locale: 'ro-RO' },
RS: { locale: 'sr-RS' },
RU: { locale: 'ru-RU' },
RW: { locale: 'fr-RW' },
SA: { locale: 'ar-SA' },
SB: { locale: 'en-SB' },
SC: { locale: 'en-SC' },
SD: { locale: 'en-SD' },
SE: { locale: 'sv-SE' },
SG: { locale: 'zh-SG' },
SI: { locale: 'sl-SI' },
SL: { locale: 'en-SL' },
SK: { locale: 'sk-SK' },
SM: { locale: 'it-SM' },
SN: { locale: 'fr-SN' },
SO: { locale: 'en-SO' },
SR: { locale: 'nl-SR' },
SS: { locale: 'en-SS' },
ST: { locale: 'pt-ST' },
SV: { locale: 'es-SV' },
SY: { locale: 'ar-SY' },
SZ: { locale: 'en-SZ' },
TD: { locale: 'ar-TD' },
TG: { locale: 'fr-TG' },
TH: { locale: 'th-TH' },
TJ: { locale: 'ru-TJ' },
TL: { locale: 'pt-TL' },
TM: { locale: 'tk-TM' },
TN: { locale: 'ar-TN' },
TO: { locale: 'en-TO' },
TR: { locale: 'tr-TR' },
TT: { locale: 'en-TT' },
TV: { locale: 'en-TV' },
TZ: { locale: 'en-TZ' },
TW: { locale: 'zh-TW' },
UA: { locale: 'uk-UA' },
UG: { locale: 'en-UG' },
US: { locale: 'en-US' },
UY: { locale: 'es-UY' },
VA: { locale: 'it-VA' },
VC: { locale: 'en-VC' },
VN: { locale: 'vi-VN' },
VU: { locale: 'en-VU' },
WS: { locale: 'en-WS' },
YE: { locale: 'ar-YE' },
ZA: { locale: 'en-ZA' },
ZM: { locale: 'en-ZM' },
ZW: { locale: 'en-ZW' },
}
export default countryLocales

Some files were not shown because too many files have changed in this diff Show more