mirror of
https://github.com/niespodd/browser-fingerprinting.git
synced 2021-11-01 22:44:07 +03:00
Add tester v1
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.idea/
|
||||
1
public/assets/index.250ee517.js
Normal file
1
public/assets/index.250ee517.js
Normal file
File diff suppressed because one or more lines are too long
1
public/assets/index.f44316bf.css
Normal file
1
public/assets/index.f44316bf.css
Normal file
@@ -0,0 +1 @@
|
||||
.__json-pretty__{line-height:1.3;color:#66d9ef;background:#272822;overflow:auto}.__json-pretty__ .__json-key__{color:#f92672}.__json-pretty__ .__json-value__{color:#a6e22e}.__json-pretty__ .__json-string__{color:#fd971f}.__json-pretty__ .__json-boolean__{color:#ac81fe}.__json-pretty-error__{line-height:1.3;color:#66d9ef;background:#272822;overflow:auto}
|
||||
77
public/assets/vendor.4ca41b87.js
Normal file
77
public/assets/vendor.4ca41b87.js
Normal file
File diff suppressed because one or more lines are too long
22
public/index.html
Normal file
22
public/index.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>tester at niespodd/browser-fingerprinting</title>
|
||||
<style>
|
||||
.bia {
|
||||
-webkit-column-break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/index.250ee517.js"></script>
|
||||
<link rel="modulepreload" href="/assets/vendor.4ca41b87.js">
|
||||
<link rel="stylesheet" href="/assets/index.f44316bf.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
5
tester/.gitignore
vendored
Normal file
5
tester/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
19
tester/index.html
Normal file
19
tester/index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>tester at niespodd/browser-fingerprinting</title>
|
||||
<style>
|
||||
.bia {
|
||||
-webkit-column-break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
break-inside: avoid;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
33
tester/package.json
Normal file
33
tester/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^1.6.3",
|
||||
"@emotion/react": "11",
|
||||
"@emotion/styled": "11",
|
||||
"@react-three/fiber": "^6.2.3",
|
||||
"chart.js": "^3.3.2",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"framer-motion": "4",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"react": "^17.0.0",
|
||||
"react-chartjs-2": "^3.0.3",
|
||||
"react-dom": "^17.0.0",
|
||||
"react-flagkit": "^2.0.4",
|
||||
"react-json-pretty": "^2.2.0",
|
||||
"react-redux": "^7.2.4",
|
||||
"redux": "^4.1.0",
|
||||
"redux-actions": "^2.6.5",
|
||||
"sass": "^1.34.1",
|
||||
"shaka-player-react": "^1.1.2",
|
||||
"three": "^0.129.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react-refresh": "^1.3.1",
|
||||
"vite": "^2.3.5"
|
||||
}
|
||||
}
|
||||
86
tester/src/App.jsx
Normal file
86
tester/src/App.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from "react";
|
||||
import {ChakraProvider, Box, Container, Divider, Text, Link, Alert} from "@chakra-ui/react";
|
||||
import {Provider, useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import Header from "./Header";
|
||||
import store from "./state/store";
|
||||
import {persistedReset, persistedSet} from "./state/actions";
|
||||
|
||||
import BasicInformation from "./testers/BasicInformation";
|
||||
import ChromeExtensions from "./testers/ChromeExtensions";
|
||||
import DocumentStatus from "./testers/DocumentStatus";
|
||||
import FeaturePolicy from "./testers/FeaturePolicy";
|
||||
import SpeechSynthesis from "./testers/SpeechSynthesis";
|
||||
import DeviceSensors from "./testers/DeviceSensors";
|
||||
import base64 from "./utils/base64";
|
||||
import MediaDevices from "./testers/MediaDevices";
|
||||
import EncryptedMediaExtensions from "./testers/EncryptedMediaExtensions";
|
||||
|
||||
const AppPersisted = () => {
|
||||
const dispatch = useDispatch();
|
||||
const hasPersisted = useSelector((state) => state.persisted);
|
||||
React.useEffect(() => {
|
||||
const hashStatus = window.location.hash.replace("#", "");
|
||||
if (hashStatus.length > 0) {
|
||||
dispatch(persistedSet(JSON.parse(base64.decode(decodeURIComponent(hashStatus)))));
|
||||
} else {
|
||||
dispatch(persistedReset());
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (hasPersisted) {
|
||||
return (
|
||||
<Container maxW="container.xl" mt={4}>
|
||||
<Alert status="info" variant="left-accent" size="sm" fontSize="sm">
|
||||
You are viewing a saved snapshot.
|
||||
<Link href={window.location.href.split("#")[0]} color="teal.600" ml={2}>Click here to run a new test.</Link>
|
||||
</Alert>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ChakraProvider>
|
||||
<Box bg="gray.800">
|
||||
<Container maxW="container.xl" py={2}>
|
||||
<Text fontSize="sm" color="gray.100">
|
||||
Read more about browser fingerprinting ➜ <Link color="teal.500" href="https://github.com/niespodd/browser-fingerprinting">https://github.com/niespodd/browser-fingerprinting</Link>
|
||||
</Text>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
<AppPersisted />
|
||||
|
||||
<Container maxW="container.xl">
|
||||
<Box py={4}>
|
||||
<Header />
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
<Divider mb={6} />
|
||||
|
||||
<Container maxW="container.xl">
|
||||
<Box w="100%" sx={{ columnCount: window.outerWidth > 500 ? 2 : 1, columnGap: "24px" }}>
|
||||
<EncryptedMediaExtensions />
|
||||
<BasicInformation />
|
||||
<MediaDevices />
|
||||
|
||||
<DeviceSensors />
|
||||
<ChromeExtensions />
|
||||
<DocumentStatus />
|
||||
<FeaturePolicy />
|
||||
<SpeechSynthesis />
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
<Divider my={6} />
|
||||
</ChakraProvider>
|
||||
</Provider>
|
||||
)
|
||||
};
|
||||
|
||||
export default App;
|
||||
121
tester/src/Header.jsx
Normal file
121
tester/src/Header.jsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from "react";
|
||||
import copy from 'copy-to-clipboard';
|
||||
import {HStack, Stack, Button, Box, Input, InputGroup, InputRightElement, Text, Modal, ModalOverlay, ModalBody, ModalContent, ModalHeader, ModalCloseButton, Link} from "@chakra-ui/react";
|
||||
import 'react-json-pretty/themes/monikai.css';
|
||||
import JSONPretty from 'react-json-pretty';
|
||||
import {useSelector} from "react-redux";
|
||||
|
||||
import md5 from "./utils/md5";
|
||||
import b64 from "./utils/base64";
|
||||
|
||||
const FingerprintInput = () => {
|
||||
const status = useSelector((state) => state.status);
|
||||
const value = React.useMemo(() => md5(JSON.stringify(status)), [status]);
|
||||
const handleCopy = React.useCallback(() => {
|
||||
copy(value);
|
||||
alert("Fingerprint hash copied to clipboard.")
|
||||
});
|
||||
return (
|
||||
<InputGroup size="md">
|
||||
<Input
|
||||
pr="4.5rem"
|
||||
type="text"
|
||||
value={value}
|
||||
id="fp"
|
||||
readOnly
|
||||
/>
|
||||
<InputRightElement width="4.5rem">
|
||||
<Button h="1.75rem" size="sm" onClick={handleCopy}>
|
||||
Copy
|
||||
</Button>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
)
|
||||
};
|
||||
|
||||
const RawModal = ({ isOpen, onClose }) => {
|
||||
const status = useSelector((state) => state.status);
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Inspect RAW report values</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Box borderRadius={4} style={{ overflow: 'hidden' }}>
|
||||
<JSONPretty json={status} mainStyle="padding:1em; font-size: 12px" />
|
||||
</Box>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const ShareModal = ({ isOpen, onClose }) => {
|
||||
const status = useSelector((state) => state.status);
|
||||
const shareLink = React.useMemo(() => {
|
||||
const b64status = b64.encode(JSON.stringify(status));
|
||||
return `${window.location.href.split("#")[0]}#${encodeURIComponent(b64status)}`;
|
||||
}, [status]);
|
||||
const handleCopy = React.useCallback(() => {
|
||||
copy(shareLink);
|
||||
alert("Copied to clipboard");
|
||||
}, []);
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Share current report</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Stack>
|
||||
<Text>
|
||||
Use the generated link to share current report:
|
||||
</Text>
|
||||
<InputGroup size="md">
|
||||
<Input
|
||||
type="text"
|
||||
value={shareLink}
|
||||
id="link"
|
||||
/>
|
||||
</InputGroup>
|
||||
<Link href="javascript:void(0)" fontSize="xs" color="teal.500" style={{ display: 'inline-block', textAlign: 'right' }} onClick={handleCopy}>
|
||||
Copy link to clipboard
|
||||
</Link>
|
||||
<Text fontSize="sm" mb={4} pb={4}>
|
||||
Please note that the report may contain some of your device data which may be in some cases
|
||||
sensitive.
|
||||
</Text>
|
||||
</Stack>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const [rawModalOpened, setRawModalOpened] = React.useState(false);
|
||||
const [shareModalOpened, setShareModalOpened] = React.useState(false);
|
||||
return (
|
||||
<>
|
||||
<Stack align="stretch">
|
||||
<HStack spacing={6} w="100%">
|
||||
<Box w="50%">
|
||||
<FingerprintInput />
|
||||
</Box>
|
||||
|
||||
<Button onClick={() => setRawModalOpened(true)} size="sm" ml="auto">
|
||||
Inspect
|
||||
</Button>
|
||||
|
||||
<Button onClick={() => setShareModalOpened(true)} colorScheme="blue" size="sm" ml="auto">
|
||||
Share
|
||||
</Button>
|
||||
</HStack>
|
||||
</Stack>
|
||||
|
||||
<RawModal isOpen={rawModalOpened} onClose={() => setRawModalOpened(false)} />
|
||||
<ShareModal isOpen={shareModalOpened} onClose={() => setShareModalOpened(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
15
tester/src/favicon.svg
Normal file
15
tester/src/favicon.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#41D1FF"/>
|
||||
<stop offset="1" stop-color="#BD34FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFEA83"/>
|
||||
<stop offset="0.0833333" stop-color="#FFDD35"/>
|
||||
<stop offset="1" stop-color="#FFA800"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
0
tester/src/index.scss
Normal file
0
tester/src/index.scss
Normal file
11
tester/src/main.jsx
Normal file
11
tester/src/main.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import './index.scss'
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
18
tester/src/state/actions.js
Normal file
18
tester/src/state/actions.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import {createActions} from "redux-actions";
|
||||
|
||||
export const {
|
||||
statusSet,
|
||||
statusReset
|
||||
} = createActions({
|
||||
STATUS_SET: (key, value) => ({ key, value }),
|
||||
STATUS_RESET: (key) => ({ key })
|
||||
});
|
||||
|
||||
export const {
|
||||
persistedSet,
|
||||
persistedReset
|
||||
} = createActions({
|
||||
PERSISTED_SET: (status) => ({ status }),
|
||||
PERSISTED_RESET: () => null,
|
||||
});
|
||||
|
||||
18
tester/src/state/reducers.js
Normal file
18
tester/src/state/reducers.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import update from "immutability-helper";
|
||||
import {handleActions} from "redux-actions";
|
||||
import {persistedReset, persistedSet, statusReset, statusSet} from "./actions";
|
||||
|
||||
export const statusReducer = handleActions({
|
||||
[statusSet.toString()]: (state, { payload: { key, value }}) => update(state, {
|
||||
[key]: { $set: value }
|
||||
}),
|
||||
[statusReset.toString()]: (state, { payload: { key }}) => update(state, {
|
||||
[key]: { $set: undefined }
|
||||
}),
|
||||
[persistedSet.toString()]: (state, { payload: { status }}) => update(state, { $set: status })
|
||||
}, {});
|
||||
|
||||
export const persistedReducer = handleActions({
|
||||
[persistedReset.toString()]: (state) => update(state, { $set: false }),
|
||||
[persistedSet.toString()]: (state) => update(state, { $set: true })
|
||||
}, true);
|
||||
12
tester/src/state/store.js
Normal file
12
tester/src/state/store.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import {combineReducers, createStore} from "redux";
|
||||
|
||||
import { statusReducer, persistedReducer } from "./reducers";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
status: statusReducer,
|
||||
persisted: persistedReducer
|
||||
});
|
||||
|
||||
const store = createStore(rootReducer, {});
|
||||
|
||||
export default store;
|
||||
109
tester/src/testers/BasicInformation.jsx
Normal file
109
tester/src/testers/BasicInformation.jsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import tester from "./tester";
|
||||
import React from "react";
|
||||
import {Code, Divider, Text, Link, HStack, Box, SimpleGrid, GridItem} from "@chakra-ui/react";
|
||||
import {DictToTable} from "./utils";
|
||||
|
||||
const devToolsOpened = () => {
|
||||
// based on: https://github.com/sindresorhus/devtools-detect/blob/main/index.js
|
||||
let devtools = {};
|
||||
const threshold = 160;
|
||||
const widthThreshold = window.outerWidth - window.innerWidth > threshold;
|
||||
const heightThreshold = window.outerHeight - window.innerHeight > threshold;
|
||||
const orientation = widthThreshold ? 'vertical' : 'horizontal';
|
||||
if (
|
||||
!(heightThreshold && widthThreshold) &&
|
||||
((window.Firebug && window.Firebug.chrome && window.Firebug.chrome.isInitialized) || widthThreshold || heightThreshold)
|
||||
) {
|
||||
devtools.isOpen = true;
|
||||
devtools.orientation = orientation;
|
||||
} else {
|
||||
devtools.isOpen = false;
|
||||
devtools.orientation = undefined;
|
||||
}
|
||||
return devtools;
|
||||
}
|
||||
|
||||
const probeStackLimit = async () => {
|
||||
let accessor = 'window.parent';
|
||||
let p = 0;
|
||||
while (true) {
|
||||
p += 50;
|
||||
try {
|
||||
eval(accessor);
|
||||
} catch (err) {
|
||||
break;
|
||||
}
|
||||
for (let i=0; i<50; i++) {
|
||||
accessor += '.parent';
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50)); // helps to prevent early freeze/crash
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
const BasicInformation = ({ fn, value }) => {
|
||||
fn(async () => {
|
||||
const devtools = devToolsOpened();
|
||||
const stackLimit = await probeStackLimit();
|
||||
return {
|
||||
"deviceMemory": navigator.deviceMemory,
|
||||
"hardwareConcurrency": navigator.hardwareConcurrency,
|
||||
"window.innerHeight": window.innerHeight,
|
||||
"window.innerWidth": window.innerWidth,
|
||||
"window.outerHeight": window.outerHeight,
|
||||
"window.outerWidth": window.outerWidth,
|
||||
"devTools.isOpen": devtools.isOpen,
|
||||
"devTools.orientation": devtools.orientation,
|
||||
"stackLimit": stackLimit,
|
||||
};
|
||||
});
|
||||
|
||||
if (!value) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SimpleGrid minChildWidth="265px" gap={8}>
|
||||
<GridItem>
|
||||
<Box mb={4}>
|
||||
<Text fontSize="sm" mb={2}>
|
||||
Available hardware details:
|
||||
</Text>
|
||||
<DictToTable dict={value} limitKeys={["deviceMemory", "hardwareConcurrency", "stackLimit"]} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Text>
|
||||
<Code>devTools</Code> opened? {value['devTools.isOpen'] ? "✔️" : "❌"} {value['devTools.orientation'] && `(${value['devTools.orientation']})`}
|
||||
</Text>
|
||||
</Box>
|
||||
</GridItem>
|
||||
<GridItem>
|
||||
<Text fontSize="sm" mb={2}>
|
||||
Window dimensions:
|
||||
</Text>
|
||||
<DictToTable dict={value} limitKeys={[
|
||||
"window.innerWidth",
|
||||
"window.innerHeight",
|
||||
"window.outerHeight",
|
||||
"window.outerWidth",
|
||||
]} />
|
||||
|
||||
</GridItem>
|
||||
</SimpleGrid>
|
||||
|
||||
<Divider my={4} />
|
||||
<Text fontSize="xs">
|
||||
DevTools information are based on window sizing (borrowed from <Link color="teal.500" href="https://github.com/sindresorhus/devtools-detect/blob/main/index.js">sindresorhus/devtools-detect</Link>)
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default tester(BasicInformation, {
|
||||
key: 'basic',
|
||||
title: "Basic Information",
|
||||
explainer: (
|
||||
<>
|
||||
Basic properties from global JS scope (e.g. <Code>window.navigator</Code>).
|
||||
</>
|
||||
)
|
||||
});
|
||||
62
tester/src/testers/ChromeExtensions.jsx
Normal file
62
tester/src/testers/ChromeExtensions.jsx
Normal file
File diff suppressed because one or more lines are too long
223
tester/src/testers/DeviceSensors.jsx
Normal file
223
tester/src/testers/DeviceSensors.jsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import { Canvas, useFrame } from "@react-three/fiber";
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import React from "react";
|
||||
import {
|
||||
Text,
|
||||
Divider,
|
||||
HStack,
|
||||
Link,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent
|
||||
} from "@chakra-ui/react";
|
||||
import tester from "./tester";
|
||||
import {YesNoList} from "./utils";
|
||||
|
||||
const CHART_RECORD_LENGTH = 15; // recent 30 readings
|
||||
const CHART_LABELS = [];
|
||||
for (let i=CHART_RECORD_LENGTH; i>0; i--) {
|
||||
CHART_LABELS.push(`-${i.toString()}`);
|
||||
}
|
||||
|
||||
const useTsMemo = (input) => {
|
||||
const [ts, setTs] = React.useState([]);
|
||||
React.useEffect(() => {
|
||||
if (!input) return;
|
||||
if (ts.length < CHART_RECORD_LENGTH) {
|
||||
setTs([...ts, input]);
|
||||
} else {
|
||||
setTs([...ts.slice(1, ts.length - 1), input]);
|
||||
}
|
||||
}, [input]);
|
||||
return ts;
|
||||
}
|
||||
|
||||
const DeviceSensors = ({ fn, value }) => {
|
||||
const deviceMesh = React.useRef();
|
||||
const [orientation, setOrientation] = React.useState(undefined);
|
||||
const [acceleration, setAcceleration] = React.useState(undefined);
|
||||
const [chartModalOpen, setChartModalOpen] = React.useState(false);
|
||||
|
||||
const orientationTs = useTsMemo(orientation);
|
||||
const accelerationTs = useTsMemo(acceleration);
|
||||
|
||||
const sensorChartData = React.useMemo(() => {
|
||||
return [{
|
||||
labels: CHART_LABELS,
|
||||
datasets: [
|
||||
{ label: "A0", data: accelerationTs.map((a) => a[0]), fill: false, backgroundColor: '#1d3557', borderColor: '#1d3557',},
|
||||
{ label: "A1", data: accelerationTs.map((a) => a[1]), fill: false, backgroundColor: '#2a9d8f', borderColor: '#2a9d8f',},
|
||||
{ label: "A2", data: accelerationTs.map((a) => a[2]), fill: false, backgroundColor: '#e9c46a', borderColor: '#e9c46a',},
|
||||
{ label: "A3", data: accelerationTs.map((a) => a[3]), fill: false, backgroundColor: '#e76f51', borderColor: '#e76f51',},
|
||||
],
|
||||
}, {
|
||||
labels: CHART_LABELS,
|
||||
datasets: [
|
||||
{ label: "O0", data: orientationTs.map((a) => a[0]), fill: false, backgroundColor: '#e63946', borderColor: '#e63946',},
|
||||
{ label: "O1", data: orientationTs.map((a) => a[1]), fill: false, backgroundColor: '#457b9d', borderColor: '#457b9d',},
|
||||
{ label: "O2", data: orientationTs.map((a) => a[2]), fill: false, backgroundColor: '#0096c7', borderColor: '#0096c7',},
|
||||
{ label: "O3", data: orientationTs.map((a) => a[3]), fill: false, backgroundColor: '#aaa', borderColor: '#aaa',},
|
||||
],
|
||||
}];
|
||||
}, [orientationTs, accelerationTs]);
|
||||
|
||||
React.useEffect(() => {
|
||||
try {
|
||||
const options = { frequency: 60, referenceFrame: 'device' };
|
||||
const aSensor = new LinearAccelerationSensor({frequency: 60});
|
||||
const oSensor = new AbsoluteOrientationSensor(options);
|
||||
oSensor.onreading = (e) => {
|
||||
const { quaternion } = e.currentTarget;
|
||||
deviceMesh.current.quaternion.fromArray(quaternion);
|
||||
setOrientation([...quaternion]);
|
||||
}
|
||||
aSensor.addEventListener('reading', function(e) {
|
||||
setAcceleration([aSensor.x, aSensor.y, aSensor.z]);
|
||||
});
|
||||
aSensor.onerror = alert;
|
||||
oSensor.start();
|
||||
aSensor.start();
|
||||
return () => {
|
||||
oSensor.stop();
|
||||
aSensor.stop();
|
||||
}
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
}, []);
|
||||
|
||||
fn(async () => {
|
||||
let aclReporting = false;
|
||||
if ('Accelerometer' in window) {
|
||||
let acl = new window.Accelerometer({ frequency: 60 });
|
||||
acl.onreading = (e) => {
|
||||
aclReporting = true;
|
||||
acl.stop();
|
||||
};
|
||||
acl.start();
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
return [
|
||||
["Accelerometer in window", 'Accelerometer' in window],
|
||||
["Support DeviceOrientationEvent?", !!window.DeviceOrientationEvent],
|
||||
["Accelerometer reporting?", aclReporting]
|
||||
];
|
||||
});
|
||||
|
||||
if (!value) return null;
|
||||
|
||||
const boxDimensions = window.outerWidth < window.outerHeight ? [2.5, 4, 1] : [4, 2.5, 1];
|
||||
|
||||
return (
|
||||
<>
|
||||
<YesNoList list={value} />
|
||||
<Divider my={2} />
|
||||
<Canvas>
|
||||
<perspectiveCamera makeDefault
|
||||
args={[60, 2, 0.1, 500]}
|
||||
position={[-1, -1, -1.5]}>
|
||||
<axesHelper args={[10]} />
|
||||
<mesh ref={deviceMesh}>
|
||||
<boxGeometry args={boxDimensions} />
|
||||
<meshBasicMaterial color="royalblue" />
|
||||
</mesh>
|
||||
</perspectiveCamera>
|
||||
</Canvas>
|
||||
{orientation && (
|
||||
<>
|
||||
<Divider my={2} />
|
||||
<Text fontSize="sm" fontWeight="500">Orientation</Text>
|
||||
<Text fontSize="xs">
|
||||
{orientation.map((o) => o.toFixed(4)).join(" ")}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{acceleration && (
|
||||
<>
|
||||
<Divider my={2} />
|
||||
<Text fontSize="sm" fontWeight="500">Acceleration</Text>
|
||||
<Text fontSize="xs">
|
||||
{acceleration.map((o) => o.toFixed(4)).join(" ")}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
{(orientation || acceleration) && (
|
||||
<Link href="javascript:void(0)" color="teal.500" onClick={() => setChartModalOpen(true)}>Display as timeseries charts</Link>
|
||||
)}
|
||||
|
||||
{(orientation || acceleration) && chartModalOpen && (
|
||||
<Modal isOpen onClose={() => setChartModalOpen(false)}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalBody>
|
||||
<ModalHeader>
|
||||
Sensor reading visualisation
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
|
||||
<Text mt={4} mb={2}>Acceleration</Text>
|
||||
<Line
|
||||
animation={false}
|
||||
data={sensorChartData[0]}
|
||||
options={{
|
||||
animation: false,
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0 // default to disabled in all datasets
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Text mt={4} mb={2}>Acceleration</Text>
|
||||
<Line
|
||||
animation={false}
|
||||
data={sensorChartData[1]}
|
||||
options={{
|
||||
animation: false,
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0 // default to disabled in all datasets
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default tester(DeviceSensors, {
|
||||
key: 'sensors',
|
||||
title: "Device sensors",
|
||||
explainer: (
|
||||
<>
|
||||
Accelerometer, gyroscope and others with visualized readings.
|
||||
<Text fontSize="sm">Please note that current reading is not part of the fingerprint</Text>
|
||||
</>
|
||||
)
|
||||
});
|
||||
22
tester/src/testers/DocumentStatus.jsx
Normal file
22
tester/src/testers/DocumentStatus.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import tester from "./tester";
|
||||
import React from "react";
|
||||
import {Code} from "@chakra-ui/react";
|
||||
import {DictToTable} from "./utils";
|
||||
|
||||
const DocumentStatus = ({ fn, value }) => {
|
||||
fn(() => ({
|
||||
hasFocus: document.hasFocus(),
|
||||
compatMode: document.compatMode,
|
||||
documentURI: document.documentURI,
|
||||
designMode: document.designMode,
|
||||
}));
|
||||
return (
|
||||
<DictToTable dict={value} />
|
||||
);
|
||||
};
|
||||
|
||||
export default tester(DocumentStatus, {
|
||||
key: 'document',
|
||||
title: "Document",
|
||||
explainer: null
|
||||
});
|
||||
24
tester/src/testers/EncryptedMediaExtensions.jsx
Normal file
24
tester/src/testers/EncryptedMediaExtensions.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import tester from "./tester";
|
||||
import React from "react";
|
||||
import {Code, Text} from "@chakra-ui/react";
|
||||
|
||||
const EncryptedMediaExtensions = ({ fn, value }) => {
|
||||
// TODO WIP
|
||||
fn(async () => {
|
||||
return {};
|
||||
});
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default tester(EncryptedMediaExtensions, {
|
||||
key: 'eme',
|
||||
title: "Encrypted Media Extensions",
|
||||
});
|
||||
|
||||
|
||||
32
tester/src/testers/FeaturePolicy.jsx
Normal file
32
tester/src/testers/FeaturePolicy.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import tester from "./tester";
|
||||
import React from "react";
|
||||
import {Code, Text} from "@chakra-ui/react";
|
||||
|
||||
const FeaturePolicy = ({ fn, value }) => {
|
||||
fn(async () => document.featurePolicy.features());
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Text mb={2}>
|
||||
Detected: {value.length}
|
||||
</Text>
|
||||
{value.map((policy, idx) => (
|
||||
<Code key={idx} mr={2} mb={1}>{policy}</Code>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default tester(FeaturePolicy, {
|
||||
key: 'featurePolicy',
|
||||
title: "Feature policy",
|
||||
explainer: (
|
||||
<>
|
||||
Some of these are reflection of browser's trials.
|
||||
</>
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
59
tester/src/testers/MediaDevices.jsx
Normal file
59
tester/src/testers/MediaDevices.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import tester from "./tester";
|
||||
import React from "react";
|
||||
import {HStack, Box, Code, Text, Tag, Divider} from "@chakra-ui/react";
|
||||
|
||||
const MediaDevices = ({ fn, value }) => {
|
||||
fn(async () => {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return {
|
||||
audioInput: devices.filter((d) => d.kind === "audioinput").length,
|
||||
audioOutput: devices.filter((d) => d.kind === "audiooutput").length,
|
||||
videoInput: devices.filter((d) => d.kind === "videoinput").length,
|
||||
supportedConstraints: Object.entries(await navigator.mediaDevices.getSupportedConstraints())
|
||||
.map(([k, v]) => !!v ? k : false)
|
||||
.filter(Boolean)
|
||||
};
|
||||
});
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<HStack spacing={8}>
|
||||
<HStack borderWidth="1px" borderRadius={8} px={4} py={2} fontSize="sm">
|
||||
<Text>Audio Input</Text>
|
||||
<Text fontWeight="500">{value.audioInput}</Text>
|
||||
</HStack>
|
||||
<HStack borderWidth="1px" borderRadius={8} px={4} py={2} fontSize="sm">
|
||||
<Text>Audio Output</Text>
|
||||
<Text fontWeight="500">{value.audioOutput}</Text>
|
||||
</HStack>
|
||||
<HStack borderWidth="1px" borderRadius={8} px={4} py={2} fontSize="sm">
|
||||
<Text>Video Input</Text>
|
||||
<Text fontWeight="500">{value.videoInput}</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
<Divider my={4} />
|
||||
|
||||
<Text mb={4}>Supported constraints (total {value.supportedConstraints.length}):</Text>
|
||||
{value.supportedConstraints.map((k, idx) => (
|
||||
<Tag key={idx} mr={2} mb={1}>
|
||||
{k}
|
||||
</Tag>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default tester(MediaDevices, {
|
||||
key: 'mediaDevices',
|
||||
title: "Media Devices",
|
||||
explainer: (
|
||||
<>
|
||||
Type of input/output devices registered by the browser.
|
||||
</>
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
59
tester/src/testers/SpeechSynthesis.jsx
Normal file
59
tester/src/testers/SpeechSynthesis.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import tester from "./tester";
|
||||
import React from "react";
|
||||
import {Code, Spinner, Wrap, WrapItem, Box, Text, HStack} from "@chakra-ui/react";
|
||||
import Flag from 'react-flagkit';
|
||||
import {DictToTable} from "./utils";
|
||||
|
||||
const fkc = (k) => {
|
||||
try {
|
||||
return k.split("#")[0].replace("_", "-").split("-")[1].toUpperCase();
|
||||
} catch (err) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const SpeechSynthesis = ({ value, fn }) => {
|
||||
fn(async () => {
|
||||
let voicesList = [];
|
||||
for (let i=0; i<10; i++) {
|
||||
voicesList = window.speechSynthesis.getVoices();
|
||||
if (voicesList.length > 0) {
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
return voicesList.map((v) => ({ lang: v.lang, name: v.name.slice(0, 24) }));
|
||||
});
|
||||
if (!value) return null;
|
||||
return (
|
||||
<>
|
||||
<Text mb={4}>
|
||||
Detected: {value.length}
|
||||
</Text>
|
||||
<Wrap>
|
||||
{value.map((v, idx) => (
|
||||
<WrapItem key={idx}>
|
||||
<Box mr={2} px={2}>
|
||||
<HStack>
|
||||
<Flag country={fkc(v.lang)} style={{ display: 'inline-block', width: 16, height: 16, marginRight: 2 }} />
|
||||
<Text fontSize="xs" color="gray.600" isTruncated style={{ maxWidth: '72px' }}>
|
||||
{v.name}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
</WrapItem>
|
||||
))}
|
||||
</Wrap>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default tester(SpeechSynthesis, {
|
||||
key: 'speechSynthesis',
|
||||
title: "Speech Synthesis API voices",
|
||||
explainer: (
|
||||
<>
|
||||
List of detected voices for speech synthesis.
|
||||
</>
|
||||
)
|
||||
});
|
||||
60
tester/src/testers/tester.jsx
Normal file
60
tester/src/testers/tester.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import {useDispatch, useSelector} from "react-redux";
|
||||
import {Box, Heading, Text, Spinner, Alert, AlertIcon} from "@chakra-ui/react";
|
||||
import {statusSet} from "../state/actions";
|
||||
|
||||
const TesterStatus = {
|
||||
LOADING: 1,
|
||||
ERROR: 2,
|
||||
LOADED: 3
|
||||
}
|
||||
|
||||
export default (cls, config) => () => {
|
||||
const { key, title, explainer } = config;
|
||||
const usePersisted = useSelector((state) => state.persisted);
|
||||
const storedValue = useSelector((state) => state.status[config.key]);
|
||||
const [status, setStatus] = React.useState(true);
|
||||
const dispatch = useDispatch();
|
||||
const assocTestFn = (fn) => React.useEffect(async () => {
|
||||
setStatus(TesterStatus.LOADING);
|
||||
if (!usePersisted) {
|
||||
try {
|
||||
dispatch(statusSet(key, await fn()));
|
||||
setTimeout(function() {
|
||||
setStatus(TesterStatus.LOADED);
|
||||
}, 500);
|
||||
} catch (e) {
|
||||
setStatus(TesterStatus.ERROR);
|
||||
}
|
||||
} else {
|
||||
setStatus(TesterStatus.LOADED);
|
||||
}
|
||||
}, [usePersisted]);
|
||||
const instance = React.createElement(cls, { fn: assocTestFn, value: storedValue });
|
||||
return (
|
||||
<Box title={title} borderRadius="lg" borderWidth={1} py={4} px={6} shadow="sm" mb={4} className="bia">
|
||||
{title && (
|
||||
<Heading as="h2" size="md">
|
||||
{title} {status === TesterStatus.LOADING && <Spinner size="sm" ml={2} />}
|
||||
</Heading>
|
||||
)}
|
||||
|
||||
{status === TesterStatus.ERROR && (
|
||||
<Alert status="error" mt={3}>
|
||||
<AlertIcon />
|
||||
There was a problem running test.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{explainer && (
|
||||
<Text color="gray.500" mt={2}>
|
||||
{explainer}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Box mt={4}>
|
||||
{instance}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
56
tester/src/testers/utils.jsx
Normal file
56
tester/src/testers/utils.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import {HStack, Box, Tag, Table, Thead, Tbody, Tr, Td, Th, Text} from "@chakra-ui/react";
|
||||
|
||||
const render = (v) => {
|
||||
switch (typeof v) {
|
||||
case "boolean":
|
||||
return v ? "✔️" : "❌";
|
||||
default:
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
export const YesNoList = ({ list = [] }) => (
|
||||
<Box>
|
||||
{list.map(([k, v], idx) => (
|
||||
<Tag key={idx} fontSize="xs" mr={2} mb={1}>
|
||||
<HStack>
|
||||
<Text>{k}</Text>
|
||||
<Text>{!!v ? "✔️" : "❌"}</Text>
|
||||
</HStack>
|
||||
</Tag>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const DictToTable = ({ dict = {}, limitKeys = [] }) => (
|
||||
<Box border="1px" borderColor="gray.100" borderRadius="md">
|
||||
<Table size="sm" shadow="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>
|
||||
Key
|
||||
</Th>
|
||||
<Th>
|
||||
Value
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{Object.entries(dict).map(([k, v], idx) => (limitKeys.length === 0 || limitKeys.indexOf(k) >= 0) ? (
|
||||
<Tr key={idx}>
|
||||
<Td>
|
||||
<Text color="gray.700">{k}</Text>
|
||||
</Td>
|
||||
<Td>
|
||||
<Text isTruncated>
|
||||
{render(v)}
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
) : null)}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
|
||||
172
tester/src/utils/base64.js
Normal file
172
tester/src/utils/base64.js
Normal file
@@ -0,0 +1,172 @@
|
||||
var Base64 = (function() {
|
||||
"use strict";
|
||||
|
||||
var _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
|
||||
var _utf8_encode = function (string) {
|
||||
|
||||
var utftext = "", c, n;
|
||||
|
||||
string = string.replace(/\r\n/g,"\n");
|
||||
|
||||
for (n = 0; n < string.length; n++) {
|
||||
|
||||
c = string.charCodeAt(n);
|
||||
|
||||
if (c < 128) {
|
||||
|
||||
utftext += String.fromCharCode(c);
|
||||
|
||||
} else if((c > 127) && (c < 2048)) {
|
||||
|
||||
utftext += String.fromCharCode((c >> 6) | 192);
|
||||
utftext += String.fromCharCode((c & 63) | 128);
|
||||
|
||||
} else {
|
||||
|
||||
utftext += String.fromCharCode((c >> 12) | 224);
|
||||
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
|
||||
utftext += String.fromCharCode((c & 63) | 128);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return utftext;
|
||||
};
|
||||
|
||||
var _utf8_decode = function (utftext) {
|
||||
var string = "", i = 0, c = 0, c1 = 0, c2 = 0;
|
||||
|
||||
while ( i < utftext.length ) {
|
||||
|
||||
c = utftext.charCodeAt(i);
|
||||
|
||||
if (c < 128) {
|
||||
|
||||
string += String.fromCharCode(c);
|
||||
i++;
|
||||
|
||||
} else if((c > 191) && (c < 224)) {
|
||||
|
||||
c1 = utftext.charCodeAt(i+1);
|
||||
string += String.fromCharCode(((c & 31) << 6) | (c1 & 63));
|
||||
i += 2;
|
||||
|
||||
} else {
|
||||
|
||||
c1 = utftext.charCodeAt(i+1);
|
||||
c2 = utftext.charCodeAt(i+2);
|
||||
string += String.fromCharCode(((c & 15) << 12) | ((c1 & 63) << 6) | (c2 & 63));
|
||||
i += 3;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return string;
|
||||
};
|
||||
|
||||
var _hexEncode = function(input) {
|
||||
var output = '', i;
|
||||
|
||||
for(i = 0; i < input.length; i++) {
|
||||
output += input.charCodeAt(i).toString(16);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
var _hexDecode = function(input) {
|
||||
var output = '', i;
|
||||
|
||||
if(input.length % 2 > 0) {
|
||||
input = '0' + input;
|
||||
}
|
||||
|
||||
for(i = 0; i < input.length; i = i + 2) {
|
||||
output += String.fromCharCode(parseInt(input.charAt(i) + input.charAt(i + 1), 16));
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
var encode = function (input) {
|
||||
var output = "", chr1, chr2, chr3, enc1, enc2, enc3, enc4, i = 0;
|
||||
|
||||
input = _utf8_encode(input);
|
||||
|
||||
while (i < input.length) {
|
||||
|
||||
chr1 = input.charCodeAt(i++);
|
||||
chr2 = input.charCodeAt(i++);
|
||||
chr3 = input.charCodeAt(i++);
|
||||
|
||||
enc1 = chr1 >> 2;
|
||||
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
|
||||
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
|
||||
enc4 = chr3 & 63;
|
||||
|
||||
if (isNaN(chr2)) {
|
||||
enc3 = enc4 = 64;
|
||||
} else if (isNaN(chr3)) {
|
||||
enc4 = 64;
|
||||
}
|
||||
|
||||
output += _keyStr.charAt(enc1);
|
||||
output += _keyStr.charAt(enc2);
|
||||
output += _keyStr.charAt(enc3);
|
||||
output += _keyStr.charAt(enc4);
|
||||
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
var decode = function (input) {
|
||||
var output = "", chr1, chr2, chr3, enc1, enc2, enc3, enc4, i = 0;
|
||||
|
||||
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
|
||||
|
||||
while (i < input.length) {
|
||||
|
||||
enc1 = _keyStr.indexOf(input.charAt(i++));
|
||||
enc2 = _keyStr.indexOf(input.charAt(i++));
|
||||
enc3 = _keyStr.indexOf(input.charAt(i++));
|
||||
enc4 = _keyStr.indexOf(input.charAt(i++));
|
||||
|
||||
chr1 = (enc1 << 2) | (enc2 >> 4);
|
||||
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
||||
chr3 = ((enc3 & 3) << 6) | enc4;
|
||||
|
||||
output += String.fromCharCode(chr1);
|
||||
|
||||
if (enc3 !== 64) {
|
||||
output += String.fromCharCode(chr2);
|
||||
}
|
||||
if (enc4 !== 64) {
|
||||
output += String.fromCharCode(chr3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return _utf8_decode(output);
|
||||
};
|
||||
|
||||
var decodeToHex = function(input) {
|
||||
return _hexEncode(decode(input));
|
||||
};
|
||||
|
||||
var encodeFromHex = function(input) {
|
||||
return encode(_hexDecode(input));
|
||||
};
|
||||
|
||||
return {
|
||||
'encode': encode,
|
||||
'decode': decode,
|
||||
'decodeToHex': decodeToHex,
|
||||
'encodeFromHex': encodeFromHex
|
||||
};
|
||||
}());
|
||||
|
||||
export default Base64;
|
||||
3
tester/src/utils/md5.js
Normal file
3
tester/src/utils/md5.js
Normal file
@@ -0,0 +1,3 @@
|
||||
function md5(d){return rstr2hex(binl2rstr(binl_md5(rstr2binl(d),8*d.length)))}function rstr2hex(d){for(var _,m="0123456789ABCDEF",f="",r=0;r<d.length;r++)_=d.charCodeAt(r),f+=m.charAt(_>>>4&15)+m.charAt(15&_);return f}function rstr2binl(d){for(var _=Array(d.length>>2),m=0;m<_.length;m++)_[m]=0;for(m=0;m<8*d.length;m+=8)_[m>>5]|=(255&d.charCodeAt(m/8))<<m%32;return _}function binl2rstr(d){for(var _="",m=0;m<32*d.length;m+=8)_+=String.fromCharCode(d[m>>5]>>>m%32&255);return _}function binl_md5(d,_){d[_>>5]|=128<<_%32,d[14+(_+64>>>9<<4)]=_;for(var m=1732584193,f=-271733879,r=-1732584194,i=271733878,n=0;n<d.length;n+=16){var h=m,t=f,g=r,e=i;f=md5_ii(f=md5_ii(f=md5_ii(f=md5_ii(f=md5_hh(f=md5_hh(f=md5_hh(f=md5_hh(f=md5_gg(f=md5_gg(f=md5_gg(f=md5_gg(f=md5_ff(f=md5_ff(f=md5_ff(f=md5_ff(f,r=md5_ff(r,i=md5_ff(i,m=md5_ff(m,f,r,i,d[n+0],7,-680876936),f,r,d[n+1],12,-389564586),m,f,d[n+2],17,606105819),i,m,d[n+3],22,-1044525330),r=md5_ff(r,i=md5_ff(i,m=md5_ff(m,f,r,i,d[n+4],7,-176418897),f,r,d[n+5],12,1200080426),m,f,d[n+6],17,-1473231341),i,m,d[n+7],22,-45705983),r=md5_ff(r,i=md5_ff(i,m=md5_ff(m,f,r,i,d[n+8],7,1770035416),f,r,d[n+9],12,-1958414417),m,f,d[n+10],17,-42063),i,m,d[n+11],22,-1990404162),r=md5_ff(r,i=md5_ff(i,m=md5_ff(m,f,r,i,d[n+12],7,1804603682),f,r,d[n+13],12,-40341101),m,f,d[n+14],17,-1502002290),i,m,d[n+15],22,1236535329),r=md5_gg(r,i=md5_gg(i,m=md5_gg(m,f,r,i,d[n+1],5,-165796510),f,r,d[n+6],9,-1069501632),m,f,d[n+11],14,643717713),i,m,d[n+0],20,-373897302),r=md5_gg(r,i=md5_gg(i,m=md5_gg(m,f,r,i,d[n+5],5,-701558691),f,r,d[n+10],9,38016083),m,f,d[n+15],14,-660478335),i,m,d[n+4],20,-405537848),r=md5_gg(r,i=md5_gg(i,m=md5_gg(m,f,r,i,d[n+9],5,568446438),f,r,d[n+14],9,-1019803690),m,f,d[n+3],14,-187363961),i,m,d[n+8],20,1163531501),r=md5_gg(r,i=md5_gg(i,m=md5_gg(m,f,r,i,d[n+13],5,-1444681467),f,r,d[n+2],9,-51403784),m,f,d[n+7],14,1735328473),i,m,d[n+12],20,-1926607734),r=md5_hh(r,i=md5_hh(i,m=md5_hh(m,f,r,i,d[n+5],4,-378558),f,r,d[n+8],11,-2022574463),m,f,d[n+11],16,1839030562),i,m,d[n+14],23,-35309556),r=md5_hh(r,i=md5_hh(i,m=md5_hh(m,f,r,i,d[n+1],4,-1530992060),f,r,d[n+4],11,1272893353),m,f,d[n+7],16,-155497632),i,m,d[n+10],23,-1094730640),r=md5_hh(r,i=md5_hh(i,m=md5_hh(m,f,r,i,d[n+13],4,681279174),f,r,d[n+0],11,-358537222),m,f,d[n+3],16,-722521979),i,m,d[n+6],23,76029189),r=md5_hh(r,i=md5_hh(i,m=md5_hh(m,f,r,i,d[n+9],4,-640364487),f,r,d[n+12],11,-421815835),m,f,d[n+15],16,530742520),i,m,d[n+2],23,-995338651),r=md5_ii(r,i=md5_ii(i,m=md5_ii(m,f,r,i,d[n+0],6,-198630844),f,r,d[n+7],10,1126891415),m,f,d[n+14],15,-1416354905),i,m,d[n+5],21,-57434055),r=md5_ii(r,i=md5_ii(i,m=md5_ii(m,f,r,i,d[n+12],6,1700485571),f,r,d[n+3],10,-1894986606),m,f,d[n+10],15,-1051523),i,m,d[n+1],21,-2054922799),r=md5_ii(r,i=md5_ii(i,m=md5_ii(m,f,r,i,d[n+8],6,1873313359),f,r,d[n+15],10,-30611744),m,f,d[n+6],15,-1560198380),i,m,d[n+13],21,1309151649),r=md5_ii(r,i=md5_ii(i,m=md5_ii(m,f,r,i,d[n+4],6,-145523070),f,r,d[n+11],10,-1120210379),m,f,d[n+2],15,718787259),i,m,d[n+9],21,-343485551),m=safe_add(m,h),f=safe_add(f,t),r=safe_add(r,g),i=safe_add(i,e)}return Array(m,f,r,i)}function md5_cmn(d,_,m,f,r,i){return safe_add(bit_rol(safe_add(safe_add(_,d),safe_add(f,i)),r),m)}function md5_ff(d,_,m,f,r,i,n){return md5_cmn(_&m|~_&f,d,_,r,i,n)}function md5_gg(d,_,m,f,r,i,n){return md5_cmn(_&f|m&~f,d,_,r,i,n)}function md5_hh(d,_,m,f,r,i,n){return md5_cmn(_^m^f,d,_,r,i,n)}function md5_ii(d,_,m,f,r,i,n){return md5_cmn(m^(_|~f),d,_,r,i,n)}function safe_add(d,_){var m=(65535&d)+(65535&_);return(d>>16)+(_>>16)+(m>>16)<<16|65535&m}function bit_rol(d,_){return d<<_|d>>>32-_}
|
||||
|
||||
export default md5;
|
||||
14
tester/vite.config.js
Normal file
14
tester/vite.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import reactRefresh from '@vitejs/plugin-react-refresh'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [reactRefresh()],
|
||||
define: {
|
||||
'process.env': {}
|
||||
},
|
||||
build: {
|
||||
target: 'es2015',
|
||||
outDir: './../public/'
|
||||
},
|
||||
})
|
||||
1895
tester/yarn.lock
Normal file
1895
tester/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user