Compare commits
45 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68e8297aa2 | ||
|
|
24644bef87 | ||
|
|
302e66552d | ||
|
|
87c7f91fd0 | ||
|
|
68352f6776 | ||
|
|
c39e55f55b | ||
|
|
6defd8e801 | ||
|
|
39233de2d8 | ||
|
|
2db9eabf78 | ||
|
|
b9b8523eb0 | ||
|
|
10cd4dedce | ||
|
|
247e4edfe7 | ||
|
|
7ba33fb6c6 | ||
|
|
14774f2648 | ||
|
|
73ea2ede74 | ||
|
|
b25d62dd61 | ||
|
|
c321921f65 | ||
|
|
7dc60c2157 | ||
|
|
431d51fd0c | ||
|
|
506fad5c41 | ||
|
|
89f1faffa0 | ||
|
|
62bac340de | ||
|
|
c350da3cc3 | ||
|
|
b264e5f1bb | ||
|
|
27e0e414f9 | ||
|
|
ebf2e47b9c | ||
|
|
867b9ef33d | ||
|
|
6f202aeae5 | ||
|
|
83af9c1c7c | ||
|
|
b17865c13f | ||
|
|
bec416ae74 | ||
|
|
2117156138 | ||
|
|
8fc026b348 | ||
|
|
3a76962921 | ||
|
|
1c4ee91a1c | ||
|
|
9f7754641e | ||
|
|
6e2372b28d | ||
|
|
8653004136 | ||
|
|
d2860e202c | ||
|
|
f635a63a1e | ||
|
|
5fe9b413e3 | ||
|
|
62a1fc10d2 | ||
|
|
1da219b110 | ||
|
|
61d52ca16d | ||
|
|
ad3a42d040 |
11
.babelrc
Normal 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
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "react-app",
|
||||
"globals": {
|
||||
"chrome": "readonly"
|
||||
}
|
||||
}
|
||||
70
.gitignore
vendored
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"requirePragma": false,
|
||||
"arrowParens": "always",
|
||||
"semi": false,
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
6
LICENSE
|
|
@ -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
|
||||
|
|
@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
62
README.md
Normal file → Executable 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).
|
||||

|
||||
|
||||
### Avail height is greater than height
|
||||
|
||||
Happens when the height width is greater than the normal height (which is impossible).
|
||||

|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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'
|
||||
]
|
||||
|
|
@ -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')),
|
||||
]
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class VytalConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'vytal'
|
||||
|
|
@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('ip/', views.IPView, name='ip'),
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 786 B |
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
|
Before Width: | Height: | Size: 167 KiB |
|
|
@ -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);
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
const ContentBlock = ({ children }) => (
|
||||
<div className="contentBlock">{children}</div>
|
||||
);
|
||||
|
||||
export default ContentBlock;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { createContext } from 'react';
|
||||
|
||||
const DataContext = createContext();
|
||||
|
||||
export default DataContext;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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't common for your system
|
||||
can make you easily identifiable.
|
||||
</p>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
export default FontsBlock;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
.centerBlockOuter {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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();
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
11596
frontend/yarn.lock
19779
package-lock.json
generated
Normal file
58
package.json
Executable 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
|
After Width: | Height: | Size: 61 KiB |
BIN
promo/screenshot-2.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
src/assets/img/icon-128.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src/assets/img/icon-32.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
1
src/assets/img/link.svg
Normal 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 |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
21
src/manifest.json
Executable 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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
115
src/pages/Background/index.js
Normal 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,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
59
src/pages/Popup/DebugSettings.js
Normal 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
|
||||
31
src/pages/Popup/IpSettings.js
Normal 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
|
|
@ -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
|
|
@ -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
|
||||
47
src/pages/Popup/ProfileSelect.jsx
Normal 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
|
||||
106
src/pages/Popup/UserAgentSettings.js
Normal 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
|
|
@ -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
|
||||
}
|
||||
11
src/pages/Popup/index.html
Normal 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>
|
||||
9
src/pages/Popup/index.jsx
Normal 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
|
|
@ -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
|
||||