Compare commits
52 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdd58eb52b | ||
|
|
43403bb3a1 | ||
|
|
bc5409fba7 | ||
|
|
aec811d3b5 | ||
|
|
ea3c7fa141 | ||
|
|
799036b84a | ||
|
|
25c17a8175 | ||
|
|
2e1593fbc7 | ||
|
|
44baad7bb8 | ||
|
|
708a9d7f33 | ||
|
|
db2d39637d | ||
|
|
5691f9ed97 | ||
|
|
59d9128afe | ||
|
|
113f57d1dd | ||
|
|
143e646e4f | ||
|
|
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 |
|
|
@ -20,7 +20,10 @@ module.exports = {
|
|||
'operator-linebreak': 'off',
|
||||
'no-use-before-define': 'off',
|
||||
'linebreak-style': 'off',
|
||||
'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.js'] }],
|
||||
'react/jsx-filename-extension': [
|
||||
1,
|
||||
{ extensions: ['.jsx', '.js', '.ts', 'tsx'] },
|
||||
],
|
||||
'jsx-a11y/label-has-associated-control': 'off',
|
||||
'one-var': 'off',
|
||||
'one-var-declaration-per-line': 'off',
|
||||
|
|
@ -39,6 +42,9 @@ module.exports = {
|
|||
'react/react-in-jsx-scope': 'off',
|
||||
'no-bitwise': 'off',
|
||||
'react/no-array-index-key': 'off',
|
||||
'dot-notation': 'off',
|
||||
'nonblock-statement-body-position': 'off',
|
||||
'react/button-has-type': 'off',
|
||||
'no-unused-vars': 'warn',
|
||||
indent: 'off',
|
||||
},
|
||||
};
|
||||
75
.gitignore
vendored
|
|
@ -1,67 +1,26 @@
|
|||
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/articles/ignoring-files/ for more about ignoring files.
|
||||
/images
|
||||
App.test.js
|
||||
|
||||
# dependencies
|
||||
/frontend/node_modules
|
||||
/frontend/.pnp
|
||||
/frontend/.pnp.js
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# 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
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
/frontend/npm-debug.log*
|
||||
/frontend/yarn-debug.log*
|
||||
/frontend/yarn-error.log*
|
||||
15
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:8080",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
75
README.md
|
|
@ -1,77 +1,12 @@
|
|||
# Vytal
|
||||
|
||||
Check it out here: https://vytal.io.
|
||||
|
||||
## 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.
|
||||
|
||||
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 contains no ads and signup is not required.
|
||||
|
||||
## 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.
|
||||
|
||||
## Types of Tampering
|
||||
|
||||
### Failed Navigator.prototype
|
||||
|
||||
`Navigator.prototype[DataType]` returns a value if the data object was tampered with. Otherwise returns an error.
|
||||
|
||||
### Failed undefined properties
|
||||
|
||||
`Object.getOwnPropertyDescriptor(navigator, [DataType])` returns an object if the data object was tampered with. Otherwise returns ‘undefined’.
|
||||
|
||||
### Failed Navigator property value
|
||||
|
||||
`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.
|
||||
|
||||
### Failed Screen.prototype
|
||||
|
||||
`Screen.prototype[DataType]` returns a value if the data object was tampered with. Otherwise returns an error.
|
||||
|
||||
### Avail width is greater than width
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Frontend React setup:
|
||||
|
||||
```
|
||||
cd frontend
|
||||
yarn
|
||||
yarn run start
|
||||
```
|
||||
|
||||
The website can then be accessed at http://localhost:3000/.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
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,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,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="#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,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,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
35698
package-lock.json
generated
Normal file
|
|
@ -1,38 +1,40 @@
|
|||
{
|
||||
"homepage": "https://z0ccc.github.io",
|
||||
"homepage": "https://vytal.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",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@emotion/react": "^11.9.0",
|
||||
"@mdx-js/react": "^2.1.1",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"countries-and-timezones": "^3.2.2",
|
||||
"country-language": "^0.1.7",
|
||||
"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"
|
||||
"theme-ui": "^0.14.5",
|
||||
"typescript": "^4.6.4",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"predeploy": "yarn run build",
|
||||
"deploy": "echo 'vytal.io' > ./build/CNAME && gh-pages -d build",
|
||||
"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"
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
|
@ -47,11 +49,11 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^7.28.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react": "^7.25.1",
|
||||
"eslint-plugin-react-hooks": "^4.2.0"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
93
public/frame.html
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<script>
|
||||
const checkDatePrototype = () => {
|
||||
if (!Date.prototype.setDate.toString().includes('[native code]')) {
|
||||
return 'Failed Date.prototype.setDate.toString()';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkIntlConstructor = () => {
|
||||
if (!Object.getPrototypeOf(Intl.DateTimeFormat.prototype).constructor.toString().includes('Object')) {
|
||||
return 'Failed Object.getPrototypeOf(Intl.DateTimeFormat.prototype).constructor.toString()';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkIntlPrototype = () => {
|
||||
if (!Intl.DateTimeFormat.prototype.resolvedOptions.toString().includes('[native code]')) {
|
||||
return 'Failed Intl.DateTimeFormat.prototype.resolvedOptions.toString()';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkNavigatorProperties = (key) => {
|
||||
if (Object.getOwnPropertyDescriptor(navigator, key) !== undefined) {
|
||||
return 'Failed Object.getOwnPropertyDescriptor(navigator, key)';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkNavigatorValue = (key) => {
|
||||
if (
|
||||
Object.getOwnPropertyDescriptor(Navigator.prototype, key).value !==
|
||||
undefined
|
||||
) {
|
||||
return 'Failed object.getOwnPropertyDescriptor(Navigator.prototype, key).value';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkNavigatorPrototype = (key) => {
|
||||
try {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const check = Navigator.prototype[key];
|
||||
return 'Failed Navigator.prototype[key]';
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getNavigatorValue = (type) =>
|
||||
[checkNavigatorProperties(type),
|
||||
checkNavigatorValue(type),
|
||||
checkNavigatorPrototype(type)].filter(Boolean);
|
||||
|
||||
const data = {
|
||||
type: 'frameData',
|
||||
data: {
|
||||
locale: {
|
||||
value: Intl.DateTimeFormat().resolvedOptions().locale,
|
||||
issues: [checkIntlPrototype(), checkIntlConstructor()].filter(Boolean)
|
||||
},
|
||||
timeZone: {
|
||||
value: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
issues: [checkIntlPrototype(), checkIntlConstructor()].filter(Boolean)
|
||||
},
|
||||
timezoneOffset: {
|
||||
value: new Date().getTimezoneOffset(),
|
||||
issues: [checkDatePrototype()].filter(Boolean),
|
||||
},
|
||||
dateString: {
|
||||
value: new Date().toString(),
|
||||
issues: [checkDatePrototype()].filter(Boolean)
|
||||
},
|
||||
dateLocale: {
|
||||
value: new Date().toLocaleString(),
|
||||
issues: [checkDatePrototype()].filter(Boolean),
|
||||
},
|
||||
userAgent: {
|
||||
value: navigator.userAgent,
|
||||
issues: getNavigatorValue('userAgent'),
|
||||
},
|
||||
appVersion: {
|
||||
value: navigator.appVersion,
|
||||
issues: getNavigatorValue('appVersion'),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
parent.postMessage(data, '*');
|
||||
|
||||
</script>
|
||||
97
public/index.html
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<script>
|
||||
const initialDataObj = {
|
||||
locale: {
|
||||
value: Intl.DateTimeFormat().resolvedOptions().locale,
|
||||
issues: [],
|
||||
},
|
||||
timeZone: {
|
||||
value: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
issues: [],
|
||||
},
|
||||
timezoneOffset: {
|
||||
value: new Date().getTimezoneOffset(),
|
||||
issues: [],
|
||||
},
|
||||
dateString: {
|
||||
value: new Date().toString(),
|
||||
issues: [],
|
||||
},
|
||||
dateLocale: {
|
||||
value: new Date().toLocaleString(),
|
||||
issues: [],
|
||||
},
|
||||
userAgent: {
|
||||
value: navigator.userAgent,
|
||||
issues: [],
|
||||
},
|
||||
appVersion: {
|
||||
value: navigator.appVersion,
|
||||
issues: [],
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#943ec5" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Check if your location is actually hidden."
|
||||
/>
|
||||
<meta name="author" content="z0ccc" />
|
||||
<meta property="og:title" content="Vytal" />
|
||||
<meta property="og:url" content="https://vytal.io" />
|
||||
<meta
|
||||
property="og:image:secure"
|
||||
content="https://vytal.io/preview.png"
|
||||
/>
|
||||
<meta
|
||||
property="og:img"
|
||||
content="https://vytal.io/preview.png"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Check if your location is actually hidden."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:card"
|
||||
content="https://vytal.io/preview.png"
|
||||
/>
|
||||
<meta name="twitter:title" content="Vytal" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Check if your location is actually hidden."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://vytal.io/preview.png"
|
||||
/>
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo-192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>Vytal Extension Scan</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<iframe id="iframe" sandbox="allow-same-origin" style="display: none"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
BIN
public/logo-192.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/logo-512.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
25
public/manifest.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"short_name": "Vytal",
|
||||
"name": "Vytal",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "rgb(87, 35, 117)",
|
||||
"background_color": "rgb(87, 35, 117)"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB |
1
public/scan.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<script>window.location.replace("/");</script>
|
||||
83
public/worker.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
const checkDatePrototype = () => {
|
||||
if (!Date.prototype.setDate.toString().includes('[native code]')) {
|
||||
return 'Failed Date.prototype.setDate.toString()';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkIntlConstructor = () => {
|
||||
if (
|
||||
!Object.getPrototypeOf(Intl.DateTimeFormat.prototype)
|
||||
.constructor.toString()
|
||||
.includes('Object')
|
||||
) {
|
||||
return 'Failed Object.getPrototypeOf(Intl.DateTimeFormat.prototype).constructor.toString()';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkIntlPrototype = () => {
|
||||
if (
|
||||
!Intl.DateTimeFormat.prototype.resolvedOptions
|
||||
.toString()
|
||||
.includes('[native code]')
|
||||
) {
|
||||
return 'Failed Intl.DateTimeFormat.prototype.resolvedOptions.toString()';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkNavigatorProperties = (key) => {
|
||||
if (Object.getOwnPropertyDescriptor(navigator, key) !== undefined) {
|
||||
return 'Failed Object.getOwnPropertyDescriptor(navigator, key)';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkNavigatorPrototype = (key) => {
|
||||
try {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const check = Navigator.prototype[key];
|
||||
return 'Failed Navigator.prototype[key]';
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getNavigatorValue = (type) =>
|
||||
[checkNavigatorProperties(type), checkNavigatorPrototype(type)].filter(
|
||||
Boolean
|
||||
);
|
||||
|
||||
const data = {
|
||||
locale: {
|
||||
value: Intl.DateTimeFormat().resolvedOptions().locale,
|
||||
issues: [checkIntlPrototype(), checkIntlConstructor()].filter(Boolean),
|
||||
},
|
||||
timeZone: {
|
||||
value: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
issues: [checkIntlPrototype(), checkIntlConstructor()].filter(Boolean),
|
||||
},
|
||||
timezoneOffset: {
|
||||
value: new Date().getTimezoneOffset(),
|
||||
issues: [checkDatePrototype()].filter(Boolean),
|
||||
},
|
||||
dateString: {
|
||||
value: new Date().toString(),
|
||||
issues: [checkDatePrototype()].filter(Boolean),
|
||||
},
|
||||
dateLocale: {
|
||||
value: new Date().toLocaleString(),
|
||||
issues: [checkDatePrototype()].filter(Boolean),
|
||||
},
|
||||
userAgent: {
|
||||
value: navigator.userAgent,
|
||||
issues: getNavigatorValue('userAgent'),
|
||||
},
|
||||
appVersion: {
|
||||
value: navigator.appVersion,
|
||||
issues: getNavigatorValue('appVersion'),
|
||||
},
|
||||
};
|
||||
|
||||
postMessage(data);
|
||||
92
src/components/App.css
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
:root {
|
||||
--main: #943ec5;
|
||||
--text: rgb(0, 0, 0, 0.8);
|
||||
--border: #ddd;
|
||||
--grey: #c4c4c4;
|
||||
--lightGrey: #f8f8f8;
|
||||
--issueBackground: #f8d7da;
|
||||
--issueText: #721c24;
|
||||
--fill: #9fa6b2;
|
||||
--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;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
b {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tableWrap {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
tr:not(:last-child) {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 105px;
|
||||
font-weight: 600;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
td:nth-child(3) {
|
||||
width: 40px;
|
||||
font-weight: 600;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
body {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
25
src/components/App.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/** @jsxImportSource theme-ui */
|
||||
|
||||
import { ThemeProvider } from 'theme-ui';
|
||||
import theme from './theme.ts';
|
||||
import './App.css';
|
||||
import MainColumn from './MainColumn';
|
||||
|
||||
const App = () => (
|
||||
<ThemeProvider theme={theme}>
|
||||
<div sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||
<div
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'var(--main)',
|
||||
zIndex: '-1',
|
||||
}}
|
||||
/>
|
||||
<MainColumn />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export default App;
|
||||
21
src/components/Block.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/** @jsxImportSource theme-ui */
|
||||
|
||||
const ContentBlock = ({ children }) => (
|
||||
<div
|
||||
sx={{
|
||||
color: 'var(--text)',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '4px',
|
||||
boxSizing: 'border-box',
|
||||
padding: ['18px', '20px', '20px'],
|
||||
boxShadow: 'rgba(0, 0, 0, 0.1) 0px 4px 12px',
|
||||
mb: ['12px', '24px', '24px'],
|
||||
width: ['100%', 'auto', 'auto'],
|
||||
minWidth: ['0', '500px', '500px'],
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ContentBlock;
|
||||
111
src/components/Blocks.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/** @jsxImportSource theme-ui */
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import DataContext from './Context';
|
||||
import InfoBlock from './InfoBlock';
|
||||
import DataBlock from './DataBlock';
|
||||
import GeolocationBlock from './GeolocationBlock';
|
||||
import windowData from '../utils/data';
|
||||
|
||||
const getWebWorker = () => {
|
||||
let w;
|
||||
if (typeof w === 'undefined') {
|
||||
w = new Worker('/worker.js');
|
||||
}
|
||||
return w;
|
||||
};
|
||||
|
||||
const Blocks = () => {
|
||||
const [workerData, setWorkerData] = useState();
|
||||
const [frameData, setFrameData] = useState();
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const initialData = initialDataObj;
|
||||
|
||||
useEffect(() => {
|
||||
const frame = document.createElement('iframe');
|
||||
document.body.appendChild(frame);
|
||||
frame.style.display = 'none';
|
||||
frame.src = '/frame.html';
|
||||
const receiveMessage = (event) => {
|
||||
if (event.data.type === 'frameData') {
|
||||
setFrameData(event.data.data);
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', receiveMessage, false);
|
||||
|
||||
if (window.Worker.length) {
|
||||
getWebWorker().onmessage = (event) => {
|
||||
setWorkerData(event.data);
|
||||
};
|
||||
} else {
|
||||
setWorkerData(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataContext.Provider
|
||||
value={{
|
||||
initialData,
|
||||
windowData,
|
||||
frameData,
|
||||
workerData,
|
||||
}}
|
||||
>
|
||||
<div sx={{ display: ['none', 'none', 'block'], maxWidth: '500px' }}>
|
||||
<InfoBlock />
|
||||
<DataBlock
|
||||
title="Intl.DateTimeFormat().resolvedOptions().timeZone"
|
||||
type="timeZone"
|
||||
/>
|
||||
|
||||
<DataBlock title="navigator.userAgent" type="userAgent" />
|
||||
<DataBlock title="navigator.appVersion" type="appVersion" />
|
||||
</div>
|
||||
<div sx={{ display: ['none', 'none', 'block'], maxWidth: '500px' }}>
|
||||
<DataBlock
|
||||
title="Intl.DateTimeFormat().resolvedOptions().locale"
|
||||
type="locale"
|
||||
/>
|
||||
<DataBlock
|
||||
title="new Date().getTimezoneOffset()"
|
||||
type="timezoneOffset"
|
||||
/>
|
||||
<DataBlock title="new Date().toLocaleString()" type="dateLocale" />
|
||||
<DataBlock title="new Date().toString()" type="dateString" />
|
||||
<GeolocationBlock />
|
||||
</div>
|
||||
<div
|
||||
sx={{
|
||||
display: ['block', 'block', 'none'],
|
||||
maxWidth: '500px',
|
||||
margin: '0 12px',
|
||||
}}
|
||||
>
|
||||
<InfoBlock />
|
||||
<DataBlock
|
||||
title="Intl.DateTimeFormat().resolvedOptions().timeZone"
|
||||
type="timeZone"
|
||||
/>
|
||||
<DataBlock
|
||||
title="Intl.DateTimeFormat().resolvedOptions().locale"
|
||||
type="locale"
|
||||
/>
|
||||
<DataBlock title="new Date().toString()" type="dateString" />
|
||||
<DataBlock title="new Date().toLocaleString()" type="dateLocale" />
|
||||
<DataBlock
|
||||
title="new Date().getTimezoneOffset()"
|
||||
type="timezoneOffset"
|
||||
/>
|
||||
<DataBlock title="navigator.userAgent" type="userAgent" />
|
||||
<DataBlock title="navigator.appVersion" type="appVersion" />
|
||||
|
||||
<GeolocationBlock />
|
||||
</div>
|
||||
</DataContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Blocks;
|
||||
55
src/components/DataBlock.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { useContext } from 'react';
|
||||
import DataContext from './Context';
|
||||
import Block from './Block';
|
||||
import TableRow from './TableRow';
|
||||
|
||||
const DataBlock = ({ title, type }) => {
|
||||
const { initialData, windowData, frameData, workerData } =
|
||||
useContext(DataContext);
|
||||
|
||||
const isWorkerValid = workerData && window.Worker.length;
|
||||
|
||||
const getWorkerValue = () => {
|
||||
if (isWorkerValid) return workerData[type].value;
|
||||
if (!window.Worker.length) return 'null';
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Block>
|
||||
<h1>{title}</h1>
|
||||
<div className="tableWrap">
|
||||
<table>
|
||||
<tbody>
|
||||
<TableRow
|
||||
title="Top Window"
|
||||
value={windowData ? windowData[type].value : ''}
|
||||
issues={windowData ? windowData[type].issues : []}
|
||||
/>
|
||||
<TableRow
|
||||
title="Initial Load"
|
||||
value={initialData ? initialData[type].value : ''}
|
||||
issues={initialData ? initialData[type].issues : []}
|
||||
/>
|
||||
<TableRow
|
||||
title="Frame"
|
||||
value={frameData ? frameData[type].value : ''}
|
||||
issues={frameData ? frameData[type].issues : []}
|
||||
/>
|
||||
<TableRow
|
||||
title="Web worker"
|
||||
value={getWorkerValue()}
|
||||
issues={
|
||||
isWorkerValid
|
||||
? workerData[type].issues
|
||||
: ['Web workers blocked']
|
||||
}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataBlock;
|
||||
79
src/components/GeolocationBlock.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/** @jsxImportSource theme-ui */
|
||||
|
||||
import { useState } from 'react';
|
||||
import Block from './Block';
|
||||
import getGeolocation from '../utils/geolocation';
|
||||
import TableRow from './TableRow';
|
||||
|
||||
const GeolocationBlock = () => {
|
||||
const [geolocationData, setGeolocationData] = useState();
|
||||
const [buttonValue, setButtonValue] = useState('Allow Geolocation API');
|
||||
return (
|
||||
<Block>
|
||||
<h1>HTML Geolocation API</h1>
|
||||
{geolocationData ? (
|
||||
<>
|
||||
{typeof geolocationData === 'string' ? (
|
||||
<div
|
||||
sx={{
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '4px',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
<div sx={{ textAlign: 'center', fontWeight: '600' }}>
|
||||
{`${geolocationData}`}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="tableWrap">
|
||||
<table>
|
||||
<tbody>
|
||||
{geolocationData.map((item) => (
|
||||
<TableRow
|
||||
key={item.key}
|
||||
title={item.key}
|
||||
value={item.value}
|
||||
issues={item.issues}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<input
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
getGeolocation(setGeolocationData);
|
||||
setButtonValue('Loading...');
|
||||
}}
|
||||
sx={{
|
||||
display: 'block',
|
||||
backgroundColor: 'var(--main)',
|
||||
color: '#fff',
|
||||
borderRadius: '4px',
|
||||
boxSizing: 'border-box',
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
height: '46px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
WebkitAppearance: 'none',
|
||||
':hover': {
|
||||
opacity: '0.7',
|
||||
},
|
||||
}}
|
||||
value={buttonValue}
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeolocationBlock;
|
||||
63
src/components/HeaderBar.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/** @jsxImportSource theme-ui */
|
||||
|
||||
import { ReactComponent as LogoImg } from '../images/logo.svg';
|
||||
import HeaderButton from './HeaderButton';
|
||||
import chromeImage from '../images/chrome.png';
|
||||
import githubImage from '../images/github.png';
|
||||
|
||||
const HeaderBar = () => (
|
||||
<div
|
||||
sx={{
|
||||
display: ['block', 'block', 'flex'],
|
||||
alignItems: 'center',
|
||||
justifyContent: ['center', 'center', 'space-between'],
|
||||
width: ['500px', '500px', '1024px'],
|
||||
margin: ['12px', '18px', '18px'],
|
||||
}}
|
||||
>
|
||||
<div
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
mb: ['12px', '18px', '0'],
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href="."
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '206px',
|
||||
height: '50px',
|
||||
}}
|
||||
alt="Vytal logo"
|
||||
>
|
||||
<LogoImg />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: ['36px', '50px', '50px'],
|
||||
gap: ['12px', '24px', '24px'],
|
||||
}}
|
||||
>
|
||||
<HeaderButton
|
||||
url="https://chrome.google.com/webstore/detail/vytal/ncbknoohfjmcfneopnfkapmkblaenokb"
|
||||
image={chromeImage}
|
||||
text="Download Extension"
|
||||
alt="Chrome logo"
|
||||
/>
|
||||
<HeaderButton
|
||||
url="https://github.com/z0ccc/Vytal"
|
||||
image={githubImage}
|
||||
text="Source Code"
|
||||
alt="Github logo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default HeaderBar;
|
||||
38
src/components/HeaderButton.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/** @jsxImportSource theme-ui */
|
||||
|
||||
const HeaderButton = ({ url, image, text, alt }) => (
|
||||
<a
|
||||
href={url}
|
||||
className="headerButton"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#fff',
|
||||
height: ['36px', '50px', '50px'],
|
||||
borderRadius: '4px',
|
||||
px: ['10px', '18px', '18px'],
|
||||
boxShadow: 'rgb(0 0 0 / 10%) 0px 4px 12px',
|
||||
fontSize: ['13px', '15px', '15px'],
|
||||
textDecoration: 'none',
|
||||
color: '#000',
|
||||
':hover': {
|
||||
backgroundColor: 'white90',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={alt}
|
||||
sx={{
|
||||
marginRight: ['5px', '8px', '8px'],
|
||||
height: ['22px', '28px', '28px'],
|
||||
width: ['22px', '28px', '28px'],
|
||||
}}
|
||||
/>
|
||||
{text}
|
||||
</a>
|
||||
);
|
||||
|
||||
export default HeaderButton;
|
||||
13
src/components/InfoBlock.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import Block from './Block';
|
||||
|
||||
const InfoBlock = () => (
|
||||
<Block>
|
||||
Vytal is a browser extension that utilizes the chrome.debugger API to mock
|
||||
device data that could otherwise reveal information about you. This website
|
||||
scans your browser for such data. A red x signifies that the scanner has
|
||||
detected tampered data. A green check means that no tampering has been
|
||||
detected.
|
||||
</Block>
|
||||
);
|
||||
|
||||
export default InfoBlock;
|
||||
22
src/components/MainColumn.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/** @jsxImportSource theme-ui */
|
||||
|
||||
import HeaderBar from './HeaderBar';
|
||||
import Blocks from './Blocks';
|
||||
|
||||
const MainColumn = () => (
|
||||
<div
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<HeaderBar />
|
||||
<div sx={{ display: 'flex', justifyContent: 'center', gap: '24px' }}>
|
||||
<Blocks />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default MainColumn;
|
||||
106
src/components/TableRow.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/** @jsxImportSource theme-ui */
|
||||
|
||||
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: '4px',
|
||||
},
|
||||
};
|
||||
|
||||
Modal.setAppElement('#root');
|
||||
|
||||
const TableRow = ({ title, value, issues }) => {
|
||||
const issuesExist = issues.length !== 0;
|
||||
const [modalIsOpen, setIsOpen] = useState(false);
|
||||
const dataValue = !value && value !== 0 ? 'null' : value;
|
||||
|
||||
const openModal = () => {
|
||||
if (issuesExist) setIsOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
sx={
|
||||
issuesExist
|
||||
? {
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
backgroundColor: 'var(--issueBackground)',
|
||||
color: 'var(--issueText)',
|
||||
},
|
||||
}
|
||||
: null
|
||||
}
|
||||
onClick={openModal}
|
||||
>
|
||||
<td>{title}</td>
|
||||
{value ? (
|
||||
<>
|
||||
<td>{Array.isArray(value) ? JSON.stringify(value) : dataValue}</td>
|
||||
<td>
|
||||
{issuesExist ? (
|
||||
<XCircle sx={{ display: 'flex', width: '20px' }} />
|
||||
) : (
|
||||
<CheckCircle sx={{ display: 'flex', width: '20px' }} />
|
||||
)}
|
||||
</td>
|
||||
</>
|
||||
) : null}
|
||||
</tr>
|
||||
<Modal
|
||||
isOpen={modalIsOpen}
|
||||
onRequestClose={closeModal}
|
||||
style={modalStyles}
|
||||
contentLabel="Issues Modal"
|
||||
>
|
||||
<div
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
margin: '0 0 6px 0',
|
||||
height: '20px',
|
||||
}}
|
||||
>
|
||||
<div sx={{ fontWeight: '600' }}>{title} issues</div>
|
||||
<X
|
||||
sx={{
|
||||
fill: 'var(--border)',
|
||||
display: 'flex',
|
||||
width: '12px',
|
||||
cursor: 'pointer',
|
||||
margin: '0 0 0 12px',
|
||||
':hover': {
|
||||
fill: 'var(--grey)',
|
||||
},
|
||||
}}
|
||||
onClick={closeModal}
|
||||
/>
|
||||
</div>
|
||||
<ul>
|
||||
{issues.filter(Boolean).map((ele, index) => (
|
||||
<li key={index}>{ele}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableRow;
|
||||
10
src/components/theme.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { Theme } from 'theme-ui';
|
||||
|
||||
const theme: Theme = {
|
||||
breakpoints: ['575px', '1060px'],
|
||||
colors: {
|
||||
white90: 'rgb(255, 255, 255, 0.90)',
|
||||
},
|
||||
};
|
||||
|
||||
export default theme;
|
||||
|
Before Width: | Height: | Size: 470 B After Width: | Height: | Size: 470 B |
BIN
src/images/chrome.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
src/images/github.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
|
|
@ -3,6 +3,7 @@
|
|||
preserveAspectRatio="xMidYMid meet"
|
||||
version="1"
|
||||
viewBox="0 0 4389 1056"
|
||||
style="display: block"
|
||||
>
|
||||
<g fill="#fff" stroke="none" transform="matrix(.1 0 0 -.1 0 1056)">
|
||||
<path d="M4890 10554c-19-2-102-11-185-20-814-84-1627-377-2332-841-666-437-1242-1047-1652-1748-345-590-582-1279-670-1950-34-261-44-422-44-710 0-388 28-679 99-1035C523 2160 2153 526 4240 106c360-73 640-99 1040-99 488 0 847 44 1305 160 665 168 1310 477 1860 891 1004 754 1703 1833 1983 3062 146 636 168 1351 61 2010-273 1699-1362 3160-2913 3911-540 261-1091 420-1716 495-100 12-240 17-535 19-220 2-416 1-435-1zm870-978c947-115 1772-494 2460-1130 303-281 615-672 815-1025 491-866 669-1875 504-2861-113-681-390-1322-814-1885-233-310-552-627-869-862-1529-1139-3637-1136-5161 7-391 294-746 669-1012 1070-367 553-599 1170-683 1814-113 869 22 1705 399 2472 103 210 151 294 287 499 316 476 726 886 1199 1199 340 225 629 368 999 495 349 120 681 187 1106 225 112 10 641-3 770-18z"></path>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 468 B After Width: | Height: | Size: 468 B |
|
Before Width: | Height: | Size: 489 B After Width: | Height: | Size: 489 B |
16
src/index.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './components/App';
|
||||
// import reportWebVitals from './reportWebVitals';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
// reportWebVitals();
|
||||