├── stealth ├── vendor │ ├── echos.json │ ├── peers.json │ ├── tasks.json │ ├── account.json │ ├── sessions.json │ ├── internet.json │ ├── interface.json │ ├── redirects.json │ ├── README.md │ ├── hosts.json │ ├── beacons.json │ └── policies.json ├── .gitignore ├── source │ ├── server │ │ ├── Defender.gz │ │ └── service │ │ │ └── Blocker.mjs │ ├── client │ │ └── service │ │ │ ├── Blocker.mjs │ │ │ ├── Settings.mjs │ │ │ ├── Echo.mjs │ │ │ ├── Mode.mjs │ │ │ ├── Task.mjs │ │ │ ├── Beacon.mjs │ │ │ ├── Policy.mjs │ │ │ ├── Redirect.mjs │ │ │ ├── Host.mjs │ │ │ └── Cache.mjs │ ├── optimizer │ │ ├── HTML.mjs │ │ └── CSS.mjs │ └── parser │ │ └── CSS.mjs ├── package │ └── archlinux │ │ ├── tholian-stealth.service │ │ └── PKGBUILD ├── .eslintrc.json ├── README.md └── review │ ├── client │ └── service │ │ ├── Blocker.mjs │ │ ├── Mode.mjs │ │ └── Beacon.mjs │ ├── connection │ └── WHOIS.mjs │ └── server │ └── service │ └── Blocker.mjs ├── base ├── .gitignore ├── package.json ├── source │ ├── node │ │ └── Buffer.mjs │ ├── Map.mjs │ ├── Set.mjs │ ├── Date.mjs │ ├── Number.mjs │ ├── RegExp.mjs │ ├── String.mjs │ ├── Boolean.mjs │ ├── Function.mjs │ ├── MODULE.mjs │ ├── Object.mjs │ └── Array.mjs ├── review │ ├── index.mjs │ ├── Set.mjs │ ├── Number.mjs │ ├── RegExp.mjs │ ├── String.mjs │ ├── Date.mjs │ ├── Map.mjs │ ├── Function.mjs │ ├── MODULE.mjs │ ├── Boolean.mjs │ ├── Object.mjs │ └── Array.mjs ├── index.mjs ├── .eslintrc.json └── README.md ├── covert ├── .gitignore ├── review │ ├── index.mjs │ └── MODULE.mjs ├── .eslintrc.json ├── index.mjs └── README.md ├── .github ├── FUNDING.yml └── workflows │ ├── covert-scan.mjs │ ├── covert-check.mjs │ ├── eslint-lint.yml │ ├── covert-check.yml │ └── covert-scan.yml ├── browser ├── design │ ├── common │ │ ├── icon.woff │ │ ├── tholian.ico │ │ ├── vera-mono.woff │ │ ├── museo-medium.woff │ │ ├── museo-regular.woff │ │ ├── tholian-android.png │ │ ├── tholian-apple.png │ │ ├── crystalline-bold.woff │ │ ├── crystalline-light.woff │ │ ├── crystalline-medium.woff │ │ ├── crystalline-regular.woff │ │ ├── theme-dark-element-select-default.svg │ │ ├── theme-dark-element-select-focus.svg │ │ ├── theme-light-element-select-default.svg │ │ ├── theme-light-element-select-focus.svg │ │ ├── tholian.svg │ │ ├── index.css │ │ ├── layout.css │ │ └── theme.css │ ├── appbar │ │ ├── Mode.click.wav │ │ ├── Address.blur.wav │ │ ├── History.back.wav │ │ ├── History.next.wav │ │ ├── History.open.wav │ │ ├── Address.focus.wav │ │ ├── History.reload.wav │ │ ├── Settings.hide.wav │ │ ├── Settings.show.wav │ │ ├── Address.navigate.wav │ │ ├── Settings.browser.wav │ │ ├── History.css │ │ ├── Settings.css │ │ ├── Mode.css │ │ ├── Splitter.css │ │ └── Splitter.mjs │ ├── menu │ │ ├── Context.hide.wav │ │ ├── Context.show.wav │ │ └── Context.select.wav │ ├── backdrop │ │ ├── Tabs.close.wav │ │ ├── Tabs.select.wav │ │ └── Webview.css │ ├── sheet │ │ ├── Console.hide.wav │ │ ├── Console.show.wav │ │ ├── Site.css │ │ ├── Session.css │ │ ├── Site.mjs │ │ ├── Console.css │ │ ├── Session.mjs │ │ └── Console.mjs │ ├── widget │ │ ├── Image.css │ │ ├── Audio.css │ │ └── Video.css │ ├── Assistant.mjs │ ├── card │ │ ├── Blocker.mjs │ │ ├── Blocker.css │ │ ├── Host.css │ │ └── Session.css │ └── index.css ├── package │ ├── electron │ │ ├── build │ │ │ └── icon.png │ │ ├── app │ │ │ └── package.json │ │ └── electron-builder.json │ └── archlinux │ │ ├── tholian-browser.desktop │ │ ├── tholian-browser.svg │ │ └── PKGBUILD ├── .gitignore ├── app │ └── package.json ├── index.webmanifest ├── index.mjs ├── internal │ ├── media.html │ ├── search.html │ ├── media.mjs │ ├── debug.html │ ├── search.mjs │ ├── settings.html │ ├── debug.mjs │ ├── settings.mjs │ ├── media │ │ └── Media.css │ ├── fix-host.html │ ├── fix-connection.html │ └── debug │ │ └── Browser.css ├── .eslintrc.json ├── README.md └── source │ ├── ENVIRONMENT.mjs │ └── Session.mjs ├── .gitignore ├── Dockerfile ├── package.json ├── guide ├── security │ └── Network.md ├── README.md └── concept │ └── Service.md └── TODO.md /stealth/vendor/echos.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /stealth/vendor/peers.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /stealth/vendor/tasks.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /base/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | 3 | -------------------------------------------------------------------------------- /stealth/vendor/account.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /stealth/vendor/sessions.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /covert/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # shared with /base 3 | /extern/base.mjs 4 | 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: cookiengineer 4 | 5 | -------------------------------------------------------------------------------- /browser/design/common/icon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/common/icon.woff -------------------------------------------------------------------------------- /stealth/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # packages 3 | /build/* 4 | 5 | # shared with /base 6 | /extern/base.mjs 7 | 8 | -------------------------------------------------------------------------------- /browser/design/common/tholian.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/common/tholian.ico -------------------------------------------------------------------------------- /stealth/source/server/Defender.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/stealth/source/server/Defender.gz -------------------------------------------------------------------------------- /browser/design/appbar/Mode.click.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/appbar/Mode.click.wav -------------------------------------------------------------------------------- /browser/design/common/vera-mono.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/common/vera-mono.woff -------------------------------------------------------------------------------- /browser/design/menu/Context.hide.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/menu/Context.hide.wav -------------------------------------------------------------------------------- /browser/design/menu/Context.show.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/menu/Context.show.wav -------------------------------------------------------------------------------- /browser/design/appbar/Address.blur.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/appbar/Address.blur.wav -------------------------------------------------------------------------------- /browser/design/appbar/History.back.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/appbar/History.back.wav -------------------------------------------------------------------------------- /browser/design/appbar/History.next.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/appbar/History.next.wav -------------------------------------------------------------------------------- /browser/design/appbar/History.open.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/appbar/History.open.wav -------------------------------------------------------------------------------- /browser/design/backdrop/Tabs.close.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/backdrop/Tabs.close.wav -------------------------------------------------------------------------------- /browser/design/menu/Context.select.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/menu/Context.select.wav -------------------------------------------------------------------------------- /browser/design/sheet/Console.hide.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/sheet/Console.hide.wav -------------------------------------------------------------------------------- /browser/design/sheet/Console.show.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/sheet/Console.show.wav -------------------------------------------------------------------------------- /stealth/vendor/internet.json: -------------------------------------------------------------------------------- 1 | { 2 | "connection": "mobile", 3 | "history": "stealth", 4 | "useragent": "stealth" 5 | } -------------------------------------------------------------------------------- /browser/design/appbar/Address.focus.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/appbar/Address.focus.wav -------------------------------------------------------------------------------- /browser/design/appbar/History.reload.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/appbar/History.reload.wav -------------------------------------------------------------------------------- /browser/design/appbar/Settings.hide.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/appbar/Settings.hide.wav -------------------------------------------------------------------------------- /browser/design/appbar/Settings.show.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/appbar/Settings.show.wav -------------------------------------------------------------------------------- /browser/design/backdrop/Tabs.select.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/backdrop/Tabs.select.wav -------------------------------------------------------------------------------- /browser/design/common/museo-medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/common/museo-medium.woff -------------------------------------------------------------------------------- /browser/design/common/museo-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/common/museo-regular.woff -------------------------------------------------------------------------------- /browser/design/common/tholian-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/common/tholian-android.png -------------------------------------------------------------------------------- /browser/design/common/tholian-apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/common/tholian-apple.png -------------------------------------------------------------------------------- /browser/package/electron/build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/package/electron/build/icon.png -------------------------------------------------------------------------------- /browser/design/appbar/Address.navigate.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/appbar/Address.navigate.wav -------------------------------------------------------------------------------- /browser/design/appbar/Settings.browser.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/appbar/Settings.browser.wav -------------------------------------------------------------------------------- /browser/design/common/crystalline-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/common/crystalline-bold.woff -------------------------------------------------------------------------------- /browser/design/common/crystalline-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/common/crystalline-light.woff -------------------------------------------------------------------------------- /browser/design/common/crystalline-medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/common/crystalline-medium.woff -------------------------------------------------------------------------------- /browser/design/common/crystalline-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2Cmo/stealth/X0/browser/design/common/crystalline-regular.woff -------------------------------------------------------------------------------- /stealth/vendor/interface.json: -------------------------------------------------------------------------------- 1 | { 2 | "assistant": false, 3 | "theme": "dark", 4 | "enforce": false, 5 | "opentab": "stealth:blank" 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /.github/ISSUES 3 | /.github/TOKEN 4 | /.github/workflows/*.report 5 | 6 | 7 | /node_modules 8 | /package-lock.json 9 | 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | RUN mkdir "/profile" 3 | RUN node "./make.mjs" 4 | EXPOSE 65432 5 | CMD [ "node", "./stealth/stealth.mjs", "serve", "--profile=/profile" ] 6 | -------------------------------------------------------------------------------- /base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "base", 3 | "main": "./build/node.mjs", 4 | "module": "./build/node.mjs", 5 | "browser": "./build/browser.mjs", 6 | "scripts": { 7 | "postinstall": "node ./make.mjs" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /browser/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # packages 3 | /build/* 4 | 5 | # shared with /base 6 | /extern/base.mjs 7 | 8 | # shared with /stealth 9 | /source/Browser.mjs 10 | /source/Tab.mjs 11 | /source/client 12 | /source/parser 13 | 14 | -------------------------------------------------------------------------------- /stealth/vendor/redirects.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "domain": "tholian.network", 4 | "redirects": [ 5 | { 6 | "path": "/", 7 | "query": null, 8 | "location": "https://tholian.network/index.html" 9 | } 10 | ] 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /base/source/node/Buffer.mjs: -------------------------------------------------------------------------------- 1 | 2 | export const Buffer = (function(global) { 3 | return global.Buffer; 4 | })(typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : this)); 5 | 6 | export const isBuffer = Buffer.isBuffer; 7 | 8 | -------------------------------------------------------------------------------- /stealth/package/archlinux/tholian-stealth.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Tholian Stealth 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/usr/bin/tholian-stealth serve 7 | Restart=always 8 | RestartSec=10 9 | 10 | [Install] 11 | WantedBy=default.target 12 | 13 | -------------------------------------------------------------------------------- /browser/package/archlinux/tholian-browser.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Tholian Browser 3 | Name[en_US]=Tholian Browser 4 | GenericName=Stealth 5 | Exec=/usr/bin/tholian-browser 6 | Icon=tholian-browser 7 | Type=Application 8 | NoDisplay=false 9 | Categories=Network; 10 | 11 | -------------------------------------------------------------------------------- /browser/design/common/theme-dark-element-select-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /browser/design/common/theme-dark-element-select-focus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /browser/design/common/theme-light-element-select-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /browser/design/common/theme-light-element-select-focus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /stealth/vendor/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Stealth Vendor Profile 3 | 4 | This Stealth Vendor Profile is shipped with its default settings 5 | for all Browser instances in the Tholian Network. 6 | 7 | The Profile's default settngs are generated via the [stealth-vendor](https://github.com/tholian-network/stealth-vendor) Repository. 8 | 9 | -------------------------------------------------------------------------------- /base/source/Map.mjs: -------------------------------------------------------------------------------- 1 | 2 | export const Map = (typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : this)).Map; 3 | 4 | if (typeof Map.isMap !== 'function') { 5 | 6 | Map.isMap = function(map) { 7 | return Object.prototype.toString.call(map) === '[object Map]'; 8 | }; 9 | 10 | } 11 | 12 | export const isMap = Map.isMap; 13 | 14 | -------------------------------------------------------------------------------- /base/source/Set.mjs: -------------------------------------------------------------------------------- 1 | 2 | export const Set = (typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : this)).Set; 3 | 4 | if (typeof Set.isSet !== 'function') { 5 | 6 | Set.isSet = function(set) { 7 | return Object.prototype.toString.call(set) === '[object Set]'; 8 | }; 9 | 10 | } 11 | 12 | export const isSet = Set.isSet; 13 | 14 | -------------------------------------------------------------------------------- /base/source/Date.mjs: -------------------------------------------------------------------------------- 1 | 2 | export const Date = (typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : this)).Date; 3 | 4 | if (typeof Date.isDate !== 'function') { 5 | 6 | Date.isDate = function(dat) { 7 | return Object.prototype.toString.call(dat) === '[object Date]'; 8 | }; 9 | 10 | } 11 | 12 | export const isDate = Date.isDate; 13 | 14 | -------------------------------------------------------------------------------- /base/source/Number.mjs: -------------------------------------------------------------------------------- 1 | 2 | export const Number = (typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : this)).Number; 3 | 4 | if (typeof Number.isNumber !== 'function') { 5 | 6 | Number.isNumber = function(num) { 7 | return Object.prototype.toString.call(num) === '[object Number]'; 8 | }; 9 | 10 | } 11 | 12 | export const isNumber = Number.isNumber; 13 | 14 | -------------------------------------------------------------------------------- /base/source/RegExp.mjs: -------------------------------------------------------------------------------- 1 | 2 | export const RegExp = (typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : this)).RegExp; 3 | 4 | if (typeof RegExp.isRegExp !== 'function') { 5 | 6 | RegExp.isRegExp = function(reg) { 7 | return Object.prototype.toString.call(reg) === '[object RegExp]'; 8 | }; 9 | 10 | } 11 | 12 | export const isRegExp = RegExp.isRegExp; 13 | 14 | -------------------------------------------------------------------------------- /base/source/String.mjs: -------------------------------------------------------------------------------- 1 | 2 | export const String = (typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : this)).String; 3 | 4 | if (typeof String.isString !== 'function') { 5 | 6 | String.isString = function(str) { 7 | return Object.prototype.toString.call(str) === '[object String]'; 8 | }; 9 | 10 | } 11 | 12 | export const isString = String.isString; 13 | 14 | -------------------------------------------------------------------------------- /base/source/Boolean.mjs: -------------------------------------------------------------------------------- 1 | 2 | export const Boolean = (typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : this)).Boolean; 3 | 4 | if (typeof Boolean.isBoolean !== 'function') { 5 | 6 | Boolean.isBoolean = function(bol) { 7 | return Object.prototype.toString.call(bol) === '[object Boolean]'; 8 | }; 9 | 10 | } 11 | 12 | export const isBoolean = Boolean.isBoolean; 13 | 14 | -------------------------------------------------------------------------------- /base/source/Function.mjs: -------------------------------------------------------------------------------- 1 | 2 | export const Function = (typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : this)).Function; 3 | 4 | if (typeof Function.isFunction !== 'function') { 5 | 6 | Function.isFunction = function(fun) { 7 | return Object.prototype.toString.call(fun) === '[object Function]'; 8 | }; 9 | 10 | } 11 | 12 | export const isFunction = Function.isFunction; 13 | 14 | -------------------------------------------------------------------------------- /browser/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tholian-browser", 3 | "version": "X0:2021-11-11", 4 | "description": "Secure, Peer-to-Peer, Private and Automateable Web Browser/Scraper/Proxy", 5 | "homepage": "https://tholian.network/index.html", 6 | "repository": "https://github.com/tholian-network/stealth", 7 | "main": "index.js", 8 | "author": { 9 | "name": "Tholian Network", 10 | "email": "void@tholian.network" 11 | } 12 | } -------------------------------------------------------------------------------- /browser/index.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "dir": "ltr", 3 | "lang": "en-us", 4 | "short_name": "Stealth", 5 | "name": "Tholian Stealth", 6 | "display": "fullscreen", 7 | "start_url": "/browser/index.html", 8 | "background_color": "#000000", 9 | "theme_color": "#000000", 10 | 11 | "icons": [{ 12 | "src": "/browser/design/common/tholian-android.png", 13 | "sizes": "256x256", 14 | "type": "image/png" 15 | }] 16 | } 17 | -------------------------------------------------------------------------------- /browser/package/electron/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tholian-browser", 3 | "version": "X0:2021-11-11", 4 | "description": "Secure, Peer-to-Peer, Private and Automateable Web Browser/Scraper/Proxy", 5 | "homepage": "https://tholian.network/index.html", 6 | "repository": "https://github.com/tholian-network/stealth", 7 | "main": "index.js", 8 | "author": { 9 | "name": "Tholian Network", 10 | "email": "void@tholian.network" 11 | }, 12 | "devDependencies": { 13 | "electron": "19.0.8" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /browser/design/common/tholian.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /browser/design/backdrop/Webview.css: -------------------------------------------------------------------------------- 1 | 2 | browser-backdrop-webview { 3 | display: block; 4 | position: fixed; 5 | width: auto; 6 | height: auto; 7 | top: 0px; 8 | right: 0px; 9 | bottom: 0px; 10 | left: 0px; 11 | margin: 0px; 12 | padding: 0px; 13 | transition: 200ms all ease-out; 14 | user-select: none; 15 | -webkit-user-select: none; 16 | pointer-events: auto; 17 | } 18 | 19 | browser-backdrop-webview iframe { 20 | display: block; 21 | width: 100%; 22 | height: 100%; 23 | border: 0px solid transparent; 24 | box-sizing: border-box; 25 | -webkit-overflow-scrolling: touch; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /browser/design/sheet/Site.css: -------------------------------------------------------------------------------- 1 | 2 | browser-sheet-site { 3 | display: block; 4 | position: fixed; 5 | width: auto; 6 | height: auto; 7 | top: 40px; 8 | right: 0px; 9 | bottom: 0px; 10 | left: 0px; 11 | margin: 0px; 12 | padding: 0px; 13 | color: var(--layout-default-color); 14 | font-family: 'museo'; 15 | background: var(--layout-default-background); 16 | border: 0px solid transparent; 17 | transform: translate(100%, 0%); 18 | transition: 200ms transform ease-out; 19 | overflow-x: hidden; 20 | overflow-y: auto; 21 | } 22 | 23 | browser-sheet-site.active { 24 | transform: translate(0%, 0%); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /browser/design/sheet/Session.css: -------------------------------------------------------------------------------- 1 | 2 | browser-sheet-session { 3 | display: block; 4 | position: fixed; 5 | width: auto; 6 | height: auto; 7 | top: 40px; 8 | right: 0px; 9 | bottom: 0px; 10 | left: 0px; 11 | margin: 0px; 12 | padding: 0px; 13 | color: var(--layout-default-color); 14 | font-family: 'museo'; 15 | background: var(--layout-default-background); 16 | border: 0px solid transparent; 17 | transform: translate(100%, 0%); 18 | transition: 200ms transform ease-out; 19 | overflow-x: hidden; 20 | overflow-y: auto; 21 | } 22 | 23 | browser-sheet-session.active { 24 | transform: translate(0%, 0%); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /browser/index.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Browser, isBrowser } from './source/Browser.mjs'; 3 | import { Client, isClient } from './source/Client.mjs'; 4 | import { ENVIRONMENT } from './source/ENVIRONMENT.mjs'; 5 | import { Session, isSession } from './source/Session.mjs'; 6 | import { Tab, isTab } from './source/Tab.mjs'; 7 | 8 | 9 | 10 | export default { 11 | 12 | isBrowser: isBrowser, 13 | isClient: isClient, 14 | isSession: isSession, 15 | isTab: isTab, 16 | 17 | Browser: Browser, 18 | Client: Client, 19 | ENVIRONMENT: ENVIRONMENT, 20 | Session: Session, 21 | Tab: Tab 22 | 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /browser/package/archlinux/tholian-browser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /browser/internal/media.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | stealth:media 6 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /browser/internal/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | stealth:search 6 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /covert/review/index.mjs: -------------------------------------------------------------------------------- 1 | 2 | // import Covert from './Covert.mjs'; 3 | // import Filesystem from './Filesystem.mjs'; 4 | import MODULE from './MODULE.mjs'; 5 | // import Network from './Network.mjs'; 6 | // import Renderer from './Renderer.mjs'; 7 | import Results from './Results.mjs'; 8 | import Review from './Review.mjs'; 9 | import Timeline from './Timeline.mjs'; 10 | 11 | 12 | 13 | export const REVIEWS = [ 14 | 15 | MODULE, 16 | 17 | // Review and Benchmark 18 | Review, 19 | Results, 20 | Timeline, 21 | 22 | // Covert 23 | // Covert, 24 | // Filesystem, 25 | // Network, // Cannot be tested 26 | // Renderer 27 | 28 | ]; 29 | 30 | export const SOURCES = { 31 | 32 | 'covert/ENVIRONMENT': null 33 | 34 | }; 35 | 36 | -------------------------------------------------------------------------------- /browser/package/electron/electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "tholian-browser", 3 | "asar": true, 4 | "asarUnpack": [ 5 | "browser/**/*", 6 | "stealth/**/*" 7 | ], 8 | "artifactName": "${name}-${version}-${os}-${arch}.${ext}", 9 | "directories": { 10 | "app": "app", 11 | "buildResources": "build", 12 | "output": "dist" 13 | }, 14 | "productName": "Tholian Browser", 15 | "copyright": "Copyright (2021) Tholian Network", 16 | "linux": { 17 | "category": "Network", 18 | "target": [ 19 | "appimage", 20 | "zip" 21 | ] 22 | }, 23 | "mac": { 24 | "category": "public.app-category.productivity", 25 | "darkModeSupport": true, 26 | "target": [ 27 | "zip" 28 | ] 29 | }, 30 | "win": { 31 | "target": [ 32 | "zip" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /browser/internal/media.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Element } from '../design/Element.mjs'; 3 | import { Media } from '../internal/media/Media.mjs'; 4 | import { ENVIRONMENT } from '../source/ENVIRONMENT.mjs'; 5 | import { URL } from '../source/parser/URL.mjs'; 6 | 7 | 8 | 9 | let browser = window.parent.BROWSER || null; 10 | if (browser !== null) { 11 | 12 | let widget = Media.from({ source: null }, [ 'refresh', 'fullscreen', 'download' ]); 13 | if (widget !== null) { 14 | 15 | let body = Element.query('body'); 16 | if (body !== null) { 17 | widget.render(body); 18 | } 19 | 20 | 21 | if (URL.isURL(ENVIRONMENT.flags.url) === true) { 22 | 23 | widget.value({ 24 | source: ENVIRONMENT.flags.url 25 | }); 26 | 27 | } 28 | 29 | } 30 | 31 | } 32 | 33 | -------------------------------------------------------------------------------- /.github/workflows/covert-scan.mjs: -------------------------------------------------------------------------------- 1 | 2 | import fs from 'fs'; 3 | import process from 'process'; 4 | 5 | 6 | let REPORT = Array.from(process.argv).slice(2).find((v) => v.startsWith('.github')) || null; 7 | if (REPORT !== null) { 8 | 9 | let buffer = fs.readFileSync(REPORT, 'utf8'); 10 | if (buffer !== null) { 11 | 12 | let failed = false; 13 | 14 | buffer.split('\n').forEach((line) => { 15 | 16 | if ( 17 | line.startsWith('(!)') 18 | && line.trim() !== '(!)' 19 | && line.includes('>') === false 20 | ) { 21 | console.log(line); 22 | failed = true; 23 | } 24 | 25 | }); 26 | 27 | if (failed === true) { 28 | process.exit(1); 29 | } else { 30 | process.exit(0); 31 | } 32 | 33 | } 34 | 35 | } else { 36 | process.exit(1); 37 | } 38 | 39 | -------------------------------------------------------------------------------- /base/source/MODULE.mjs: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | 4 | console: console, 5 | 6 | Array: Array, 7 | Boolean: Boolean, 8 | Buffer: Buffer, 9 | Date: Date, 10 | Emitter: Emitter, 11 | Function: Function, 12 | Map: Map, 13 | Number: Number, 14 | Object: Object, 15 | RegExp: RegExp, 16 | Set: Set, 17 | String: String, 18 | 19 | isArray: Array.isArray, 20 | isBoolean: Boolean.isBoolean, 21 | isBuffer: Buffer.isBuffer, 22 | isDate: Date.isDate, 23 | isEmitter: Emitter.isEmitter, 24 | isFunction: Function.isFunction, 25 | isMap: Map.isMap, 26 | isNumber: Number.isNumber, 27 | isObject: Object.isObject, 28 | isRegExp: RegExp.isRegExp, 29 | isSet: Set.isSet, 30 | isString: String.isString 31 | 32 | }; 33 | 34 | -------------------------------------------------------------------------------- /browser/internal/debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | stealth:debug 6 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/covert-check.mjs: -------------------------------------------------------------------------------- 1 | 2 | import fs from 'fs'; 3 | import process from 'process'; 4 | 5 | 6 | let REPORT = Array.from(process.argv).slice(2).find((v) => v.startsWith('.github')) || null; 7 | if (REPORT !== null) { 8 | 9 | let buffer = fs.readFileSync(REPORT, 'utf8'); 10 | if (buffer !== null) { 11 | 12 | let failed = false; 13 | 14 | buffer.split('\n').forEach((line) => { 15 | 16 | if (line.includes('>') === false) { 17 | 18 | if (line.startsWith('(!)') && line.trim() !== '(!)') { 19 | console.log(line); 20 | failed = true; 21 | } else if (line.startsWith('(?)') && line.trim() !== '(?)') { 22 | console.log(line); 23 | failed = true; 24 | } 25 | 26 | } 27 | 28 | }); 29 | 30 | if (failed === true) { 31 | process.exit(1); 32 | } else { 33 | process.exit(0); 34 | } 35 | 36 | } 37 | 38 | } else { 39 | process.exit(1); 40 | } 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/eslint-lint.yml: -------------------------------------------------------------------------------- 1 | 2 | name: ESLint Lint 3 | 4 | on: [push] 5 | 6 | jobs: 7 | 8 | lint-base: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: 14 17 | - run: npm install 18 | - run: npx eslint ./base 19 | 20 | lint-covert: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 14 29 | - run: npm install 30 | - run: npx eslint ./covert 31 | 32 | lint-stealth: 33 | 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - uses: actions/checkout@v2 38 | - uses: actions/setup-node@v1 39 | with: 40 | node-version: 14 41 | - run: npm install 42 | - run: npx eslint ./stealth 43 | 44 | -------------------------------------------------------------------------------- /stealth/vendor/hosts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "domain": "tholian.network", 4 | "hosts": [ 5 | { 6 | "ip": "185.199.108.153", 7 | "scope": "public", 8 | "type": "v4" 9 | }, { 10 | "ip": "185.199.109.153", 11 | "scope": "public", 12 | "type": "v4" 13 | }, { 14 | "ip": "185.199.110.153", 15 | "scope": "public", 16 | "type": "v4" 17 | }, { 18 | "ip": "185.199.111.153", 19 | "scope": "public", 20 | "type": "v4" 21 | } 22 | ] 23 | }, { 24 | "domain": "searx.prvcy.eu", 25 | "hosts": [ 26 | { 27 | "ip": "95.217.14.39", 28 | "scope": "public", 29 | "type": "v4" 30 | }, 31 | { 32 | "ip": "2a01:4f9:c010:5df1::1", 33 | "scope": "public", 34 | "type": "v6" 35 | } 36 | ] 37 | }, { 38 | "domain": "wiby.me", 39 | "hosts": [ 40 | { 41 | "ip": "172.93.49.59", 42 | "scope": "public", 43 | "type": "v4" 44 | } 45 | ] 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /browser/internal/search.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { isString } from '../extern/base.mjs'; 3 | import { Element } from '../design/Element.mjs'; 4 | import { Search } from '../internal/search/Search.mjs'; 5 | import { ENVIRONMENT } from '../source/ENVIRONMENT.mjs'; 6 | 7 | 8 | 9 | let browser = window.parent.BROWSER || null; 10 | if (browser !== null) { 11 | 12 | let widget = Search.from({ keywords: '', results: [] }, [ 'search' ]); 13 | if (widget !== null) { 14 | 15 | let body = Element.query('body'); 16 | if (body !== null) { 17 | widget.render(body); 18 | } 19 | 20 | 21 | if (isString(ENVIRONMENT.flags.keywords) === true) { 22 | 23 | let keywords = decodeURIComponent(ENVIRONMENT.flags.keywords).trim(); 24 | if (keywords.length > 3) { 25 | 26 | widget.value({ 27 | keywords: keywords, 28 | results: [] 29 | }); 30 | 31 | widget.buttons.search.emit('click'); 32 | 33 | } 34 | 35 | } 36 | 37 | } 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /stealth/vendor/beacons.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "domain": "tholian.network", 4 | "beacons": [ 5 | { 6 | "path": "/*", 7 | "query": null, 8 | "select": "article div,article figure,article p,article ul", 9 | "term": "article" 10 | }, 11 | { 12 | "path": "/*", 13 | "query": null, 14 | "select": "meta[property=\"og:description\"]", 15 | "term": "description" 16 | }, 17 | { 18 | "path": "/*", 19 | "query": null, 20 | "select": "meta[property=\"og:locale\"]", 21 | "term": "language" 22 | }, 23 | { 24 | "path": "/*", 25 | "query": null, 26 | "select": "footer", 27 | "term": "publisher" 28 | }, 29 | { 30 | "path": "/*", 31 | "query": null, 32 | "select": "meta[property=\"og:title\"]", 33 | "term": "title" 34 | }, 35 | { 36 | "path": "/*", 37 | "query": null, 38 | "select": "meta[property=\"og:type\"]", 39 | "term": "type" 40 | } 41 | ] 42 | } 43 | ] 44 | -------------------------------------------------------------------------------- /browser/design/sheet/Site.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Element } from '../Element.mjs'; 3 | import { Widget } from '../Widget.mjs'; 4 | 5 | 6 | 7 | const update = function(/* tab, tabs */) { 8 | // TODO: Update according to tab meta info 9 | }; 10 | 11 | 12 | const Site = function(browser) { 13 | 14 | this.element = new Element('browser-sheet-site', [ 15 | '
', 16 | '\t

Site Modes

', 17 | '
', 18 | '
', 19 | '\t

Site Beacons

', 20 | '
', 21 | '
', 22 | '\t

Site Echoes

', 23 | '
' 24 | ].join('')); 25 | 26 | 27 | this.element.on('contextmenu', (e) => { 28 | e.preventDefault(); 29 | e.stopPropagation(); 30 | }); 31 | 32 | this.element.on('show', () => { 33 | this.element.state('active'); 34 | }); 35 | 36 | this.element.on('hide', () => { 37 | this.element.state(''); 38 | }); 39 | 40 | 41 | browser.on('show', (tab) => update.call(this, tab)); 42 | browser.on('refresh', (tab) => update.call(this, tab)); 43 | 44 | 45 | Widget.call(this); 46 | 47 | }; 48 | 49 | 50 | Site.prototype = Object.assign({}, Widget.prototype); 51 | 52 | 53 | export { Site }; 54 | 55 | -------------------------------------------------------------------------------- /stealth/source/client/service/Blocker.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Emitter, isFunction, isObject } from '../../../extern/base.mjs'; 3 | 4 | 5 | 6 | const Blocker = function(client) { 7 | 8 | this.client = client; 9 | Emitter.call(this); 10 | 11 | }; 12 | 13 | 14 | Blocker.prototype = Object.assign({}, Emitter.prototype, { 15 | 16 | toJSON: function() { 17 | 18 | let blob = Emitter.prototype.toJSON.call(this); 19 | let data = { 20 | events: blob.data.events, 21 | journal: blob.data.journal 22 | }; 23 | 24 | return { 25 | 'type': 'Blocker Service', 26 | 'data': data 27 | }; 28 | 29 | }, 30 | 31 | read: function(payload, callback) { 32 | 33 | payload = isObject(payload) ? payload : null; 34 | callback = isFunction(callback) ? callback : null; 35 | 36 | 37 | if (payload !== null && callback !== null) { 38 | 39 | this.once('read', (response) => callback(response)); 40 | 41 | this.client.send({ 42 | headers: { 43 | service: 'blocker', 44 | method: 'read' 45 | }, 46 | payload: payload 47 | }); 48 | 49 | } else if (callback !== null) { 50 | callback(null); 51 | } 52 | 53 | } 54 | 55 | }); 56 | 57 | 58 | export { Blocker }; 59 | 60 | -------------------------------------------------------------------------------- /browser/internal/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | stealth:settings 6 | 30 | 31 | 32 |
33 |

Browser Settings

34 |

35 | Here you can configure all Browser Settings that are stored 36 | in your Profile folder. On this machine, the Stealth Profile 37 | folder is located at: 38 |

39 |

40 | /home/$USER/Stealth 41 |

42 |
43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /base/review/index.mjs: -------------------------------------------------------------------------------- 1 | 2 | import _console from './console.mjs'; 3 | import _Array from './Array.mjs'; 4 | import _Boolean from './Boolean.mjs'; 5 | import _Buffer from './Buffer.mjs'; 6 | import _Date from './Date.mjs'; 7 | import Emitter from './Emitter.mjs'; 8 | import _Function from './Function.mjs'; 9 | import _Map from './Map.mjs'; 10 | import MODULE from './MODULE.mjs'; 11 | import _Number from './Number.mjs'; 12 | import _Object from './Object.mjs'; 13 | import _RegExp from './RegExp.mjs'; 14 | import _Set from './Set.mjs'; 15 | import _String from './String.mjs'; 16 | 17 | 18 | 19 | export const REVIEWS = [ 20 | 21 | MODULE, 22 | 23 | _console, 24 | 25 | _Array, 26 | _Boolean, 27 | _Buffer, 28 | _Date, 29 | Emitter, 30 | _Function, 31 | _Map, 32 | _Number, 33 | _Object, 34 | _RegExp, 35 | _Set, 36 | _String 37 | 38 | ]; 39 | 40 | export const SOURCES = { 41 | 42 | // Map source.id to review.id 43 | 'base/browser/Buffer': 'base/Buffer', 44 | 'base/browser/console': 'base/console', 45 | 'base/node/Buffer': 'base/Buffer', 46 | 'base/node/console': 'base/console', 47 | 48 | // Map review.id to source.id 49 | 'base/Buffer': 'base/browser/Buffer', 50 | 'base/console': 'base/node/console' 51 | 52 | }; 53 | 54 | -------------------------------------------------------------------------------- /browser/design/sheet/Console.css: -------------------------------------------------------------------------------- 1 | 2 | base-console base-console-line { 3 | font-family: 'vera-mono' !important; 4 | } 5 | 6 | browser-sheet-console { 7 | position: fixed; 8 | top: auto; 9 | right: auto; 10 | bottom: 1px; 11 | left: 1px; 12 | } 13 | 14 | browser-sheet-console button { 15 | display: inline-block; 16 | min-width: 32px; 17 | margin: 0px; 18 | padding: 0px; 19 | line-height: 32px; 20 | height: 32px; 21 | color: var(--element-default-color); 22 | font-size: 24px; 23 | text-align: center; 24 | background: var(--layout-default-background); 25 | border: 1px solid transparent; 26 | border-radius: 4px; 27 | box-sizing: border-box; 28 | transition: 200ms all ease-out; 29 | vertical-align: middle; 30 | user-select: none; 31 | -webkit-user-select: none; 32 | appearance: none; 33 | -moz-appearance: none; 34 | -webkit-appearance: none; 35 | outline: none; 36 | cursor: pointer; 37 | } 38 | 39 | browser-sheet-console button:hover, 40 | browser-sheet-console button.active { 41 | color: var(--element-active-color); 42 | border: 1px solid var(--element-focus-color); 43 | } 44 | 45 | browser-sheet-console button:before { 46 | display: inline-block; 47 | width: 100%; 48 | font-family: 'icon'; 49 | text-align: center; 50 | speak: none; 51 | -webkit-font-smoothing: antialiased; 52 | content: '\e1b0'; 53 | } 54 | 55 | -------------------------------------------------------------------------------- /base/source/Object.mjs: -------------------------------------------------------------------------------- 1 | 2 | export const Object = (typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : this)).Object; 3 | 4 | if (typeof Object.isObject !== 'function') { 5 | 6 | Object.isObject = function(obj) { 7 | return Object.prototype.toString.call(obj) === '[object Object]'; 8 | }; 9 | 10 | } 11 | 12 | if (typeof Object.clone !== 'function') { 13 | 14 | Object.clone = function(target) { 15 | 16 | target = target instanceof Object ? target : {}; 17 | 18 | 19 | for (let a = 1, al = arguments.length; a < al; a++) { 20 | 21 | let source = arguments[a]; 22 | if (source) { 23 | 24 | for (let prop in source) { 25 | 26 | if (Object.prototype.hasOwnProperty.call(source, prop) === true) { 27 | 28 | let other_value = source[prop]; 29 | if (other_value instanceof Array) { 30 | 31 | target[prop] = []; 32 | Object.clone(target[prop], source[prop]); 33 | 34 | } else if (other_value instanceof Object) { 35 | 36 | target[prop] = {}; 37 | Object.clone(target[prop], source[prop]); 38 | 39 | } else { 40 | 41 | target[prop] = source[prop]; 42 | 43 | } 44 | 45 | } 46 | 47 | } 48 | 49 | } 50 | 51 | } 52 | 53 | 54 | return target; 55 | 56 | }; 57 | 58 | } 59 | 60 | export const isObject = Object.isObject; 61 | 62 | -------------------------------------------------------------------------------- /stealth/source/optimizer/HTML.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { isFunction, isObject } from '../../extern/base.mjs'; 3 | 4 | 5 | 6 | const Optimizer = { 7 | 8 | check: function(ref, config, callback) { 9 | 10 | ref = isObject(ref) ? ref : null; 11 | config = isObject(config) ? config : null; 12 | callback = isFunction(callback) ? callback : null; 13 | 14 | 15 | if (ref !== null && config !== null && callback !== null) { 16 | 17 | let path = ref.path || null; 18 | if (path !== null && path.endsWith('.html')) { 19 | callback(true); 20 | } else { 21 | callback(false); 22 | } 23 | 24 | } else if (ref !== null && config !== null) { 25 | 26 | let path = ref.path || null; 27 | if (path !== null && path.endsWith('.html')) { 28 | return true; 29 | } else { 30 | return false; 31 | } 32 | 33 | } else if (callback !== null) { 34 | callback(false); 35 | } else { 36 | return false; 37 | } 38 | 39 | }, 40 | 41 | optimize: function(ref, config, callback) { 42 | 43 | ref = isObject(ref) ? ref : null; 44 | config = isObject(config) ? config : null; 45 | callback = isFunction(callback) ? callback : null; 46 | 47 | } 48 | 49 | }; 50 | 51 | 52 | export const check = Optimizer.check; 53 | export const optimize = Optimizer.optimize; 54 | 55 | export { Optimizer }; 56 | 57 | -------------------------------------------------------------------------------- /stealth/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "@babel/eslint-parser", 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "sourceType": "module", 12 | "requireConfigFile": false 13 | }, 14 | "overrides": [{ 15 | "files": [ "*.mjs" ], 16 | "rules": {} 17 | }, { 18 | "files": [ "extern/base.mjs" ], 19 | "rules": { 20 | "no-control-regex": "off", 21 | "no-undef": "off" 22 | } 23 | }], 24 | "rules": { 25 | "no-restricted-globals": [ 26 | "error", 27 | { "name": "__dirname" }, 28 | { "name": "__filename" }, 29 | { "name": "Buffer" }, 30 | { "name": "console" }, 31 | { "name": "exports" }, 32 | { "name": "module" }, 33 | { "name": "process" }, 34 | { "name": "require" }, 35 | { "name": "TextDecoder" }, 36 | { "name": "TextEncoder" }, 37 | { "name": "URL" }, 38 | { "name": "URLSearchParams" } 39 | ], 40 | "arrow-parens": [ 41 | "error", 42 | "always" 43 | ], 44 | "indent": [ 45 | "error", 46 | "tab" 47 | ], 48 | "linebreak-style": [ 49 | "error", 50 | "unix" 51 | ], 52 | "quotes": [ 53 | "error", 54 | "single" 55 | ], 56 | "semi": [ 57 | "error", 58 | "always" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /stealth/package/archlinux/PKGBUILD: -------------------------------------------------------------------------------- 1 | 2 | pkgname=tholian-stealth 3 | pkgver=VERSION 4 | pkgrel=1 5 | pkgdesc="Secure, Peer-to-Peer, Private and Automateable Web Browser/Scraper/Proxy" 6 | arch=(any) 7 | url="https://tholian.network/index.html" 8 | license=("GPL") 9 | depends=("nodejs") 10 | optdepends=("systemd: System Daemon Integration") 11 | conflicts=("tholian-browser") 12 | 13 | 14 | 15 | package() { 16 | 17 | mkdir -p "$pkgdir/usr/bin"; 18 | mkdir -p "$pkgdir/usr/lib/systemd/system"; 19 | mkdir -p "$pkgdir/usr/lib/tholian/browser"; 20 | mkdir -p "$pkgdir/usr/lib/tholian/stealth"; 21 | mkdir -p "$pkgdir/usr/lib/tholian/stealth/vendor/cache/headers"; 22 | mkdir -p "$pkgdir/usr/lib/tholian/stealth/vendor/cache/payload"; 23 | 24 | cp -r $srcdir/browser/* "$pkgdir/usr/lib/tholian/browser/"; 25 | find "$pkgdir/usr/lib/tholian/browser" -type d -exec chmod 755 {} \; 26 | find "$pkgdir/usr/lib/tholian/browser" -type f -exec chmod 644 {} \; 27 | 28 | cp -r $srcdir/stealth/* "$pkgdir/usr/lib/tholian/stealth/"; 29 | find "$pkgdir/usr/lib/tholian/stealth" -type d -exec chmod 755 {} \; 30 | find "$pkgdir/usr/lib/tholian/stealth" -type f -exec chmod 644 {} \; 31 | 32 | install -Dm755 "$srcdir/tholian-stealth.mjs" "$pkgdir/usr/bin/tholian-stealth"; 33 | install -Dm644 "$srcdir/tholian-stealth.service" "$pkgdir/usr/lib/systemd/system/tholian-stealth.service"; 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /base/index.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | console, 4 | Array, 5 | Boolean, 6 | Buffer, 7 | Date, 8 | Emitter, 9 | Function, 10 | Map, 11 | Number, 12 | Object, 13 | RegExp, 14 | Set, 15 | String 16 | } from './build/node.mjs'; 17 | 18 | export { 19 | console, 20 | Array, isArray, 21 | Boolean, isBoolean, 22 | Buffer, isBuffer, 23 | Date, isDate, 24 | Emitter, isEmitter, 25 | Function, isFunction, 26 | Map, isMap, 27 | Number, isNumber, 28 | Object, isObject, 29 | RegExp, isRegExp, 30 | Set, isSet, 31 | String, isString 32 | } from './build/node.mjs'; 33 | 34 | 35 | 36 | export default { 37 | 38 | console: console, 39 | 40 | Array: Array, 41 | Boolean: Boolean, 42 | Buffer: Buffer, 43 | Date: Date, 44 | Emitter: Emitter, 45 | Function: Function, 46 | Map: Map, 47 | Number: Number, 48 | Object: Object, 49 | RegExp: RegExp, 50 | Set: Set, 51 | String: String, 52 | 53 | isArray: Array.isArray, 54 | isBoolean: Boolean.isBoolean, 55 | isBuffer: Buffer.isBuffer, 56 | isDate: Date.isDate, 57 | isEmitter: Emitter.isEmitter, 58 | isFunction: Function.isFunction, 59 | isMap: Map.isMap, 60 | isNumber: Number.isNumber, 61 | isObject: Object.isObject, 62 | isRegExp: RegExp.isRegExp, 63 | isSet: Set.isSet, 64 | isString: String.isString 65 | 66 | }; 67 | 68 | -------------------------------------------------------------------------------- /base/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "@babel/eslint-parser", 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "sourceType": "module", 12 | "requireConfigFile": false 13 | }, 14 | "overrides": [{ 15 | "files": [ "*.mjs" ], 16 | "rules": {} 17 | }, { 18 | "files": [ "build/browser.mjs" ], 19 | "rules": { 20 | "no-undef": "off" 21 | } 22 | }, { 23 | "files": [ "build/node.mjs" ], 24 | "globals": { 25 | "global": true, 26 | "window": true 27 | }, 28 | "rules": { 29 | "no-control-regex": "off", 30 | "no-undef": "off" 31 | } 32 | }, { 33 | "files": [ "source/node/console.mjs" ], 34 | "rules": { "no-control-regex": "off" } 35 | }, { 36 | "files": [ "source/MODULE.mjs" ], 37 | "globals": { 38 | "console": true, 39 | "Buffer": true, 40 | "Emitter": true 41 | } 42 | }], 43 | "rules": { 44 | "arrow-parens": [ 45 | "error", 46 | "always" 47 | ], 48 | "indent": [ 49 | "error", 50 | "tab" 51 | ], 52 | "linebreak-style": [ 53 | "error", 54 | "unix" 55 | ], 56 | "no-unused-vars": [ 57 | "error", { 58 | "argsIgnorePattern": "^(assert)$" 59 | } 60 | ], 61 | "quotes": [ 62 | "error", 63 | "single" 64 | ], 65 | "semi": [ 66 | "error", 67 | "always" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /browser/design/widget/Image.css: -------------------------------------------------------------------------------- 1 | 2 | browser-widget-image { 3 | all: unset; 4 | display: block; 5 | position: relative; 6 | width: 800px; 7 | margin: 0px; 8 | padding: 16px; 9 | background: var(--surface-default-background); 10 | border: 1px solid var(--surface-default-color); 11 | border-radius: 8px; 12 | box-sizing: border-box; 13 | transition: 200ms all ease-out; 14 | overflow: hidden; 15 | cursor: default; 16 | } 17 | 18 | 19 | 20 | browser-widget-image-article { 21 | display: block; 22 | position: relative; 23 | width: 100%; 24 | height: auto; 25 | text-align: center; 26 | overflow: hidden; 27 | } 28 | 29 | browser-widget-image-article img { 30 | display: block; 31 | width: auto; 32 | max-width: 100%; 33 | height: auto; 34 | max-height: 100%; 35 | margin: 0px auto; 36 | padding: 0px; 37 | } 38 | 39 | 40 | 41 | browser-widget-image-footer { 42 | display: block; 43 | height: 32px; 44 | margin: 16px 0px 0px 0px; 45 | padding: 0px; 46 | text-align: right; 47 | } 48 | 49 | browser-widget-image-footer > button { 50 | margin-left: 16px; 51 | } 52 | 53 | browser-widget-image-footer > button[data-action="download"]:before { content: '\e2c4'; } 54 | browser-widget-image-footer > button[data-action="fullscreen"]:before { content: '\e5d0'; } 55 | 56 | 57 | 58 | @media screen and (max-width: 800px) { 59 | 60 | browser-widget-image { 61 | width: auto; 62 | } 63 | 64 | } 65 | 66 | -------------------------------------------------------------------------------- /browser/package/archlinux/PKGBUILD: -------------------------------------------------------------------------------- 1 | 2 | pkgname=tholian-browser 3 | pkgver=VERSION 4 | pkgrel=1 5 | pkgdesc="Secure, Peer-to-Peer, Private and Automateable Web Browser/Scraper/Proxy" 6 | arch=(any) 7 | url="https://tholian.network/index.html" 8 | license=("GPL") 9 | depends=("electron") 10 | conflicts=("tholian-stealth") 11 | 12 | 13 | 14 | package() { 15 | 16 | mkdir -p "$pkgdir/usr/bin"; 17 | mkdir -p "$pkgdir/usr/lib/tholian/browser"; 18 | mkdir -p "$pkgdir/usr/lib/tholian/stealth"; 19 | mkdir -p "$pkgdir/usr/lib/tholian/stealth/vendor/cache/headers"; 20 | mkdir -p "$pkgdir/usr/lib/tholian/stealth/vendor/cache/payload"; 21 | mkdir -p "$pkgdir/usr/share/applications"; 22 | 23 | cp -r $srcdir/browser/* "$pkgdir/usr/lib/tholian/browser/"; 24 | find "$pkgdir/usr/lib/tholian/browser" -type d -exec chmod 755 {} \; 25 | find "$pkgdir/usr/lib/tholian/browser" -type f -exec chmod 644 {} \; 26 | 27 | cp -r $srcdir/stealth/* "$pkgdir/usr/lib/tholian/stealth/"; 28 | find "$pkgdir/usr/lib/tholian/stealth" -type d -exec chmod 755 {} \; 29 | find "$pkgdir/usr/lib/tholian/stealth" -type f -exec chmod 644 {} \; 30 | 31 | install -Dm644 "$srcdir/tholian-browser.desktop" "$pkgdir/usr/share/applications/tholian-browser.desktop"; 32 | install -Dm755 "$srcdir/tholian-browser.js" "$pkgdir/usr/bin/tholian-browser"; 33 | install -Dm644 "$srcdir/tholian-browser.svg" "$pkgdir/usr/share/icons/tholian-browser.svg"; 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /browser/design/sheet/Session.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { console } from '../../extern/base.mjs'; 3 | import { Element } from '../Element.mjs'; 4 | import { Widget } from '../Widget.mjs'; 5 | 6 | 7 | 8 | const Session = function(browser) { 9 | 10 | this.element = new Element('browser-sheet-session', [ 11 | '
', 12 | '\t

Sessions

', 13 | '
', 14 | '
', 15 | '\t

Tabs

', 16 | '
', 17 | ]); 18 | 19 | this.session = null; 20 | this.sessions = []; 21 | 22 | 23 | this.element.on('contextmenu', (e) => { 24 | e.preventDefault(); 25 | e.stopPropagation(); 26 | }); 27 | 28 | this.element.on('show', () => { 29 | 30 | browser.client.services.session.read({}, (session) => { 31 | 32 | browser.client.services.session.query({ 33 | domain: '*' 34 | }, (sessions) => { 35 | 36 | // Make sure reference is to same Array 37 | session = sessions.find((s) => s.domain === session.domain) || null; 38 | 39 | this.session = session.domain; 40 | this.sessions = sessions; 41 | 42 | console.info(session); 43 | console.warn(sessions); 44 | 45 | }); 46 | 47 | 48 | }); 49 | 50 | this.element.state('active'); 51 | 52 | }); 53 | 54 | this.element.on('hide', () => { 55 | this.element.state(''); 56 | }); 57 | 58 | 59 | Widget.call(this); 60 | 61 | }; 62 | 63 | 64 | Session.prototype = Object.assign({}, Widget.prototype); 65 | 66 | 67 | export { Session }; 68 | 69 | -------------------------------------------------------------------------------- /stealth/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Stealth Service 3 | 4 | The Stealth Service is a web daemon that allows to load, parse, filter 5 | and render web sites from both a local cache and the interwebz. 6 | 7 | The idea is that the [Browser UI](../browser) is also delivered by 8 | the Stealth Service, so that other platforms (like Android or iOS 9 | tablets and smartphones) can simply access a Stealth instance by 10 | visiting it with any modern Web Browser and to use the Progressive 11 | Web App in fullscreen mode. 12 | 13 | 14 | ## Quickstart 15 | 16 | - Install [node.js](https://nodejs.org/en/download) latest (minimum version `12`). 17 | 18 | ```bash 19 | cd /path/to/stealth; 20 | 21 | # Build 22 | node ./stealth/make.mjs; 23 | 24 | # Show Help 25 | node ./stealth/stealth.mjs; 26 | ``` 27 | 28 | 29 | ## Building and Packaging 30 | 31 | ```bash 32 | cd /path/to/stealth; 33 | 34 | # Build 35 | node ./stealth/make.mjs /path/to/sandbox; 36 | 37 | # Package 38 | node ./stealth/make.mjs pack /path/to/sandbox; 39 | ``` 40 | 41 | 42 | ## ECMAScript Usage 43 | 44 | The Stealth Library exports a `default` export that contains a namespaced 45 | `Object`. Due to otherwise conflicting names, there are no separately 46 | named exports available. 47 | 48 | ```javascript 49 | // Import Stealth Library 50 | import stealth from './stealth/index.mjs'; 51 | ``` 52 | 53 | 54 | ## NPM Usage 55 | 56 | ```javascript 57 | // Import Stealth Library 58 | import stealth from 'stealth/stealth'; 59 | ``` 60 | 61 | -------------------------------------------------------------------------------- /covert/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "@babel/eslint-parser", 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "sourceType": "module", 12 | "requireConfigFile": false 13 | }, 14 | "overrides": [{ 15 | "files": [ "*.mjs" ], 16 | "rules": {} 17 | }, { 18 | "files": [ "extern/base.mjs" ], 19 | "rules": { 20 | "no-control-regex": "off", 21 | "no-undef": "off" 22 | } 23 | }], 24 | "rules": { 25 | "no-restricted-globals": [ 26 | "error", 27 | { "name": "__dirname" }, 28 | { "name": "__filename" }, 29 | { "name": "Buffer" }, 30 | { "name": "console" }, 31 | { "name": "exports" }, 32 | { "name": "module" }, 33 | { "name": "process" }, 34 | { "name": "require" }, 35 | { "name": "TextDecoder" }, 36 | { "name": "TextEncoder" }, 37 | { "name": "URL" }, 38 | { "name": "URLSearchParams" } 39 | ], 40 | "arrow-parens": [ 41 | "error", 42 | "always" 43 | ], 44 | "indent": [ 45 | "error", 46 | "tab" 47 | ], 48 | "linebreak-style": [ 49 | "error", 50 | "unix" 51 | ], 52 | "no-unused-vars": [ 53 | "error", { 54 | "argsIgnorePattern": "^(assert)$" 55 | } 56 | ], 57 | "quotes": [ 58 | "error", 59 | "single" 60 | ], 61 | "semi": [ 62 | "error", 63 | "always" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /base/review/Set.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { describe, finish } from '../../covert/index.mjs'; 3 | import { Set, isSet } from '../source/Set.mjs'; 4 | 5 | 6 | 7 | describe('Set.isSet()', function(assert) { 8 | 9 | let set1 = new Set(); 10 | let set2 = new Set([ 1, 3, 3, 7 ]); 11 | 12 | assert(typeof Set.isSet, 'function'); 13 | 14 | assert(Set.isSet(set1), true); 15 | assert(Set.isSet(set2), true); 16 | 17 | }); 18 | 19 | describe('isSet()', function(assert) { 20 | 21 | let set1 = new Set(); 22 | let set2 = new Set([ 1, 3, 3, 7 ]); 23 | 24 | assert(typeof isSet, 'function'); 25 | 26 | assert(isSet(set1), true); 27 | assert(isSet(set2), true); 28 | 29 | }); 30 | 31 | describe('Set.prototype.toString()', function(assert) { 32 | 33 | let set1 = new Set(); 34 | let set2 = new Set([ 1, 3, 3, 7 ]); 35 | 36 | assert(Object.prototype.toString.call(set1), '[object Set]'); 37 | assert(Object.prototype.toString.call(set2), '[object Set]'); 38 | 39 | assert((set1).toString(), '[object Set]'); 40 | assert((set2).toString(), '[object Set]'); 41 | 42 | }); 43 | 44 | describe('Set.prototype.valueOf()', function(assert) { 45 | 46 | let set1 = new Set(); 47 | let set2 = new Set([ 1, 3, 3, 7 ]); 48 | 49 | assert((set1).valueOf(), new Set()); 50 | assert((set2).valueOf(), new Set([ 1, 3, 7 ])); 51 | 52 | assert(JSON.stringify(set1), '{}'); 53 | assert(JSON.stringify(set2), '{}'); 54 | 55 | }); 56 | 57 | 58 | export default finish('base/Set', { 59 | internet: false, 60 | network: false 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stealth", 3 | "description": "Tholian® Stealth - Secure, Peer-to-Peer, Private and Automatable Web Browser/Scraper/Proxy", 4 | "private": true, 5 | "version": "X0:2021-11-11", 6 | "module": "./stealth/index.mjs", 7 | "bin": { 8 | "browser": "./browser/browser.mjs", 9 | "covert": "./covert/covert.mjs", 10 | "stealth": "./stealth/stealth.mjs" 11 | }, 12 | "scripts": { 13 | "lint": "npx eslint ./base ./browser ./covert ./stealth", 14 | "postinstall": "node ./make.mjs build", 15 | "clean": "node ./make.mjs clean", 16 | "clean:base": "node ./base/make.mjs clean", 17 | "clean:browser": "node ./browser/make.mjs clean", 18 | "clean:covert": "node ./covert/make.mjs clean", 19 | "clean:stealth": "node ./stealth/make.mjs clean", 20 | "build": "node ./make.mjs build", 21 | "build:base": "node ./base/make.mjs build", 22 | "build:browser": "node ./browser/make.mjs build", 23 | "build:covert": "node ./covert/make.mjs build", 24 | "build:stealth": "node ./stealth/make.mjs build" 25 | }, 26 | "exports": { 27 | "./base": { 28 | "import": "./base/index.mjs", 29 | "browser": "./base/build/browser.mjs", 30 | "node": "./base/build/node.mjs" 31 | }, 32 | "./browser": { 33 | "browser": "./browser/index.mjs" 34 | }, 35 | "./covert": { 36 | "import": "./covert/index.mjs" 37 | }, 38 | "./stealth": { 39 | "import": "./stealth/index.mjs" 40 | } 41 | }, 42 | "devDependencies": { 43 | "@babel/eslint-parser": "^7.15.8", 44 | "eslint": "^8.1.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /browser/design/common/index.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face { 3 | font-family: 'icon'; 4 | src: url('icon.woff') format('woff'); 5 | font-variant: normal; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | @font-face { 11 | font-family: 'crystalline'; 12 | src: url('crystalline-light.woff') format('woff'); 13 | font-variant: normal; 14 | font-weight: 250; 15 | font-style: normal; 16 | } 17 | 18 | @font-face { 19 | font-family: 'crystalline'; 20 | src: url('crystalline-regular.woff') format('woff'); 21 | font-variant: normal; 22 | font-weight: normal; 23 | font-style: normal; 24 | } 25 | 26 | @font-face { 27 | font-family: 'crystalline'; 28 | src: url('crystalline-medium.woff') format('woff'); 29 | font-variant: normal; 30 | font-weight: 500; 31 | font-style: normal; 32 | } 33 | 34 | @font-face { 35 | font-family: 'crystalline'; 36 | src: url('crystalline-bold.woff') format('woff'); 37 | font-variant: normal; 38 | font-weight: 700; 39 | font-style: normal; 40 | } 41 | 42 | @font-face { 43 | font-family: 'museo'; 44 | src: url('museo-regular.woff') format('woff'); 45 | font-variant: normal; 46 | font-weight: normal; 47 | font-style: normal; 48 | } 49 | 50 | @font-face { 51 | font-family: 'museo'; 52 | src: url('museo-medium.woff') format('woff'); 53 | font-variant: normal; 54 | font-weight: 500; 55 | font-style: normal; 56 | } 57 | 58 | @font-face { 59 | font-family: 'vera-mono'; 60 | src: url('vera-mono.woff') format('woff'); 61 | font-variant: normal; 62 | font-weight: normal; 63 | font-style: normal; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /base/review/Number.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { describe, finish } from '../../covert/index.mjs'; 3 | import { Number, isNumber } from '../source/Number.mjs'; 4 | 5 | 6 | 7 | describe('Number.isNumber()', function(assert) { 8 | 9 | let number1 = 13.37; 10 | let number2 = new Number(13.37); 11 | 12 | assert(typeof Number.isNumber, 'function'); 13 | 14 | assert(Number.isNumber(number1), true); 15 | assert(Number.isNumber(number2), true); 16 | 17 | }); 18 | 19 | describe('isNumber()', function(assert) { 20 | 21 | let number1 = 13.37; 22 | let number2 = new Number(13.37); 23 | 24 | assert(typeof isNumber, 'function'); 25 | 26 | assert(isNumber(number1), true); 27 | assert(isNumber(number2), true); 28 | 29 | }); 30 | 31 | describe('Number.prototype.toString()', function(assert) { 32 | 33 | let number1 = 13.37; 34 | let number2 = new Number(13.37); 35 | 36 | assert(Object.prototype.toString.call(number1), '[object Number]'); 37 | assert(Object.prototype.toString.call(number2), '[object Number]'); 38 | 39 | assert((number1).toString(), '13.37'); 40 | assert((number2).toString(), '13.37'); 41 | 42 | }); 43 | 44 | describe('Number.prototype.valueOf()', function(assert) { 45 | 46 | let number1 = 13.37; 47 | let number2 = new Number(13.37); 48 | 49 | assert((number1).valueOf(), 13.37); 50 | assert((number2).valueOf(), 13.37); 51 | 52 | assert(JSON.stringify(number1), '13.37'); 53 | assert(JSON.stringify(number2), '13.37'); 54 | 55 | }); 56 | 57 | 58 | export default finish('base/Number', { 59 | internet: false, 60 | network: false 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /covert/index.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { after, before, describe, finish } from './source/Review.mjs'; 3 | import { Covert, isCovert } from './source/Covert.mjs'; 4 | import { ENVIRONMENT } from './source/ENVIRONMENT.mjs'; 5 | import { Filesystem, isFilesystem } from './source/Filesystem.mjs'; 6 | import { Linter, isLinter } from './source/Linter.mjs'; 7 | import { Network, isNetwork } from './source/Network.mjs'; 8 | import { Renderer, isRenderer } from './source/Renderer.mjs'; 9 | import { Results, isResults } from './source/Results.mjs'; 10 | import { Review, isReview } from './source/Review.mjs'; 11 | import { Timeline, isTimeline } from './source/Timeline.mjs'; 12 | 13 | export { after, before, describe, finish } from './source/Review.mjs'; 14 | export { ENVIRONMENT } from './source/ENVIRONMENT.mjs'; 15 | 16 | 17 | 18 | export default { 19 | 20 | after, 21 | before, 22 | describe, 23 | finish, 24 | 25 | isCovert: isCovert, 26 | isFilesystem: isFilesystem, 27 | isLinter: isLinter, 28 | isNetwork: isNetwork, 29 | isRenderer: isRenderer, 30 | isResults: isResults, 31 | isReview: isReview, 32 | isTimeline: isTimeline, 33 | 34 | Covert: Covert, 35 | ENVIRONMENT: ENVIRONMENT, 36 | Filesystem: Filesystem, 37 | Linter: Linter, 38 | Network: Network, 39 | Renderer: Renderer, 40 | Results: Results, 41 | Review: Review, 42 | Timeline: Timeline 43 | 44 | }; 45 | 46 | -------------------------------------------------------------------------------- /guide/security/Network.md: -------------------------------------------------------------------------------- 1 | 2 | # Network Security Guide 3 | 4 | (This Document is Work in Progress) 5 | 6 | The Stealth Service allows to relay traffic with multiple mechanisms, 7 | and allows to share network connections with its Peers in a local 8 | manner and/or global manner, so it's hard to get a hold of it in terms 9 | of networked state complexity or whether or not it can be abused to 10 | identify an end-user uniquely. 11 | 12 | Every Stealth Service also contains: 13 | 14 | - An HTTP/S Proxy (that supports `CONNECT` and `GET`) on port `65432`. 15 | - A Webserver that serves the Browser UI at port `65432` and path `/browser/*`. 16 | - A Websocket Services that serves the Peer-to-Peer API on port `65432`. 17 | - A Multicast DNS-SD Service that interacts with other local Stealth Peers on port `5353`. 18 | - A DNS Router that can resolve DNS Requests for other Peers on port `65432`. 19 | - A SOCKS Proxy running on port `65432`. 20 | 21 | 22 | ## Attack Vector: TCP/UDP Manipulation 23 | 24 | ## Attack Vector: TCP/UDP Fingerprinting 25 | 26 | ## Attack Vector: NAT Blocking 27 | 28 | ## Attack Vector: DHT / Radar Access Blocking 29 | 30 | ## Attack Vector: DNS Manipulation 31 | 32 | ## Attack Vector: DNS Tracking 33 | 34 | ## Attack Vector: HTTP Downgrade Attack 35 | 36 | ## Attack Vector: TLS Downgrade Attack(s) 37 | 38 | ## Attack Vector: TLS Timing/Side-Channel Attack(s) 39 | 40 | ## Attack Vector: HTTP/S Traffic Correlation Tracking 41 | 42 | ## Attack Vector: Multicast DNS-SD Manipulation 43 | 44 | ## Attack Vector: Multicast DNS-SD Tracking 45 | 46 | -------------------------------------------------------------------------------- /browser/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": false 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "@babel/eslint-parser", 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "sourceType": "module", 12 | "requireConfigFile": false 13 | }, 14 | "globals": { 15 | "Buffer": true 16 | }, 17 | "overrides": [{ 18 | "files": [ "*.mjs" ], 19 | "rules": {} 20 | }, { 21 | "files": [ "app/index.js" ], 22 | "env": { 23 | "node": true 24 | }, 25 | "globals": { 26 | "require": true 27 | } 28 | }, { 29 | "files": [ "app/console.js" ], 30 | "env": { 31 | "node": true 32 | } 33 | }, { 34 | "files": [ "service.js" ], 35 | "rules": { 36 | "no-control-regex": "off", 37 | "no-console": "off", 38 | "no-restricted-globals": "off" 39 | } 40 | }, { 41 | "files": [ "extern/base.mjs" ], 42 | "rules": { 43 | "no-undef": "off" 44 | } 45 | }], 46 | "rules": { 47 | "no-restricted-globals": [ 48 | "error", 49 | { "name": "console" }, 50 | { "name": "location" }, 51 | { "name": "URL" }, 52 | { "name": "URLSearchParams" } 53 | ], 54 | "arrow-parens": [ 55 | "error", 56 | "always" 57 | ], 58 | "indent": [ 59 | "error", 60 | "tab" 61 | ], 62 | "linebreak-style": [ 63 | "error", 64 | "unix" 65 | ], 66 | "no-unused-vars": [ 67 | "error", { 68 | "varsIgnorePattern": "^(BROWSER|ELEMENTS|WIDGETS)$" 69 | } 70 | ], 71 | "quotes": [ 72 | "error", 73 | "single" 74 | ], 75 | "semi": [ 76 | "error", 77 | "always" 78 | ] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /base/review/RegExp.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { describe, finish } from '../../covert/index.mjs'; 3 | import { RegExp, isRegExp } from '../source/RegExp.mjs'; 4 | 5 | 6 | 7 | describe('RegExp.isRegExp()', function(assert) { 8 | 9 | let regexp1 = /foo\/bar/g; 10 | let regexp2 = new RegExp('foo/bar', 'g'); 11 | 12 | assert(typeof RegExp.isRegExp, 'function'); 13 | 14 | assert(RegExp.isRegExp(regexp1), true); 15 | assert(RegExp.isRegExp(regexp2), true); 16 | 17 | }); 18 | 19 | describe('isRegExp()', function(assert) { 20 | 21 | let regexp1 = /foo\/bar/g; 22 | let regexp2 = new RegExp('foo/bar', 'g'); 23 | 24 | assert(typeof isRegExp, 'function'); 25 | 26 | assert(isRegExp(regexp1), true); 27 | assert(isRegExp(regexp2), true); 28 | 29 | }); 30 | 31 | describe('RegExp.prototype.toString()', function(assert) { 32 | 33 | let regexp1 = /foo\/bar/g; 34 | let regexp2 = new RegExp('foo/bar', 'g'); 35 | 36 | assert(Object.prototype.toString.call(regexp1), '[object RegExp]'); 37 | assert(Object.prototype.toString.call(regexp2), '[object RegExp]'); 38 | 39 | assert((regexp1).toString(), '/foo\\/bar/g'); 40 | assert((regexp2).toString(), '/foo\\/bar/g'); 41 | 42 | }); 43 | 44 | describe('RegExp.prototype.valueOf()', function(assert) { 45 | 46 | let regexp1 = /foo\/bar/g; 47 | let regexp2 = new RegExp('foo/bar', 'g'); 48 | 49 | assert((regexp1).valueOf(), regexp1); 50 | assert((regexp2).valueOf(), regexp2); 51 | 52 | assert(JSON.stringify(regexp1), '{}'); 53 | assert(JSON.stringify(regexp2), '{}'); 54 | 55 | }); 56 | 57 | 58 | export default finish('base/RegExp', { 59 | internet: false, 60 | network: false 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /base/source/Array.mjs: -------------------------------------------------------------------------------- 1 | 2 | export const Array = (typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : this)).Array; 3 | 4 | if (typeof Array.isArray !== 'function') { 5 | 6 | Array.isArray = function(arr) { 7 | return Object.prototype.toString.call(arr) === '[object Array]'; 8 | }; 9 | 10 | } 11 | 12 | if (typeof Array.prototype.remove !== 'function') { 13 | 14 | Array.prototype.remove = function(value) { 15 | 16 | let index = this.indexOf(value); 17 | 18 | while (index !== -1) { 19 | 20 | this.splice(index, 1); 21 | 22 | index = this.indexOf(value); 23 | 24 | } 25 | 26 | return this; 27 | 28 | }; 29 | 30 | } 31 | 32 | if (typeof Array.prototype.removeEvery !== 'function') { 33 | 34 | Array.prototype.removeEvery = function(predicate/*, thisArg */) { 35 | 36 | if (this === null || this === undefined) { 37 | throw new TypeError('Array.prototype.removeEvery called on null or undefined'); 38 | } 39 | 40 | if (typeof predicate !== 'function') { 41 | throw new TypeError('predicate must be a function'); 42 | } 43 | 44 | 45 | let list = Object(this); 46 | let length = list.length >>> 0; 47 | let thisArg = arguments.length >= 2 ? arguments[1] : void 0; 48 | let value; 49 | 50 | for (let i = 0; i < length; i++) { 51 | 52 | if (i in list) { 53 | 54 | value = list[i]; 55 | 56 | if (!!predicate.call(thisArg, value, i, list) === true) { 57 | this.splice(i, 1); 58 | length--; 59 | i--; 60 | } 61 | 62 | } 63 | 64 | } 65 | 66 | return this; 67 | 68 | }; 69 | 70 | } 71 | 72 | export const isArray = Array.isArray; 73 | 74 | -------------------------------------------------------------------------------- /browser/internal/debug.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { ENVIRONMENT } from '../source/ENVIRONMENT.mjs'; 3 | import { Element } from '../design/Element.mjs'; 4 | import { Settings } from '../design/card/Settings.mjs'; 5 | import { Browser } from '../internal/debug/Browser.mjs'; 6 | import { URL } from '../source/parser/URL.mjs'; 7 | 8 | 9 | 10 | let browser = window.parent.BROWSER || null; 11 | if (browser !== null) { 12 | 13 | let browser_settings = Browser.from(browser.settings, [ 'tabs', 'sessions' ]); 14 | let site_settings = Settings.from({ domain: '' }, [ 'beacons', 'blockers', 'echos', 'hosts', 'modes', 'peers', 'policies', 'redirects', 'tasks' ]); 15 | 16 | if (URL.isURL(ENVIRONMENT.flags.url) === true) { 17 | 18 | let domain = URL.toDomain(ENVIRONMENT.flags.url); 19 | if (domain !== null) { 20 | site_settings.value({ domain: domain }); 21 | } 22 | 23 | let footer = site_settings.query('browser-card-settings-footer'); 24 | if (footer !== null) { 25 | 26 | let button = new Element('button'); 27 | 28 | button.attr('title', 'Open URL in new Tab'); 29 | button.attr('data-action', 'open'); 30 | 31 | button.on('click', () => { 32 | 33 | let tab = browser.open(ENVIRONMENT.flags.url.link); 34 | if (tab !== null) { 35 | browser.show(tab); 36 | } 37 | 38 | }); 39 | 40 | button.render(footer); 41 | 42 | } 43 | 44 | } 45 | 46 | let body = Element.query('body'); 47 | if (body !== null) { 48 | 49 | site_settings.render(body); 50 | site_settings.emit('show'); 51 | 52 | browser_settings.render(body); 53 | browser_settings.emit('show'); 54 | 55 | } 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /stealth/source/optimizer/CSS.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { isFunction, isObject } from '../../extern/base.mjs'; 3 | 4 | 5 | 6 | const Optimizer = { 7 | 8 | check: function(ref, config, callback) { 9 | 10 | ref = isObject(ref) ? ref : null; 11 | config = isObject(config) ? config : null; 12 | callback = isFunction(callback) ? callback : null; 13 | 14 | 15 | if (ref !== null && config !== null && callback !== null) { 16 | 17 | let path = ref.path || null; 18 | if (path !== null && path.endsWith('.css')) { 19 | callback(true); 20 | } else { 21 | callback(false); 22 | } 23 | 24 | } else if (ref !== null && config !== null) { 25 | 26 | let path = ref.path || null; 27 | if (path !== null && path.endsWith('.css')) { 28 | return true; 29 | } else { 30 | return false; 31 | } 32 | 33 | } else if (callback !== null) { 34 | callback(false); 35 | } else { 36 | return false; 37 | } 38 | 39 | }, 40 | 41 | optimize: function(ref, config, callback) { 42 | 43 | ref = isObject(ref) ? ref : null; 44 | config = isObject(config) ? config : null; 45 | callback = isFunction(callback) ? callback : null; 46 | 47 | 48 | if (ref !== null && config !== null && callback !== null) { 49 | 50 | // TODO: Implement me 51 | 52 | } else if (ref !== null && config !== null) { 53 | 54 | // TODO: Implement me 55 | 56 | } else if (callback !== null) { 57 | callback(null); 58 | } else { 59 | return null; 60 | } 61 | 62 | } 63 | 64 | }; 65 | 66 | 67 | export const check = Optimizer.check; 68 | export const optimize = Optimizer.optimize; 69 | 70 | export { Optimizer }; 71 | 72 | -------------------------------------------------------------------------------- /base/review/String.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { describe, finish } from '../../covert/index.mjs'; 3 | import { String, isString } from '../source/String.mjs'; 4 | 5 | 6 | 7 | describe('String.isString()', function(assert) { 8 | 9 | let string1 = 'example.com'; 10 | let string2 = new String('example.com'); 11 | 12 | assert(typeof String.isString, 'function'); 13 | assert(String.isString, isString); 14 | 15 | assert(String.isString(string1), true); 16 | assert(String.isString(string2), true); 17 | 18 | }); 19 | 20 | describe('isString()', function(assert) { 21 | 22 | let string1 = 'example.com'; 23 | let string2 = new String('example.com'); 24 | 25 | assert(typeof isString, 'function'); 26 | 27 | assert(isString(string1), true); 28 | assert(isString(string2), true); 29 | 30 | }); 31 | 32 | describe('String.prototype.toString()', function(assert) { 33 | 34 | let string1 = 'example.com'; 35 | let string2 = new String('example.com'); 36 | 37 | assert(Object.prototype.toString.call(string1), '[object String]'); 38 | assert(Object.prototype.toString.call(string2), '[object String]'); 39 | 40 | assert((string1).toString(), 'example.com'); 41 | assert((string2).toString(), 'example.com'); 42 | 43 | }); 44 | 45 | describe('String.prototype.valueOf()', function(assert) { 46 | 47 | let string1 = 'example.com'; 48 | let string2 = new String('example.com'); 49 | 50 | assert((string1).valueOf(), 'example.com'); 51 | assert((string2).valueOf(), 'example.com'); 52 | 53 | assert(JSON.stringify(string1), '"example.com"'); 54 | assert(JSON.stringify(string2), '"example.com"'); 55 | 56 | }); 57 | 58 | 59 | export default finish('base/String', { 60 | internet: false, 61 | network: false 62 | }); 63 | 64 | -------------------------------------------------------------------------------- /base/review/Date.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { describe, finish } from '../../covert/index.mjs'; 3 | import { Date, isDate } from '../source/Date.mjs'; 4 | 5 | 6 | 7 | describe('Date.isDate()', function(assert) { 8 | 9 | let date1 = new Date('01.02.20 13:37Z'); 10 | let date2 = new Date('2020-02-01T13:37:00Z'); 11 | 12 | assert(typeof Date.isDate, 'function'); 13 | 14 | assert(Date.isDate(date1), true); 15 | assert(Date.isDate(date2), true); 16 | 17 | }); 18 | 19 | 20 | describe('isDate()', function(assert) { 21 | 22 | let date1 = new Date('01.02.20 13:37Z'); 23 | let date2 = new Date('2020-02-01T13:37:00Z'); 24 | 25 | assert(typeof isDate, 'function'); 26 | 27 | assert(isDate(date1), true); 28 | assert(isDate(date2), true); 29 | 30 | }); 31 | 32 | describe('Date.prototype.toISOString()', function(assert) { 33 | 34 | let date1 = new Date('02.01.2020 13:37Z'); 35 | let date2 = new Date('2020-02-01T13:37:00Z'); 36 | 37 | assert(Object.prototype.toString.call(date1), '[object Date]'); 38 | assert(Object.prototype.toString.call(date2), '[object Date]'); 39 | 40 | assert((date1).toISOString(), '2020-02-01T13:37:00.000Z'); 41 | assert((date2).toISOString(), '2020-02-01T13:37:00.000Z'); 42 | 43 | }); 44 | 45 | describe('Date.prototype.valueOf()', function(assert) { 46 | 47 | let date1 = new Date('03.04.2020 13:37Z'); 48 | let date2 = new Date('2020-03-04T13:37:00Z'); 49 | 50 | assert((date1).valueOf(), 1583329020000); 51 | assert((date2).valueOf(), 1583329020000); 52 | 53 | assert(JSON.stringify(date1), '"2020-03-04T13:37:00.000Z"'); 54 | assert(JSON.stringify(date2), '"2020-03-04T13:37:00.000Z"'); 55 | 56 | }); 57 | 58 | 59 | export default finish('base/Date', { 60 | internet: false, 61 | network: false 62 | }); 63 | 64 | -------------------------------------------------------------------------------- /browser/internal/settings.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { ENVIRONMENT } from '../source/ENVIRONMENT.mjs'; 3 | import { Element } from '../design/Element.mjs'; 4 | import { Widget } from '../design/Widget.mjs'; 5 | import { Interface } from '../design/card/Interface.mjs'; 6 | import { Internet } from '../design/card/Internet.mjs'; 7 | import { Settings } from '../design/card/Settings.mjs'; 8 | 9 | 10 | 11 | const render_widget = (widget) => { 12 | 13 | let body = Element.query('body'); 14 | if (body !== null && widget !== null) { 15 | 16 | widget.render(body); 17 | widget.emit('show'); 18 | 19 | } 20 | 21 | }; 22 | 23 | 24 | 25 | let browser = window.parent.BROWSER || null; 26 | if (browser !== null) { 27 | 28 | let elements = {}; 29 | let widgets = {}; 30 | 31 | 32 | elements['profile'] = Element.query('[data-key="profile"]'); 33 | 34 | if (elements['profile'] !== null) { 35 | 36 | browser.client.services['settings'].info(null, (response) => { 37 | 38 | if (response.profile !== null) { 39 | elements['profile'].value(response.profile); 40 | } 41 | 42 | }); 43 | 44 | } 45 | 46 | 47 | widgets['interface'] = Interface.from(browser.settings['interface']); 48 | widgets['internet'] = Internet.from(browser.settings['internet']); 49 | widgets['settings'] = Settings.from({ domain: '' }, [ 'beacons', 'echos', 'hosts', 'modes', 'policies', 'redirects', 'tasks' ]); 50 | 51 | render_widget(widgets['interface']); 52 | render_widget(widgets['internet']); 53 | render_widget(widgets['settings']); 54 | 55 | 56 | if (ENVIRONMENT.flags.debug === true) { 57 | 58 | window.ELEMENTS = elements; 59 | window.WIDGETS = widgets; 60 | 61 | window.Element = Element; 62 | window.Widget = Widget; 63 | 64 | } 65 | 66 | } 67 | 68 | -------------------------------------------------------------------------------- /stealth/source/parser/CSS.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { isArray, isBuffer, isObject, isString } from '../../extern/base.mjs'; 3 | import { Parser } from '../../source/language/Parser.mjs'; 4 | import { GRAMMAR } from '../../source/language/CSS/GRAMMAR.mjs'; 5 | import { SYNTAX } from '../../source/language/CSS/SYNTAX.mjs'; 6 | 7 | 8 | 9 | const CSS = { 10 | 11 | compare: function(a, b) { 12 | 13 | // TODO: Can CSS trees be compared to each other? 14 | 15 | }, 16 | 17 | isCSS: function(payload) { 18 | 19 | payload = isObject(payload) ? payload : null; 20 | 21 | 22 | if (payload !== null) { 23 | 24 | // TODO: Validate Payload (tree) 25 | 26 | } 27 | 28 | return false; 29 | 30 | }, 31 | 32 | parse: function(buf_or_str) { 33 | 34 | let raw = null; 35 | 36 | if (isBuffer(buf_or_str) === true) { 37 | raw = buf_or_str.toString('utf8'); 38 | } else if (isString(buf_or_str) === true) { 39 | raw = buf_or_str; 40 | } 41 | 42 | 43 | let parser = new Parser({ grammar: GRAMMAR, syntax: SYNTAX }); 44 | let tree = { type: 'root', rules: [] }; 45 | 46 | if (raw !== null) { 47 | 48 | tree = parser.parse(raw); 49 | 50 | } 51 | 52 | return tree; 53 | 54 | }, 55 | 56 | render: function(tree) { 57 | 58 | // TODO: Render CSS tree into Buffer 59 | 60 | }, 61 | 62 | sort: function(array) { 63 | 64 | array = isArray(array) ? array : null; 65 | 66 | 67 | if (array !== null) { 68 | 69 | return array.filter((tree) => { 70 | return CSS.isCSS(tree) === true; 71 | }).sort((a, b) => { 72 | return CSS.compare(a, b); 73 | }); 74 | 75 | } 76 | 77 | 78 | return []; 79 | 80 | } 81 | 82 | }; 83 | 84 | 85 | export { CSS }; 86 | 87 | -------------------------------------------------------------------------------- /stealth/vendor/policies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "domain": "amazon.com", 4 | "policies": [ 5 | { 6 | "path": "/s/*", 7 | "query": "k" 8 | } 9 | ] 10 | }, 11 | { 12 | "domain": "bing.com", 13 | "policies": [ 14 | { 15 | "path": "/search", 16 | "query": "q&search" 17 | } 18 | ] 19 | }, 20 | { 21 | "domain": "clickserve.dartsearch.net", 22 | "policies": [ 23 | { 24 | "path": "/link/click", 25 | "query": "ds_dest_url" 26 | } 27 | ] 28 | }, 29 | { 30 | "domain": "ebay.com", 31 | "policies": [ 32 | { 33 | "path": "/sch/i.html", 34 | "query": "_nkw&_sacat&LH_*" 35 | } 36 | ] 37 | }, 38 | { 39 | "domain": "google.com", 40 | "policies": [ 41 | { 42 | "path": "/search", 43 | "query": "q&tbm" 44 | }, 45 | { 46 | "path": "/aclk", 47 | "query": "ai&sa" 48 | } 49 | ] 50 | }, 51 | { 52 | "domain": "imdb.com", 53 | "policies": [ 54 | { 55 | "path": "/find", 56 | "query": "q&s&ttype" 57 | } 58 | ] 59 | }, 60 | { 61 | "domain": "reddit.com", 62 | "policies": [ 63 | { 64 | "path": "/r/*/controversial/", 65 | "query": "sort&t" 66 | }, 67 | { 68 | "path": "/r/*/top/", 69 | "query": "t" 70 | } 71 | ] 72 | }, 73 | { 74 | "domain": "shutterstock.com", 75 | "policies": [ 76 | { 77 | "path": "/search/", 78 | "query": "age&artists&color&editorialðnicity&exclude_keywords&gender&image_type&measurement&min_height&min_width&mreleased&number_of_people&orientation&safe&sort" 79 | } 80 | ] 81 | }, 82 | { 83 | "domain": "tholian.network", 84 | "policies": [] 85 | }, 86 | { 87 | "domain": "youtube.com", 88 | "policies": [ 89 | { 90 | "path": "/watch", 91 | "query": "index&list&v" 92 | } 93 | ] 94 | } 95 | ] 96 | -------------------------------------------------------------------------------- /base/review/Map.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { describe, finish } from '../../covert/index.mjs'; 3 | import { Map, isMap } from '../source/Map.mjs'; 4 | 5 | 6 | 7 | describe('Map.isMap()', function(assert) { 8 | 9 | let map1 = new Map(); 10 | let map2 = new Map([ 11 | [ 1, 'l' ], 12 | [ 3, 'e' ], 13 | [ 3, 'e' ], 14 | [ 7, 't' ] 15 | ]); 16 | 17 | assert(typeof Map.isMap, 'function'); 18 | 19 | assert(Map.isMap(map1), true); 20 | assert(Map.isMap(map2), true); 21 | 22 | }); 23 | 24 | describe('isMap()', function(assert) { 25 | 26 | let map1 = new Map(); 27 | let map2 = new Map([ 28 | [ 1, 'l' ], 29 | [ 3, 'e' ], 30 | [ 3, 'e' ], 31 | [ 7, 't' ] 32 | ]); 33 | 34 | assert(typeof isMap, 'function'); 35 | 36 | assert(isMap(map1), true); 37 | assert(isMap(map2), true); 38 | 39 | }); 40 | 41 | describe('Map.prototype.toString()', function(assert) { 42 | 43 | let map1 = new Map(); 44 | let map2 = new Map([ 45 | [ 1, 'l' ], 46 | [ 3, 'e' ], 47 | [ 3, 'e' ], 48 | [ 7, 't' ] 49 | ]); 50 | 51 | assert(Object.prototype.toString.call(map1), '[object Map]'); 52 | assert(Object.prototype.toString.call(map2), '[object Map]'); 53 | 54 | assert((map1).toString(), '[object Map]'); 55 | assert((map2).toString(), '[object Map]'); 56 | 57 | }); 58 | 59 | describe('Map.prototype.valueOf()', function(assert) { 60 | 61 | let map1 = new Map(); 62 | let map2 = new Map([ 63 | [ 1, 'l' ], 64 | [ 3, 'e' ], 65 | [ 3, 'e' ], 66 | [ 7, 't' ] 67 | ]); 68 | 69 | assert((map1).valueOf(), new Map()); 70 | assert((map2).valueOf(), new Map([ [1,'l'], [3,'e'], [7,'t'] ])); 71 | 72 | assert(JSON.stringify(map1), '{}'); 73 | assert(JSON.stringify(map2), '{}'); 74 | 75 | }); 76 | 77 | 78 | export default finish('base/Map', { 79 | internet: false, 80 | network: false 81 | }); 82 | 83 | -------------------------------------------------------------------------------- /browser/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Stealth Browser 3 | 4 | The Stealth Browser is a Browser UI which is implemented using 5 | Web Technologies, so that it is possible to change the Browser 6 | Engine or Web View easily. 7 | 8 | 9 | ## URL-Parameter Flags 10 | 11 | - `/browser/index.html?debug=true` is a parameter to toggle 12 | loading/debugging of the UI by disabling the Service Worker. 13 | The default value is `false`. 14 | 15 | 16 | ## Quickstart 17 | 18 | - Install [node.js](https://nodejs.org/en/download) latest (minimum version `12`). 19 | - Install [Electron](https://www.electronjs.org/releases/stable) latest (minimum version `12`). 20 | - (Alternatively) Install [Ungoogled Chromium](https://github.com/Eloston/ungoogled-chromium/releases) latest (minimum version `70`). 21 | 22 | ```bash 23 | cd /path/to/stealth; 24 | 25 | # Build 26 | node ./stealth/make.mjs; 27 | 28 | # Start Stealth Service 29 | node ./stealth/stealth.mjs serve; 30 | 31 | # Build 32 | node ./browser/make.mjs; 33 | 34 | # Open Progressive Web App 35 | node ./browser/browser.mjs; 36 | ``` 37 | 38 | 39 | ## Building and Packaging 40 | 41 | ```bash 42 | cd /path/to/stealth; 43 | 44 | # Build 45 | node ./browser/make.mjs /path/to/sandbox; 46 | 47 | # Package 48 | node ./browser/make.mjs pack /path/to/sandbox; 49 | ``` 50 | 51 | 52 | ## ECMAScript Usage 53 | 54 | The Browser Library exports a `default` export that contains a namespaced 55 | `Object`. Due to otherwise conflicting names, there are no separately 56 | named exports available. 57 | 58 | As the [extern/base.mjs](./extern/base.mjs) file uses the HTML5 Polyfills 59 | variant of the [Base Library](../base), the Browser Library is also only 60 | compatible with HTML5 Environments. 61 | 62 | ```html 63 | 67 | ``` 68 | 69 | -------------------------------------------------------------------------------- /base/review/Function.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { describe, finish } from '../../covert/index.mjs'; 3 | import { Function, isFunction } from '../source/Function.mjs'; 4 | 5 | 6 | 7 | describe('Function.isFunction()', function(assert) { 8 | 9 | let function1 = function() {console.log('test');}; 10 | let function2 = new Function('console.log(\'test\');'); 11 | 12 | assert(typeof Function.isFunction, 'function'); 13 | 14 | assert(Function.isFunction(function1), true); 15 | assert(Function.isFunction(function2), true); 16 | 17 | }); 18 | 19 | describe('isFunction()', function(assert) { 20 | 21 | let function1 = function() {console.log('test');}; 22 | let function2 = new Function('console.log(\'test\');'); 23 | 24 | assert(typeof isFunction, 'function'); 25 | 26 | assert(isFunction(function1), true); 27 | assert(isFunction(function2), true); 28 | 29 | }); 30 | 31 | describe('Function.prototype.toString()', function(assert) { 32 | 33 | let function1 = function() {console.log('test');}; 34 | let function2 = new Function('console.log(\'test\');'); 35 | 36 | assert(Object.prototype.toString.call(function1), '[object Function]'); 37 | assert(Object.prototype.toString.call(function2), '[object Function]'); 38 | 39 | assert((function1).toString(), 'function() {console.log(\'test\');}'); 40 | assert((function2).toString(), 'function anonymous(\n) {\nconsole.log(\'test\');\n}'); 41 | 42 | }); 43 | 44 | describe('Function.prototype.valueOf()', function(assert) { 45 | 46 | let function1 = function() {console.log('test');}; 47 | let function2 = new Function('console.log(\'test\');'); 48 | 49 | assert((function1).valueOf(), function1); 50 | assert((function2).valueOf(), function2); 51 | 52 | assert(JSON.stringify(function1) === undefined); 53 | assert(JSON.stringify(function2) === undefined); 54 | 55 | }); 56 | 57 | 58 | export default finish('base/Function', { 59 | internet: false, 60 | network: false 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /covert/review/MODULE.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { describe, finish } from '../../covert/index.mjs'; 3 | import COVERT from '../../covert/index.mjs'; 4 | 5 | 6 | 7 | describe('MODULE', function(assert) { 8 | 9 | assert(typeof COVERT['Covert'], 'function'); 10 | assert(typeof COVERT['ENVIRONMENT'], 'object'); 11 | assert(typeof COVERT['Filesystem'], 'function'); 12 | assert(typeof COVERT['Linter'], 'function'); 13 | assert(typeof COVERT['Network'], 'function'); 14 | assert(typeof COVERT['Renderer'], 'function'); 15 | assert(typeof COVERT['Results'], 'function'); 16 | assert(typeof COVERT['Review'], 'function'); 17 | assert(typeof COVERT['Timeline'], 'function'); 18 | 19 | assert(typeof COVERT['after'], 'function'); 20 | assert(typeof COVERT['before'], 'function'); 21 | assert(typeof COVERT['describe'], 'function'); 22 | assert(typeof COVERT['finish'], 'function'); 23 | 24 | assert(typeof COVERT['isCovert'], 'function'); 25 | assert(typeof COVERT['isFilesystem'], 'function'); 26 | assert(typeof COVERT['isLinter'], 'function'); 27 | assert(typeof COVERT['isNetwork'], 'function'); 28 | assert(typeof COVERT['isRenderer'], 'function'); 29 | assert(typeof COVERT['isResults'], 'function'); 30 | assert(typeof COVERT['isReview'], 'function'); 31 | assert(typeof COVERT['isTimeline'], 'function'); 32 | 33 | assert(Object.keys(COVERT), [ 34 | 35 | 'after', 36 | 'before', 37 | 'describe', 38 | 'finish', 39 | 40 | 'isCovert', 41 | 'isFilesystem', 42 | 'isLinter', 43 | 'isNetwork', 44 | 'isRenderer', 45 | 'isResults', 46 | 'isReview', 47 | 'isTimeline', 48 | 49 | 'Covert', 50 | 'ENVIRONMENT', 51 | 'Filesystem', 52 | 'Linter', 53 | 'Network', 54 | 'Renderer', 55 | 'Results', 56 | 'Review', 57 | 'Timeline' 58 | 59 | ]); 60 | 61 | }); 62 | 63 | 64 | export default finish('covert/MODULE', { 65 | internet: false, 66 | network: false 67 | }); 68 | 69 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | 2 | # TODO 3 | 4 | - `progress` event's payload is `null` probably due to packet parser. 5 | - `response` event has wrong `@transfer` headers, these need to be fixed. 6 | - Webproxy Review needs more tests for webview requests. 7 | 8 | 9 | # Cache Service 10 | 11 | - Cache needs meta information that contains a map of datetime, content-length, 12 | last-modified, etag for each URL. 13 | 14 | - Cache is compressed or filtered on disk, which means that original file 15 | sizes have to be persisted to be compareable later; so the cache structure 16 | needs an additional `size` (amount of bytes) header. 17 | 18 | 19 | # Session Service 20 | 21 | - Tab open event 22 | - Tab close event 23 | - Tab navigate event 24 | - Tab refresh event (creates a task with `type=refresh` event for current URL) 25 | 26 | 27 | # `stealth:radar` Page 28 | 29 | - Popular Sites and URLs 30 | - Shareable Topics and Bookmarks/Subscribed Content 31 | 32 | # `stealth:beacons` Page 33 | 34 | - Adapters to extract content, available for `echoes` and its flow graph. 35 | - Export and Synchronize assets per URL pattern (e.g. texts, audios, images and videos). 36 | 37 | # `stealth:echoes` Page 38 | 39 | - User Action Recording Wizard, which records `keyboard`, `mouse`, `click`, 40 | and `touch` events for automation. 41 | 42 | # `stealth:tasks` Page 43 | 44 | - Implementation of a task scheduling wizard that allows to schedule `beacon` 45 | and `echo` actions at specific repeating times and/or a single datetime. 46 | 47 | - Tasks with type `request` and `scrape` should contain incremental checks to 48 | prevent unnecessary downloads. This means check of datetime, content-length, etag 49 | headers etc. 50 | 51 | - Quality of proposal for shared tasks, the broader the options, the more likely 52 | to succeed. Higher quality of proposals lead to more peers adopting the tasks. 53 | 54 | - Debate phase where different tasks and objectives are evaluated. 55 | - Consensus: Same goal, different choices. 56 | - Vote: Different goals, one choice each, usually mutually exclusive. 57 | 58 | -------------------------------------------------------------------------------- /browser/design/appbar/History.css: -------------------------------------------------------------------------------- 1 | 2 | browser-appbar-history { 3 | display: inline-block; 4 | position: relative; 5 | width: auto; 6 | height: 32px; 7 | margin: 0px; 8 | padding: 0px; 9 | line-height: 32px; 10 | text-align: center; 11 | vertical-align: top; 12 | white-space: nowrap; 13 | user-select: none; 14 | -webkit-user-select: none; 15 | } 16 | 17 | browser-appbar-history button { 18 | display: inline-block; 19 | min-width: 32px; 20 | margin: 0px; 21 | padding: 0px; 22 | line-height: 32px; 23 | height: 32px; 24 | color: var(--element-default-color); 25 | font-size: 24px; 26 | text-align: center; 27 | background: var(--element-default-background); 28 | border: 1px solid transparent; 29 | border-radius: 4px; 30 | box-sizing: border-box; 31 | transition: 200ms all ease-out; 32 | appearance: none; 33 | -moz-appearance: none; 34 | -webkit-appearance: none; 35 | outline: none; 36 | cursor: pointer; 37 | } 38 | 39 | browser-appbar-history button:before { 40 | display: inline-block; 41 | width: 100%; 42 | font-family: 'icon'; 43 | text-align: center; 44 | speak: none; 45 | -webkit-font-smoothing: antialiased; 46 | } 47 | 48 | browser-appbar-history button[disabled], 49 | browser-appbar-history button[disabled]:hover { 50 | color: var(--element-disable-color); 51 | border: 1px solid transparent; 52 | background: var(--element-disable-background); 53 | cursor: not-allowed; 54 | } 55 | 56 | browser-appbar-history button:focus, 57 | browser-appbar-history button:hover { 58 | color: var(--element-focus-color); 59 | border: 1px solid var(--element-focus-color); 60 | background: var(--element-focus-background); 61 | outline: none; 62 | } 63 | 64 | browser-appbar-history button::-moz-focus-inner { 65 | border: 0px solid transparent; 66 | } 67 | 68 | browser-appbar-history button[data-key="back"]:before { content: '\e5c4'; } 69 | browser-appbar-history button[data-key="next"]:before { content: '\e5c8'; } 70 | browser-appbar-history button[data-key="reload"]:before { content: '\e5d5'; } 71 | browser-appbar-history button[data-key="open"]:before { content: '\e145'; } 72 | 73 | -------------------------------------------------------------------------------- /base/review/MODULE.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { describe, finish } from '../../covert/index.mjs'; 3 | import BASE from '../../base/index.mjs'; 4 | 5 | 6 | 7 | describe('MODULE', function(assert) { 8 | 9 | assert(typeof BASE['console'], 'object'); 10 | 11 | assert(typeof BASE['Array'], 'function'); 12 | assert(typeof BASE['Boolean'], 'function'); 13 | assert(typeof BASE['Buffer'], 'function'); 14 | assert(typeof BASE['Date'], 'function'); 15 | assert(typeof BASE['Emitter'], 'function'); 16 | assert(typeof BASE['Function'], 'function'); 17 | assert(typeof BASE['Map'], 'function'); 18 | assert(typeof BASE['Number'], 'function'); 19 | assert(typeof BASE['Object'], 'function'); 20 | assert(typeof BASE['RegExp'], 'function'); 21 | assert(typeof BASE['Set'], 'function'); 22 | assert(typeof BASE['String'], 'function'); 23 | 24 | assert(typeof BASE['isArray'], 'function'); 25 | assert(typeof BASE['isBoolean'], 'function'); 26 | assert(typeof BASE['isBuffer'], 'function'); 27 | assert(typeof BASE['isDate'], 'function'); 28 | assert(typeof BASE['isEmitter'], 'function'); 29 | assert(typeof BASE['isFunction'], 'function'); 30 | assert(typeof BASE['isMap'], 'function'); 31 | assert(typeof BASE['isNumber'], 'function'); 32 | assert(typeof BASE['isObject'], 'function'); 33 | assert(typeof BASE['isRegExp'], 'function'); 34 | assert(typeof BASE['isSet'], 'function'); 35 | assert(typeof BASE['isString'], 'function'); 36 | 37 | assert(Object.keys(BASE), [ 38 | 39 | 'console', 40 | 41 | 'Array', 42 | 'Boolean', 43 | 'Buffer', 44 | 'Date', 45 | 'Emitter', 46 | 'Function', 47 | 'Map', 48 | 'Number', 49 | 'Object', 50 | 'RegExp', 51 | 'Set', 52 | 'String', 53 | 54 | 'isArray', 55 | 'isBoolean', 56 | 'isBuffer', 57 | 'isDate', 58 | 'isEmitter', 59 | 'isFunction', 60 | 'isMap', 61 | 'isNumber', 62 | 'isObject', 63 | 'isRegExp', 64 | 'isSet', 65 | 'isString' 66 | 67 | ]); 68 | 69 | }); 70 | 71 | 72 | export default finish('base/MODULE', { 73 | internet: false, 74 | network: false 75 | }); 76 | 77 | -------------------------------------------------------------------------------- /browser/design/Assistant.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { console, isObject, isString } from '../extern/base.mjs'; 3 | 4 | 5 | 6 | let TIMEOUT = Date.now(); 7 | 8 | const Assistant = function(settings) { 9 | 10 | settings = isObject(settings) ? settings : {}; 11 | 12 | 13 | this.settings = Object.freeze(Object.assign({ 14 | name: null, 15 | widget: null, 16 | events: {} 17 | }, settings)); 18 | 19 | 20 | this.__state = { 21 | sounds: {}, 22 | texts: {} 23 | }; 24 | 25 | 26 | if ( 27 | isString(this.settings.widget) === true 28 | && isObject(this.settings.events) === true 29 | ) { 30 | 31 | for (let name in this.settings.events) { 32 | 33 | let text = this.settings.events[name]; 34 | let audio = new Audio('/browser/design/' + this.settings.widget + '.' + name + '.wav'); 35 | 36 | this.__state.sounds[name] = audio; 37 | this.__state.texts[name] = text; 38 | 39 | } 40 | 41 | } 42 | 43 | }; 44 | 45 | Assistant.prototype = { 46 | 47 | emit: function(event) { 48 | 49 | event = isString(event) ? event : null; 50 | 51 | 52 | if (event !== null) { 53 | 54 | let sound = this.__state.sounds[event] || null; 55 | let text = this.__state.texts[event] || null; 56 | 57 | let allowed = false; 58 | let browser = window.parent.BROWSER || null; 59 | if (browser !== null) { 60 | 61 | if (browser.settings['interface']['assistant'] === true) { 62 | allowed = true; 63 | } 64 | 65 | } 66 | 67 | if (allowed === true) { 68 | 69 | if (sound !== null) { 70 | 71 | if (Date.now() > TIMEOUT + 100) { 72 | 73 | try { 74 | sound.play().catch(() => {}); 75 | } catch (err) { 76 | // Do Nothing 77 | } 78 | 79 | TIMEOUT = Date.now(); 80 | 81 | } 82 | 83 | } 84 | 85 | if (text !== null) { 86 | console.log('Assistant: ' + text); 87 | } 88 | 89 | } 90 | 91 | 92 | return true; 93 | 94 | } 95 | 96 | return false; 97 | 98 | } 99 | 100 | }; 101 | 102 | 103 | export { Assistant }; 104 | 105 | -------------------------------------------------------------------------------- /.github/workflows/covert-check.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Covert Check 3 | 4 | on: [push] 5 | 6 | jobs: 7 | 8 | check-base: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: 14 17 | - run: npm install 18 | - run: node ./make.mjs build 19 | - run: | 20 | cd ./base 21 | node ../covert/covert.mjs check "base/*" --debug=true --report=../.github/workflows/covert-check-base 22 | - uses: actions/upload-artifact@v2 23 | with: 24 | name: Report for Base 25 | path: .github/workflows/covert-check-base.* 26 | - run: node .github/workflows/covert-check.mjs .github/workflows/covert-check-base.report 27 | 28 | check-covert: 29 | 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - uses: actions/checkout@v2 34 | - uses: actions/setup-node@v1 35 | with: 36 | node-version: 14 37 | - run: npm install 38 | - run: node ./make.mjs build 39 | - run: | 40 | cd ./covert 41 | node ../covert/covert.mjs check "covert/*" --debug=true --report=../.github/workflows/covert-check-covert 42 | - uses: actions/upload-artifact@v2 43 | with: 44 | name: Report for Covert 45 | path: .github/workflows/covert-check-covert.* 46 | - run: node .github/workflows/covert-check.mjs .github/workflows/covert-check-covert.report 47 | 48 | check-stealth: 49 | 50 | runs-on: ubuntu-latest 51 | 52 | steps: 53 | - uses: actions/checkout@v2 54 | - uses: actions/setup-node@v1 55 | with: 56 | node-version: 14 57 | - run: npm install 58 | - run: node ./make.mjs build 59 | - run: | 60 | cd ./stealth 61 | node ../covert/covert.mjs check "stealth/*" --debug=true --report=../.github/workflows/covert-check-stealth 62 | - uses: actions/upload-artifact@v2 63 | with: 64 | name: Report for Stealth 65 | path: .github/workflows/covert-check-stealth.* 66 | - run: node .github/workflows/covert-check.mjs .github/workflows/covert-check-stealth.report 67 | 68 | -------------------------------------------------------------------------------- /base/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Base Library 3 | 4 | The Base Library is a polyfilling abstraction layer that allows to use the 5 | identical code bases among the Web Browser and node.js without having to 6 | integrate a third-party build toolchain or build workflow. 7 | 8 | The intention of this library is to have an ECMAScript Modules abstraction 9 | that allows to include otherwise globally defined data types via a single 10 | file, and allows cross-context and cross-sandbox serialization of data 11 | types, which otherwise wouldn't be possible. 12 | 13 | As data type instances in ECMAScript runtimes are instanciated per-sandbox, 14 | this library also includes `is(Datatype)` replacements for `typeof` and 15 | `instanceof`, and it is recommended to use the `[Symbol.toStringTag]` on 16 | the prototype of custom Function Templates to stay compatible. 17 | 18 | 19 | ## Usage 20 | 21 | The Base Library is using a simple [make.mjs](./make.mjs) script that 22 | generates the [/build](./build) folder. 23 | 24 | The `make.mjs` is called by other build scripts, too: 25 | 26 | - [Browser's make.mjs](../browser/make.mjs) 27 | - [Covert's make.mjs](../covert/make.mjs) 28 | - [Stealth's make.mjs](../stealth/make.mjs) 29 | 30 | 31 | ```bash 32 | cd /path/to/stealth; 33 | 34 | # Build the Base Library 35 | node ./base/make.mjs; 36 | 37 | # Afterwards, simply copy/paste the generated ESM module and use import syntax 38 | # cp ./base/build/browser.mjs ./to/a/browser/project/extern/base.mjs; 39 | # cp ./base/build/node.mjs ./to/a/node/project/extern/base.mjs; 40 | ``` 41 | 42 | 43 | ## ECMAScript Usage 44 | 45 | The Base Library exports a `default` export that contains an `Object` with 46 | all exports. Additionally, all methods and data types are exported as named 47 | exports. 48 | 49 | ```javascript 50 | // Import Base Library 51 | import base from './base/index.mjs'; 52 | 53 | // Import selected named exports 54 | import { console, Emitter, isEmitter } from './base/index.mjs'; 55 | ``` 56 | 57 | 58 | ## NPM Usage 59 | 60 | ```javascript 61 | // Import Base Library 62 | import base from 'stealth/base'; 63 | 64 | // Import selected named exports 65 | import { console, Emitter, isEmitter } from 'stealth/base'; 66 | ``` 67 | 68 | -------------------------------------------------------------------------------- /stealth/source/client/service/Settings.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Emitter, isFunction, isObject } from '../../../extern/base.mjs'; 3 | 4 | 5 | 6 | const Settings = function(client) { 7 | 8 | this.client = client; 9 | Emitter.call(this); 10 | 11 | }; 12 | 13 | 14 | Settings.prototype = Object.assign({}, Emitter.prototype, { 15 | 16 | toJSON: function() { 17 | 18 | let blob = Emitter.prototype.toJSON.call(this); 19 | let data = { 20 | events: blob.data.events, 21 | journal: blob.data.journal 22 | }; 23 | 24 | return { 25 | 'type': 'Settings Service', 26 | 'data': data 27 | }; 28 | 29 | }, 30 | 31 | info: function(payload, callback) { 32 | 33 | payload = isObject(payload) ? payload : null; 34 | callback = isFunction(callback) ? callback : null; 35 | 36 | 37 | if (callback !== null) { 38 | 39 | this.once('info', (response) => callback(response)); 40 | 41 | this.client.send({ 42 | headers: { 43 | service: 'settings', 44 | method: 'info' 45 | }, 46 | payload: payload 47 | }); 48 | 49 | } 50 | 51 | }, 52 | 53 | read: function(payload, callback) { 54 | 55 | payload = isObject(payload) ? payload : null; 56 | callback = isFunction(callback) ? callback : null; 57 | 58 | 59 | if (callback !== null) { 60 | 61 | this.once('read', (response) => callback(response)); 62 | 63 | this.client.send({ 64 | headers: { 65 | service: 'settings', 66 | method: 'read' 67 | }, 68 | payload: payload 69 | }); 70 | 71 | } 72 | 73 | }, 74 | 75 | save: function(payload, callback) { 76 | 77 | payload = isObject(payload) ? payload : null; 78 | callback = isFunction(callback) ? callback : null; 79 | 80 | 81 | if (payload !== null && callback !== null) { 82 | 83 | this.once('save', (result) => callback(result)); 84 | 85 | this.client.send({ 86 | headers: { 87 | service: 'settings', 88 | method: 'save' 89 | }, 90 | payload: payload 91 | }); 92 | 93 | } else if (callback !== null) { 94 | callback(false); 95 | } 96 | 97 | } 98 | 99 | }); 100 | 101 | 102 | export { Settings }; 103 | 104 | -------------------------------------------------------------------------------- /guide/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Developer Guide 3 | 4 | This Guide is intended as a reference document for both the concept and architecture, 5 | and their documentation of implementations. 6 | 7 | The idea is that this guide helps developers to implement their own Browser Frontend 8 | (or Client or Peer) that can reuse the Stealth Service and its provided APIs. 9 | 10 | Currently, the Repository consists of these important folders: 11 | 12 | - [Base](/base) is a Library that contains Polyfills for Browser, Covert and Stealth. 13 | - [Browser](/browser) is the Web-based Frontend for Stealth that can be used as a Progressive Web App. 14 | - [Covert](/covert) is the Test Runner. 15 | - [Profile](/profile) is the Tholian Vendor Profile. 16 | - [Stealth](/stealth) is the Stealth Service (that runs in node.js). 17 | 18 | 19 | # Guide Contents 20 | 21 | - [README.md](./README.md) is this file. 22 | - [security/Network.md](./security/Network.md) contains the Network Security guide and explains potential Attack Vectors. 23 | - [security/Site.md](./security/Site.md) contains the Site Security guide and explains potential Attack Vectors. 24 | - [concept/Browser.md](./concept/Browser.md) explains the Browser and Browser UI. 25 | - [concept/Covert.md](./concept/Covert.md) explains the Covert Testrunner. 26 | - [concept/Stealth.md](./concept/Stealth.md) explains the Stealth Service. 27 | - [concept/Service.md](./concept/Service.md) explains the Stealth Service API. 28 | - [concept/Network.md](./concept/Network.md) explains Network Topology and Service Discovery Workflows. 29 | 30 | 31 | # Implementation Notes 32 | 33 | - [implementation/Parser.md](./implementation/Parser.md) documents parser implementation quirks and differences. 34 | 35 | 36 | # Implementation Examples 37 | 38 | Each Project has a `/review` folder which contains Reviews for Covert. The idea behind a 39 | `Review` is the complete audit-by-example of the equivalent implementation in the `/source` 40 | folder. 41 | 42 | For example, the [/stealth/review/Request.mjs](/stealth/review/Request.mjs) reviews the 43 | [/stealth/source/Request.mjs](/stealth/source/Request.mjs) implementation and it can be 44 | run directly with `covert scan stealth/Request`. 45 | 46 | The Usage of `Covert` is documented in the Covert's [README.md](/covert/README.md) file. 47 | 48 | -------------------------------------------------------------------------------- /browser/design/card/Blocker.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Element } from '../Element.mjs'; 3 | import { Widget } from '../Widget.mjs'; 4 | import { isArray, isObject } from '../../extern/base.mjs'; 5 | 6 | 7 | 8 | const Blocker = function(browser, actions) { 9 | 10 | this.actions = isArray(actions) ? actions : []; 11 | this.element = new Element('browser-card-blocker', [ 12 | '

', 13 | '', 14 | '', 15 | '', 16 | '', 17 | '' 18 | ]); 19 | 20 | this.buttons = { 21 | toggle: this.element.query('button[data-action="toggle"]') 22 | }; 23 | 24 | this.model = { 25 | domain: this.element.query('[data-key="domain"]') 26 | }; 27 | 28 | Widget.call(this); 29 | 30 | 31 | this.element.on('show', () => { 32 | 33 | this.element.state('active'); 34 | 35 | if (this.buttons.toggle !== null) { 36 | this.buttons.toggle.state('active'); 37 | } 38 | 39 | }); 40 | 41 | this.element.on('hide', () => { 42 | 43 | this.element.state(''); 44 | 45 | if (this.buttons.toggle !== null) { 46 | this.buttons.toggle.state(''); 47 | } 48 | 49 | }); 50 | 51 | this.element.on('update', () => { 52 | // Ignore 53 | }); 54 | 55 | 56 | if (this.buttons.toggle !== null) { 57 | 58 | this.buttons.toggle.on('click', () => { 59 | 60 | if (this.element.state() === 'active') { 61 | this.element.emit('hide'); 62 | } else { 63 | this.element.emit('show'); 64 | } 65 | 66 | }); 67 | 68 | } 69 | 70 | this.element.emit('update'); 71 | 72 | }; 73 | 74 | 75 | Blocker.from = function(value, actions) { 76 | 77 | value = isObject(value) ? value : null; 78 | actions = isArray(actions) ? actions : null; 79 | 80 | 81 | let widget = null; 82 | 83 | if (value !== null) { 84 | 85 | widget = new Blocker(window.parent.BROWSER || null, actions); 86 | widget.value(value); 87 | 88 | } 89 | 90 | return widget; 91 | 92 | }; 93 | 94 | 95 | Blocker.prototype = Object.assign({}, Widget.prototype); 96 | 97 | 98 | export { Blocker }; 99 | 100 | -------------------------------------------------------------------------------- /browser/internal/media/Media.css: -------------------------------------------------------------------------------- 1 | 2 | @import url("../../design/widget/Image.css"); 3 | @import url("../../design/widget/Audio.css"); 4 | @import url("../../design/widget/Video.css"); 5 | 6 | 7 | 8 | browser-widget-media { 9 | all: unset; 10 | display: block; 11 | position: relative; 12 | width: 800px; 13 | margin: 0px; 14 | padding: 16px; 15 | background: var(--surface-default-background); 16 | border: 1px solid var(--surface-default-color); 17 | border-radius: 8px; 18 | box-sizing: border-box; 19 | transition: 200ms all ease-out; 20 | overflow: hidden; 21 | cursor: default; 22 | } 23 | 24 | browser-widget-media > h3 { 25 | display: block; 26 | margin: 0px 0px 16px 0px; 27 | padding: 0px; 28 | font-family: 'crystalline'; 29 | font-size: 32px; 30 | font-weight: 500; 31 | line-height: 32px; 32 | text-transform: uppercase; 33 | border-bottom: 1px solid var(--surface-default-color); 34 | } 35 | 36 | browser-widget-media > h3:before { 37 | display: inline; 38 | content: '\e038'; 39 | margin: 0px 16px 0px 0px; 40 | padding: 0px; 41 | font-family: 'icon'; 42 | font-size: 24px; 43 | font-weight: 400; 44 | text-align: center; 45 | vertical-align: bottom; 46 | speak: none; 47 | -webkit-font-smoothing: antialiased; 48 | } 49 | 50 | 51 | 52 | browser-widget-media-header { 53 | display: block; 54 | width: 100%; 55 | height: auto; 56 | } 57 | 58 | browser-widget-media-header input[type="text"] { 59 | display: block; 60 | width: 100%; 61 | margin: 0px auto 16px auto; 62 | text-align: center; 63 | } 64 | 65 | 66 | 67 | browser-widget-media-article { 68 | display: block; 69 | width: 100%; 70 | height: auto; 71 | } 72 | 73 | browser-widget-media-article > * { 74 | width: 100% !important; 75 | margin: 0px 0px 16px 0px !important; 76 | } 77 | 78 | 79 | 80 | browser-widget-media-footer { 81 | display: block; 82 | height: 32px; 83 | margin: 16px 0px 0px 0px; 84 | padding: 0px; 85 | text-align: right; 86 | } 87 | 88 | browser-widget-media-footer > button { 89 | margin-left: 16px; 90 | } 91 | 92 | browser-widget-media-footer > button[data-action="refresh"]:before { content: '\e5d5'; } 93 | 94 | 95 | 96 | @media screen and (max-width: 800px) { 97 | 98 | browser-widget-media { 99 | width: auto; 100 | } 101 | 102 | } 103 | 104 | -------------------------------------------------------------------------------- /stealth/source/client/service/Echo.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Emitter, isFunction, isObject } from '../../../extern/base.mjs'; 3 | 4 | 5 | 6 | const Echo = function(client) { 7 | 8 | this.client = client; 9 | Emitter.call(this); 10 | 11 | }; 12 | 13 | 14 | Echo.prototype = Object.assign({}, Emitter.prototype, { 15 | 16 | toJSON: function() { 17 | 18 | let blob = Emitter.prototype.toJSON.call(this); 19 | let data = { 20 | events: blob.data.events, 21 | journal: blob.data.journal 22 | }; 23 | 24 | return { 25 | 'type': 'Echo Service', 26 | 'data': data 27 | }; 28 | 29 | }, 30 | 31 | read: function(payload, callback) { 32 | 33 | payload = isObject(payload) ? payload : null; 34 | callback = isFunction(callback) ? callback : null; 35 | 36 | 37 | if (payload !== null && callback !== null) { 38 | 39 | this.once('read', (response) => callback(response)); 40 | 41 | this.client.send({ 42 | headers: { 43 | service: 'echo', 44 | method: 'read' 45 | }, 46 | payload: payload 47 | }); 48 | 49 | } else if (callback !== null) { 50 | callback(null); 51 | } 52 | 53 | }, 54 | 55 | remove: function(payload, callback) { 56 | 57 | payload = isObject(payload) ? payload : null; 58 | callback = isFunction(callback) ? callback : null; 59 | 60 | 61 | if (payload !== null && callback !== null) { 62 | 63 | this.once('remove', (result) => callback(result)); 64 | 65 | this.client.send({ 66 | headers: { 67 | service: 'echo', 68 | method: 'remove' 69 | }, 70 | payload: payload 71 | }); 72 | 73 | } else if (callback !== null) { 74 | callback(false); 75 | } 76 | 77 | }, 78 | 79 | save: function(payload, callback) { 80 | 81 | payload = isObject(payload) ? payload : null; 82 | callback = isFunction(callback) ? callback : null; 83 | 84 | 85 | if (payload !== null && callback !== null) { 86 | 87 | this.once('save', (result) => callback(result)); 88 | 89 | this.client.send({ 90 | headers: { 91 | service: 'echo', 92 | method: 'save' 93 | }, 94 | payload: payload 95 | }); 96 | 97 | } else if (callback !== null) { 98 | callback(false); 99 | } 100 | 101 | } 102 | 103 | }); 104 | 105 | 106 | export { Echo }; 107 | 108 | -------------------------------------------------------------------------------- /stealth/source/client/service/Mode.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Emitter, isFunction, isObject } from '../../../extern/base.mjs'; 3 | 4 | 5 | 6 | const Mode = function(client) { 7 | 8 | this.client = client; 9 | Emitter.call(this); 10 | 11 | }; 12 | 13 | 14 | Mode.prototype = Object.assign({}, Emitter.prototype, { 15 | 16 | toJSON: function() { 17 | 18 | let blob = Emitter.prototype.toJSON.call(this); 19 | let data = { 20 | events: blob.data.events, 21 | journal: blob.data.journal 22 | }; 23 | 24 | return { 25 | 'type': 'Mode Service', 26 | 'data': data 27 | }; 28 | 29 | }, 30 | 31 | read: function(payload, callback) { 32 | 33 | payload = isObject(payload) ? payload : null; 34 | callback = isFunction(callback) ? callback : null; 35 | 36 | 37 | if (payload !== null && callback !== null) { 38 | 39 | this.once('read', (response) => callback(response)); 40 | 41 | this.client.send({ 42 | headers: { 43 | service: 'mode', 44 | method: 'read' 45 | }, 46 | payload: payload 47 | }); 48 | 49 | } else if (callback !== null) { 50 | callback(null); 51 | } 52 | 53 | }, 54 | 55 | remove: function(payload, callback) { 56 | 57 | payload = isObject(payload) ? payload : null; 58 | callback = isFunction(callback) ? callback : null; 59 | 60 | 61 | if (payload !== null && callback !== null) { 62 | 63 | this.once('remove', (result) => callback(result)); 64 | 65 | this.client.send({ 66 | headers: { 67 | service: 'mode', 68 | method: 'remove' 69 | }, 70 | payload: payload 71 | }); 72 | 73 | } else if (callback !== null) { 74 | callback(false); 75 | } 76 | 77 | }, 78 | 79 | save: function(payload, callback) { 80 | 81 | payload = isObject(payload) ? payload : null; 82 | callback = isFunction(callback) ? callback : null; 83 | 84 | 85 | if (payload !== null && callback !== null) { 86 | 87 | this.once('save', (result) => callback(result)); 88 | 89 | this.client.send({ 90 | headers: { 91 | service: 'mode', 92 | method: 'save' 93 | }, 94 | payload: payload 95 | }); 96 | 97 | } else if (callback !== null) { 98 | callback(false); 99 | } 100 | 101 | } 102 | 103 | }); 104 | 105 | 106 | export { Mode }; 107 | 108 | -------------------------------------------------------------------------------- /stealth/source/client/service/Task.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Emitter, isFunction, isObject } from '../../../extern/base.mjs'; 3 | 4 | 5 | 6 | const Task = function(client) { 7 | 8 | this.client = client; 9 | Emitter.call(this); 10 | 11 | }; 12 | 13 | 14 | Task.prototype = Object.assign({}, Emitter.prototype, { 15 | 16 | toJSON: function() { 17 | 18 | let blob = Emitter.prototype.toJSON.call(this); 19 | let data = { 20 | events: blob.data.events, 21 | journal: blob.data.journal 22 | }; 23 | 24 | return { 25 | 'type': 'Task Service', 26 | 'data': data 27 | }; 28 | 29 | }, 30 | 31 | read: function(payload, callback) { 32 | 33 | payload = isObject(payload) ? payload : null; 34 | callback = isFunction(callback) ? callback : null; 35 | 36 | 37 | if (payload !== null && callback !== null) { 38 | 39 | this.once('read', (response) => callback(response)); 40 | 41 | this.client.send({ 42 | headers: { 43 | service: 'task', 44 | method: 'read' 45 | }, 46 | payload: payload 47 | }); 48 | 49 | } else if (callback !== null) { 50 | callback(null); 51 | } 52 | 53 | }, 54 | 55 | remove: function(payload, callback) { 56 | 57 | payload = isObject(payload) ? payload : null; 58 | callback = isFunction(callback) ? callback : null; 59 | 60 | 61 | if (payload !== null && callback !== null) { 62 | 63 | this.once('remove', (result) => callback(result)); 64 | 65 | this.client.send({ 66 | headers: { 67 | service: 'task', 68 | method: 'remove' 69 | }, 70 | payload: payload 71 | }); 72 | 73 | } else if (callback !== null) { 74 | callback(false); 75 | } 76 | 77 | }, 78 | 79 | save: function(payload, callback) { 80 | 81 | payload = isObject(payload) ? payload : null; 82 | callback = isFunction(callback) ? callback : null; 83 | 84 | 85 | if (payload !== null && callback !== null) { 86 | 87 | this.once('save', (result) => callback(result)); 88 | 89 | this.client.send({ 90 | headers: { 91 | service: 'task', 92 | method: 'save' 93 | }, 94 | payload: payload 95 | }); 96 | 97 | } else if (callback !== null) { 98 | callback(false); 99 | } 100 | 101 | } 102 | 103 | }); 104 | 105 | 106 | export { Task }; 107 | 108 | -------------------------------------------------------------------------------- /stealth/source/client/service/Beacon.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Emitter, isFunction, isObject } from '../../../extern/base.mjs'; 3 | 4 | 5 | 6 | const Beacon = function(client) { 7 | 8 | this.client = client; 9 | Emitter.call(this); 10 | 11 | }; 12 | 13 | 14 | Beacon.prototype = Object.assign({}, Emitter.prototype, { 15 | 16 | toJSON: function() { 17 | 18 | let blob = Emitter.prototype.toJSON.call(this); 19 | let data = { 20 | events: blob.data.events, 21 | journal: blob.data.journal 22 | }; 23 | 24 | return { 25 | 'type': 'Beacon Service', 26 | 'data': data 27 | }; 28 | 29 | }, 30 | 31 | read: function(payload, callback) { 32 | 33 | payload = isObject(payload) ? payload : null; 34 | callback = isFunction(callback) ? callback : null; 35 | 36 | 37 | if (payload !== null && callback !== null) { 38 | 39 | this.once('read', (response) => callback(response)); 40 | 41 | this.client.send({ 42 | headers: { 43 | service: 'beacon', 44 | method: 'read' 45 | }, 46 | payload: payload 47 | }); 48 | 49 | } else if (callback !== null) { 50 | callback(null); 51 | } 52 | 53 | }, 54 | 55 | remove: function(payload, callback) { 56 | 57 | payload = isObject(payload) ? payload : null; 58 | callback = isFunction(callback) ? callback : null; 59 | 60 | 61 | if (payload !== null && callback !== null) { 62 | 63 | this.once('remove', (result) => callback(result)); 64 | 65 | this.client.send({ 66 | headers: { 67 | service: 'beacon', 68 | method: 'remove' 69 | }, 70 | payload: payload 71 | }); 72 | 73 | } else if (callback !== null) { 74 | callback(false); 75 | } 76 | 77 | }, 78 | 79 | save: function(payload, callback) { 80 | 81 | payload = isObject(payload) ? payload : null; 82 | callback = isFunction(callback) ? callback : null; 83 | 84 | 85 | if (payload !== null && callback !== null) { 86 | 87 | this.once('save', (result) => callback(result)); 88 | 89 | this.client.send({ 90 | headers: { 91 | service: 'beacon', 92 | method: 'save' 93 | }, 94 | payload: payload 95 | }); 96 | 97 | } else if (callback !== null) { 98 | callback(false); 99 | } 100 | 101 | } 102 | 103 | }); 104 | 105 | 106 | export { Beacon }; 107 | 108 | -------------------------------------------------------------------------------- /stealth/source/client/service/Policy.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Emitter, isFunction, isObject } from '../../../extern/base.mjs'; 3 | 4 | 5 | 6 | const Policy = function(client) { 7 | 8 | this.client = client; 9 | Emitter.call(this); 10 | 11 | }; 12 | 13 | 14 | Policy.prototype = Object.assign({}, Emitter.prototype, { 15 | 16 | toJSON: function() { 17 | 18 | let blob = Emitter.prototype.toJSON.call(this); 19 | let data = { 20 | events: blob.data.events, 21 | journal: blob.data.journal 22 | }; 23 | 24 | return { 25 | 'type': 'Policy Service', 26 | 'data': data 27 | }; 28 | 29 | }, 30 | 31 | read: function(payload, callback) { 32 | 33 | payload = isObject(payload) ? payload : null; 34 | callback = isFunction(callback) ? callback : null; 35 | 36 | 37 | if (payload !== null && callback !== null) { 38 | 39 | this.once('read', (response) => callback(response)); 40 | 41 | this.client.send({ 42 | headers: { 43 | service: 'policy', 44 | method: 'read' 45 | }, 46 | payload: payload 47 | }); 48 | 49 | } else if (callback !== null) { 50 | callback(null); 51 | } 52 | 53 | }, 54 | 55 | remove: function(payload, callback) { 56 | 57 | payload = isObject(payload) ? payload : null; 58 | callback = isFunction(callback) ? callback : null; 59 | 60 | 61 | if (payload !== null && callback !== null) { 62 | 63 | this.once('remove', (result) => callback(result)); 64 | 65 | this.client.send({ 66 | headers: { 67 | service: 'policy', 68 | method: 'remove' 69 | }, 70 | payload: payload 71 | }); 72 | 73 | } else if (callback !== null) { 74 | callback(false); 75 | } 76 | 77 | }, 78 | 79 | save: function(payload, callback) { 80 | 81 | payload = isObject(payload) ? payload : null; 82 | callback = isFunction(callback) ? callback : null; 83 | 84 | 85 | if (payload !== null && callback !== null) { 86 | 87 | this.once('save', (result) => callback(result)); 88 | 89 | this.client.send({ 90 | headers: { 91 | service: 'policy', 92 | method: 'save' 93 | }, 94 | payload: payload 95 | }); 96 | 97 | } else if (callback !== null) { 98 | callback(false); 99 | } 100 | 101 | } 102 | 103 | }); 104 | 105 | 106 | export { Policy }; 107 | 108 | -------------------------------------------------------------------------------- /stealth/review/client/service/Blocker.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { isFunction } from '../../../../base/index.mjs'; 3 | import { after, before, describe, finish } from '../../../../covert/index.mjs'; 4 | import { Blocker } from '../../../../stealth/source/client/service/Blocker.mjs'; 5 | import { connect as connect_stealth, disconnect as disconnect_stealth } from '../../../../stealth/review/Stealth.mjs'; 6 | import { connect as connect_client, disconnect as disconnect_client } from '../../../../stealth/review/Client.mjs'; 7 | 8 | 9 | 10 | before(connect_stealth); 11 | before(connect_client); 12 | 13 | describe('new Blocker()', function(assert) { 14 | 15 | assert(this.client !== null); 16 | assert(this.client.services.blocker instanceof Blocker, true); 17 | 18 | }); 19 | 20 | describe('Blocker.prototype.toJSON()', function(assert) { 21 | 22 | assert(this.client !== null); 23 | assert(isFunction(this.client.services.blocker.toJSON), true); 24 | 25 | assert(this.client.services.blocker.toJSON(), { 26 | type: 'Blocker Service', 27 | data: { 28 | events: [], 29 | journal: [] 30 | } 31 | }); 32 | 33 | }); 34 | 35 | describe('Blocker.prototype.read()/success', function(assert) { 36 | 37 | assert(this.client !== null); 38 | assert(isFunction(this.client.services.blocker.read), true); 39 | 40 | this.client.services.blocker.read({ 41 | domain: 'ads.quantserve.com' 42 | }, (response) => { 43 | 44 | assert(response, { 45 | domain: 'ads.quantserve.com' 46 | }); 47 | 48 | }); 49 | 50 | this.client.services.blocker.read({ 51 | domain: 'subdomain.ads.quantserve.com' 52 | }, (response) => { 53 | 54 | assert(response, { 55 | domain: 'ads.quantserve.com' 56 | }); 57 | 58 | }); 59 | 60 | }); 61 | 62 | describe('Blocker.prototype.read()/failure', function(assert) { 63 | 64 | assert(this.client !== null); 65 | assert(isFunction(this.client.services.blocker.read), true); 66 | 67 | this.client.services.blocker.read({ 68 | domain: 'quantserve.com' 69 | }, (response) => { 70 | 71 | assert(response, null); 72 | 73 | }); 74 | 75 | }); 76 | 77 | after(disconnect_client); 78 | after(disconnect_stealth); 79 | 80 | 81 | export default finish('stealth/client/service/Blocker', { 82 | internet: false, 83 | network: true 84 | }); 85 | 86 | -------------------------------------------------------------------------------- /stealth/source/client/service/Redirect.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Emitter, isFunction, isObject } from '../../../extern/base.mjs'; 3 | 4 | 5 | 6 | const Redirect = function(client) { 7 | 8 | this.client = client; 9 | Emitter.call(this); 10 | 11 | }; 12 | 13 | 14 | Redirect.prototype = Object.assign({}, Emitter.prototype, { 15 | 16 | toJSON: function() { 17 | 18 | let blob = Emitter.prototype.toJSON.call(this); 19 | let data = { 20 | events: blob.data.events, 21 | journal: blob.data.journal 22 | }; 23 | 24 | return { 25 | 'type': 'Redirect Service', 26 | 'data': data 27 | }; 28 | 29 | }, 30 | 31 | read: function(payload, callback) { 32 | 33 | payload = isObject(payload) ? payload : null; 34 | callback = isFunction(callback) ? callback : null; 35 | 36 | 37 | if (payload !== null && callback !== null) { 38 | 39 | this.once('read', (response) => callback(response)); 40 | 41 | this.client.send({ 42 | headers: { 43 | service: 'redirect', 44 | method: 'read' 45 | }, 46 | payload: payload 47 | }); 48 | 49 | } else if (callback !== null) { 50 | callback(null); 51 | } 52 | 53 | }, 54 | 55 | remove: function(payload, callback) { 56 | 57 | payload = isObject(payload) ? payload : null; 58 | callback = isFunction(callback) ? callback : null; 59 | 60 | 61 | if (payload !== null && callback !== null) { 62 | 63 | this.once('remove', (result) => callback(result)); 64 | 65 | this.client.send({ 66 | headers: { 67 | service: 'redirect', 68 | method: 'remove' 69 | }, 70 | payload: payload 71 | }); 72 | 73 | } else if (callback !== null) { 74 | callback(false); 75 | } 76 | 77 | }, 78 | 79 | save: function(payload, callback) { 80 | 81 | payload = isObject(payload) ? payload : null; 82 | callback = isFunction(callback) ? callback : null; 83 | 84 | 85 | if (payload !== null && callback !== null) { 86 | 87 | this.once('save', (result) => callback(result)); 88 | 89 | this.client.send({ 90 | headers: { 91 | service: 'redirect', 92 | method: 'save' 93 | }, 94 | payload: payload 95 | }); 96 | 97 | } else if (callback !== null) { 98 | callback(false); 99 | } 100 | 101 | } 102 | 103 | }); 104 | 105 | 106 | export { Redirect }; 107 | 108 | -------------------------------------------------------------------------------- /browser/design/appbar/Settings.css: -------------------------------------------------------------------------------- 1 | 2 | browser-appbar-settings { 3 | display: inline-block; 4 | position: relative; 5 | width: auto; 6 | height: 32px; 7 | margin: 0px 0px 0px 32px; 8 | padding: 0px; 9 | line-height: 32px; 10 | text-align: center; 11 | vertical-align: top; 12 | white-space: nowrap; 13 | user-select: none; 14 | -webkit-user-select: none; 15 | } 16 | 17 | browser-appbar-settings button { 18 | display: inline-block; 19 | min-width: 32px; 20 | margin: 0px; 21 | padding: 0px; 22 | line-height: 32px; 23 | height: 32px; 24 | color: var(--element-default-color); 25 | font-size: 24px; 26 | text-align: center; 27 | background: var(--element-default-background); 28 | border: 1px solid transparent; 29 | border-radius: 4px; 30 | box-sizing: border-box; 31 | transition: 200ms all ease-out; 32 | appearance: none; 33 | -moz-appearance: none; 34 | -webkit-appearance: none; 35 | outline: none; 36 | cursor: pointer; 37 | } 38 | 39 | browser-appbar-settings button:before { 40 | display: inline-block; 41 | width: 100%; 42 | font-family: 'icon'; 43 | text-align: center; 44 | speak: none; 45 | -webkit-font-smoothing: antialiased; 46 | } 47 | 48 | browser-appbar-settings button.active { 49 | color: var(--element-active-color); 50 | } 51 | 52 | browser-appbar-settings button[disabled], 53 | browser-appbar-settings button[disabled]:hover { 54 | color: var(--element-disable-color); 55 | border: 1px solid transparent; 56 | background: var(--element-disable-background); 57 | cursor: not-allowed; 58 | } 59 | 60 | browser-appbar-settings button[disabled].active { 61 | color: var(--element-active-color); 62 | } 63 | 64 | browser-appbar-settings button:focus, 65 | browser-appbar-settings button:hover { 66 | color: var(--element-focus-color); 67 | border: 1px solid var(--element-focus-color); 68 | background: var(--element-focus-background); 69 | outline: none; 70 | } 71 | 72 | browser-appbar-settings button::-moz-focus-inner { 73 | border: 0px solid transparent; 74 | } 75 | 76 | browser-appbar-settings button[data-key="session"]:before { content: '\e1e2'; } 77 | browser-appbar-settings button[data-key="site"]:before { content: '\e869'; } 78 | browser-appbar-settings button[data-key="browser"]:before { content: '\e8b8'; } 79 | 80 | 81 | 82 | @media screen and (max-width: 800px) { 83 | 84 | browser-appbar-settings { 85 | margin-left: 0px; 86 | } 87 | 88 | } 89 | 90 | -------------------------------------------------------------------------------- /browser/design/card/Blocker.css: -------------------------------------------------------------------------------- 1 | 2 | browser-card-blocker { 3 | all: unset; 4 | display: block; 5 | position: relative; 6 | width: 800px; 7 | max-height: 64px; 8 | margin: 0px; 9 | padding: 16px; 10 | background: var(--surface-default-background); 11 | border: 1px solid var(--surface-default-color); 12 | border-radius: 8px; 13 | box-sizing: border-box; 14 | transition: 200ms all ease-out; 15 | overflow: hidden; 16 | cursor: default; 17 | } 18 | 19 | browser-card-blocker.active { 20 | max-height: unset; 21 | } 22 | 23 | browser-card-blocker > h3 { 24 | display: block; 25 | margin: 0px 0px 16px 0px; 26 | padding: 0px; 27 | } 28 | 29 | browser-card-blocker > h3:before { 30 | display: inline; 31 | content: '\e32a'; 32 | margin: 0px 16px 0px 0px; 33 | padding: 0px; 34 | font-family: 'icon'; 35 | font-size: 24px; 36 | font-weight: 400; 37 | text-align: center; 38 | vertical-align: middle; 39 | speak: none; 40 | -webkit-font-smoothing: antialiased; 41 | } 42 | 43 | browser-card-blocker > h3 input[type="text"] { 44 | display: inline-block; 45 | width: calc(100% - 32px - 32px - 24px); 46 | font-family: 'vera-mono'; 47 | font-size: 16px; 48 | font-weight: 400; 49 | line-height: 32px; 50 | } 51 | 52 | browser-card-blocker > button[data-action="toggle"] { 53 | display: block; 54 | position: absolute; 55 | top: 16px; 56 | right: 16px; 57 | bottom: auto; 58 | left: auto; 59 | } 60 | 61 | browser-card-blocker > button[data-action="toggle"]:before { 62 | content: '\e5c5'; 63 | transition: 200ms transform ease-out; 64 | } 65 | 66 | browser-card-blocker > button[data-action="toggle"].active:before { 67 | transform: rotate(180deg); 68 | transition: 200ms transform ease-out; 69 | } 70 | 71 | 72 | 73 | browser-card-blocker-article { 74 | display: none; 75 | width: 100%; 76 | height: auto; 77 | } 78 | 79 | browser-card-blocker.active browser-card-blocker-article { 80 | display: block; 81 | } 82 | 83 | 84 | 85 | browser-card-blocker-footer { 86 | display: none; 87 | height: 32px; 88 | margin: 16px 0px 0px 0px; 89 | padding: 0px; 90 | text-align: right; 91 | } 92 | 93 | browser-card-blocker.active browser-card-blocker-footer { 94 | display: block; 95 | } 96 | 97 | 98 | 99 | @media screen and (max-width: 800px) { 100 | 101 | browser-card-blocker { 102 | width: auto; 103 | } 104 | 105 | } 106 | 107 | -------------------------------------------------------------------------------- /browser/design/appbar/Mode.css: -------------------------------------------------------------------------------- 1 | 2 | browser-appbar-mode { 3 | display: inline-block; 4 | position: relative; 5 | width: auto; 6 | height: 32px; 7 | margin: 0px; 8 | padding: 0px; 9 | line-height: 32px; 10 | text-align: center; 11 | vertical-align: top; 12 | white-space: nowrap; 13 | user-select: none; 14 | -webkit-user-select: none; 15 | } 16 | 17 | browser-appbar-mode button { 18 | display: inline-block; 19 | min-width: 32px; 20 | margin: 0px; 21 | padding: 0px; 22 | line-height: 32px; 23 | height: 32px; 24 | color: var(--element-default-color); 25 | font-size: 24px; 26 | text-align: center; 27 | background: var(--element-default-background); 28 | border: 1px solid transparent; 29 | border-radius: 4px; 30 | box-sizing: border-box; 31 | transition: 200ms all ease-out; 32 | appearance: none; 33 | -moz-appearance: none; 34 | -webkit-appearance: none; 35 | outline: none; 36 | cursor: pointer; 37 | } 38 | 39 | browser-appbar-mode button:before { 40 | display: inline-block; 41 | width: 100%; 42 | font-family: 'icon'; 43 | text-align: center; 44 | speak: none; 45 | -webkit-font-smoothing: antialiased; 46 | } 47 | 48 | browser-appbar-mode button[disabled], 49 | browser-appbar-mode button[disabled]:hover { 50 | color: var(--element-disable-color); 51 | border: 1px solid transparent; 52 | background: var(--element-disable-background); 53 | cursor: not-allowed; 54 | } 55 | 56 | browser-appbar-mode button:focus, 57 | browser-appbar-mode button:hover { 58 | color: var(--element-focus-color); 59 | border: 1px solid var(--element-focus-color); 60 | background: var(--element-focus-background); 61 | outline: none; 62 | } 63 | 64 | browser-appbar-mode button::-moz-focus-inner { 65 | border: 0px solid transparent; 66 | } 67 | 68 | browser-appbar-mode input[data-key="domain"] { 69 | display: none; 70 | } 71 | 72 | browser-appbar-mode button[data-key="mode.text"]:before { content: '\e06f'; } 73 | browser-appbar-mode button[data-key="mode.image"]:before { content: '\e432'; } 74 | browser-appbar-mode button[data-key="mode.audio"]:before { content: '\e049'; } 75 | browser-appbar-mode button[data-key="mode.video"]:before { content: '\e404'; } 76 | browser-appbar-mode button[data-key="mode.other"]:before { content: '\e048'; } 77 | browser-appbar-mode button[data-val="true"] { color: var(--element-active-color); } 78 | browser-appbar-mode button[data-val="true"]:hover { color: var(--element-active-color); } 79 | 80 | -------------------------------------------------------------------------------- /base/review/Boolean.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { describe, finish } from '../../covert/index.mjs'; 3 | import { Boolean, isBoolean } from '../source/Boolean.mjs'; 4 | 5 | 6 | 7 | describe('Boolean.isBoolean()', function(assert) { 8 | 9 | let boolean1 = true; 10 | let boolean2 = false; 11 | let boolean3 = new Boolean(true); 12 | let boolean4 = new Boolean(false); 13 | 14 | assert(typeof Boolean.isBoolean, 'function'); 15 | 16 | assert(Boolean.isBoolean(boolean1), true); 17 | assert(Boolean.isBoolean(boolean2), true); 18 | assert(Boolean.isBoolean(boolean3), true); 19 | assert(Boolean.isBoolean(boolean4), true); 20 | 21 | }); 22 | 23 | describe('isBoolean()', function(assert) { 24 | 25 | let boolean1 = true; 26 | let boolean2 = false; 27 | let boolean3 = new Boolean(true); 28 | let boolean4 = new Boolean(false); 29 | 30 | assert(typeof isBoolean, 'function'); 31 | 32 | assert(isBoolean(boolean1), true); 33 | assert(isBoolean(boolean2), true); 34 | assert(isBoolean(boolean3), true); 35 | assert(isBoolean(boolean4), true); 36 | 37 | }); 38 | 39 | describe('Boolean.prototype.toString()', function(assert) { 40 | 41 | let boolean1 = true; 42 | let boolean2 = false; 43 | let boolean3 = new Boolean(true); 44 | let boolean4 = new Boolean(false); 45 | 46 | assert(Object.prototype.toString.call(boolean1), '[object Boolean]'); 47 | assert(Object.prototype.toString.call(boolean2), '[object Boolean]'); 48 | assert(Object.prototype.toString.call(boolean3), '[object Boolean]'); 49 | assert(Object.prototype.toString.call(boolean4), '[object Boolean]'); 50 | 51 | assert((boolean1).toString(), 'true'); 52 | assert((boolean2).toString(), 'false'); 53 | assert((boolean3).toString(), 'true'); 54 | assert((boolean4).toString(), 'false'); 55 | 56 | }); 57 | 58 | describe('Boolean.prototype.valueOf()', function(assert) { 59 | 60 | let boolean1 = true; 61 | let boolean2 = false; 62 | let boolean3 = new Boolean(true); 63 | let boolean4 = new Boolean(false); 64 | 65 | assert((boolean1).valueOf(), true); 66 | assert((boolean2).valueOf(), false); 67 | assert((boolean3).valueOf(), true); 68 | assert((boolean4).valueOf(), false); 69 | 70 | assert(JSON.stringify(boolean1), 'true'); 71 | assert(JSON.stringify(boolean2), 'false'); 72 | assert(JSON.stringify(boolean3), 'true'); 73 | assert(JSON.stringify(boolean4), 'false'); 74 | 75 | }); 76 | 77 | 78 | export default finish('base/Boolean', { 79 | internet: false, 80 | network: false 81 | }); 82 | 83 | -------------------------------------------------------------------------------- /browser/source/ENVIRONMENT.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { URL } from '../source/parser/URL.mjs'; 3 | 4 | 5 | 6 | const flags = ((global) => { 7 | 8 | let flags = { 9 | cause: null, 10 | code: null, 11 | debug: false, 12 | url: URL.parse() 13 | }; 14 | 15 | let tmp1 = (global.location || {}).search || ''; 16 | let tmp2 = (global.parent.location || {}).search || ''; 17 | if (tmp1.includes('debug') === true) { 18 | flags.debug = true; 19 | } else if (tmp2.includes('debug') === true) { 20 | flags.debug = true; 21 | } 22 | 23 | if (tmp1.startsWith('?') === true) { 24 | 25 | tmp1.substr(1).split('&').forEach((flag) => { 26 | 27 | let tmp = flag.split('='); 28 | if (tmp.length >= 2) { 29 | 30 | let key = tmp[0]; 31 | let val = tmp.slice(1).join('='); 32 | 33 | if (key === 'url') { 34 | 35 | flags[key] = URL.parse(decodeURIComponent(val)); 36 | 37 | } else { 38 | 39 | let num = parseInt(val, 10); 40 | if (Number.isNaN(num) === false && (num).toString() === val) { 41 | val = num; 42 | } 43 | 44 | if (val === 'true') val = true; 45 | if (val === 'false') val = false; 46 | if (val === 'null') val = null; 47 | 48 | flags[key] = val; 49 | 50 | } 51 | 52 | } 53 | 54 | }); 55 | 56 | } 57 | 58 | return flags; 59 | 60 | })(typeof window !== 'undefined' ? window : this); 61 | 62 | const hostname = ((global) => { 63 | 64 | let host = 'localhost'; 65 | 66 | let tmp1 = (global.location || {}).host || ''; 67 | if (tmp1.includes(':') === true) { 68 | 69 | let tmp2 = tmp1.split(':').slice(0, -1).join(':'); 70 | if (tmp2 !== 'localhost') { 71 | host = tmp2; 72 | } 73 | 74 | } else if (tmp1 !== '') { 75 | host = tmp1; 76 | } 77 | 78 | return host; 79 | 80 | })(typeof window !== 'undefined' ? window : this); 81 | 82 | const secure = ((global) => { 83 | 84 | let secure = true; 85 | 86 | let tmp1 = (global.location || {}).protocol || ''; 87 | if (tmp1.includes(':') === true) { 88 | 89 | let tmp2 = tmp1.split(':').shift(); 90 | if (tmp2 !== 'https') { 91 | secure = false; 92 | } 93 | 94 | } 95 | 96 | return secure; 97 | 98 | })(typeof window !== 'undefined' ? window : this); 99 | 100 | 101 | const ENVIRONMENT = { 102 | 103 | flags: flags, 104 | hostname: hostname, 105 | secure: secure 106 | 107 | }; 108 | 109 | 110 | export { ENVIRONMENT }; 111 | 112 | -------------------------------------------------------------------------------- /browser/internal/fix-host.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | stealth:fix-host 6 | 22 | 23 | 24 |
25 |

Fix Host

26 |

27 | Stealth could not connect to the cached hosts for this domain. 28 |

29 |

30 | Please verify the correctness of the IPv4/IPv6 addresses. 31 | If they are incorrect, set them manually. 32 |

33 |
34 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /.github/workflows/covert-scan.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Covert Scan 3 | 4 | on: [push] 5 | 6 | jobs: 7 | 8 | scan-base: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - run: | 15 | sudo apt-get install tcpdump 16 | sudo setcap cap_net_raw,cap_net_admin=eip /usr/bin/tcpdump 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: 14 20 | - run: npm install 21 | - run: node ./make.mjs build 22 | - run: | 23 | cd ./base 24 | node ../covert/covert.mjs scan "base/*" --debug=true --report=../.github/workflows/covert-scan-base 25 | - uses: actions/upload-artifact@v2 26 | with: 27 | name: Report for Base 28 | path: .github/workflows/covert-scan-base.* 29 | - run: node .github/workflows/covert-scan.mjs .github/workflows/covert-scan-base.report 30 | 31 | scan-covert: 32 | 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | - run: | 38 | sudo apt-get install tcpdump 39 | sudo setcap cap_net_raw,cap_net_admin=eip /usr/bin/tcpdump 40 | - uses: actions/setup-node@v1 41 | with: 42 | node-version: 14 43 | - run: npm install 44 | - run: node ./make.mjs build 45 | - run: | 46 | cd ./covert 47 | node ../covert/covert.mjs scan "covert/*" --debug=true --report=../.github/workflows/covert-scan-covert 48 | - uses: actions/upload-artifact@v2 49 | with: 50 | name: Report for Covert 51 | path: .github/workflows/covert-scan-covert.* 52 | - run: node .github/workflows/covert-scan.mjs .github/workflows/covert-scan-covert.report 53 | 54 | scan-stealth: 55 | 56 | runs-on: ubuntu-latest 57 | 58 | steps: 59 | - uses: actions/checkout@v2 60 | - run: | 61 | sudo apt-get install tcpdump 62 | sudo setcap cap_net_raw,cap_net_admin=eip /usr/bin/tcpdump 63 | - uses: actions/setup-node@v1 64 | with: 65 | node-version: 14 66 | - run: npm install 67 | - run: node ./make.mjs build 68 | - run: | 69 | cd ./stealth 70 | node ../covert/covert.mjs scan "stealth/*" --debug=true --report=../.github/workflows/covert-scan-stealth 71 | - uses: actions/upload-artifact@v2 72 | with: 73 | name: Report for Stealth 74 | path: .github/workflows/covert-scan-stealth.* 75 | - run: node .github/workflows/covert-scan.mjs .github/workflows/covert-scan-stealth.report 76 | 77 | -------------------------------------------------------------------------------- /browser/design/sheet/Console.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Assistant } from '../Assistant.mjs'; 3 | import { Element } from '../Element.mjs'; 4 | import { Widget } from '../Widget.mjs'; 5 | 6 | 7 | 8 | const ASSISTANT = new Assistant({ 9 | name: 'Console', 10 | widget: 'sheet/Console', 11 | events: { 12 | 'hide': 'Hiding Console.', 13 | 'show': 'Showing Console.' 14 | } 15 | }); 16 | 17 | const Console = function(/* browser */) { 18 | 19 | this.button = new Element('button'); 20 | this.console = Element.query('base-console'); 21 | this.element = new Element('browser-sheet-console', [ 22 | this.button 23 | ]); 24 | 25 | this.button.attr('title', 'Toggle visibility of Developer Console'); 26 | 27 | this.button.on('click', () => { 28 | 29 | if (this.element.state() === 'active') { 30 | this.element.emit('hide'); 31 | } else { 32 | this.element.emit('show'); 33 | } 34 | 35 | }); 36 | 37 | if (this.console !== null) { 38 | 39 | this.console.on('contextmenu', (e) => { 40 | 41 | let context = Widget.query('browser-menu-context'); 42 | if (context !== null) { 43 | 44 | let element = Element.toElement(e.target); 45 | if (element.type === 'i') { 46 | element = Element.toElement(e.target.parentNode); 47 | } 48 | 49 | if (element.type === 'base-console-line') { 50 | 51 | let area = element.area(); 52 | if (area !== null) { 53 | 54 | context.value([{ 55 | label: 'copy', 56 | value: element.value() 57 | }]); 58 | 59 | context.area({ 60 | x: e.x, 61 | y: e.y 62 | }); 63 | 64 | context.emit('show'); 65 | 66 | } 67 | 68 | } 69 | 70 | } 71 | 72 | }); 73 | 74 | } 75 | 76 | this.element.on('show', () => { 77 | 78 | if (this.console === null) { 79 | this.console = Element.query('base-console'); 80 | } 81 | 82 | if (this.console !== null) { 83 | this.console.state('active'); 84 | } 85 | 86 | this.element.state('active'); 87 | this.button.state('active'); 88 | ASSISTANT.emit('show'); 89 | 90 | }); 91 | 92 | this.element.on('hide', () => { 93 | 94 | if (this.console === null) { 95 | this.console = Element.query('base-console'); 96 | } 97 | 98 | if (this.console !== null) { 99 | this.console.state(''); 100 | } 101 | 102 | if (this.element.state() === 'active') { 103 | this.element.state(''); 104 | this.button.state(''); 105 | ASSISTANT.emit('hide'); 106 | } 107 | 108 | }); 109 | 110 | 111 | Widget.call(this); 112 | 113 | }; 114 | 115 | 116 | Console.prototype = Object.assign({}, Widget.prototype); 117 | 118 | 119 | export { Console }; 120 | 121 | -------------------------------------------------------------------------------- /base/review/Object.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { describe, finish } from '../../covert/index.mjs'; 3 | import { Object, isObject } from '../source/Object.mjs'; 4 | 5 | 6 | 7 | describe('Object.isObject()', function(assert) { 8 | 9 | let object1 = { foo: 'bar' }; 10 | let object2 = new Object({ foo: 'bar' }); 11 | 12 | assert(typeof Object.isObject, 'function'); 13 | 14 | assert(Object.isObject(object1), true); 15 | assert(Object.isObject(object2), true); 16 | 17 | }); 18 | 19 | describe('isObject()', function(assert) { 20 | 21 | let object1 = { foo: 'bar' }; 22 | let object2 = new Object({ foo: 'bar' }); 23 | 24 | assert(typeof isObject, 'function'); 25 | 26 | assert(isObject(object1), true); 27 | assert(isObject(object2), true); 28 | 29 | }); 30 | 31 | describe('Object.clone()', function(assert) { 32 | 33 | let object1 = { foo: 'bar' }; 34 | let object2 = new Object({ foo: 'bar' }); 35 | let object3 = { foo: 'bar', qux: [ 1, 3, 3, 7 ] }; 36 | let object4 = new Object({ foo: 'bar', qux: [ 1, 3, 3, 7 ] }); 37 | 38 | let target3 = {}; 39 | let target4 = {}; 40 | let clone1 = Object.clone(null, object1); 41 | let clone2 = Object.clone(null, object2); 42 | let clone3 = Object.clone(target3, object3); 43 | let clone4 = Object.clone(target4, object4); 44 | 45 | assert(isObject(clone1), true); 46 | assert(isObject(clone2), true); 47 | assert(isObject(clone3), true); 48 | assert(isObject(clone4), true); 49 | 50 | assert(object1 === clone1, false); 51 | assert(object2 === clone2, false); 52 | assert(object3 === clone3, false); 53 | assert(object4 === clone4, false); 54 | 55 | assert(target3 === clone3, true); 56 | assert(target4 === clone4, true); 57 | 58 | assert(object1, clone1); 59 | assert(object2, clone2); 60 | assert(object3, clone3); 61 | assert(object4, clone4); 62 | 63 | }); 64 | 65 | describe('Object.prototype.toString()', function(assert) { 66 | 67 | let object1 = { foo: 'bar' }; 68 | let object2 = new Object({ foo: 'bar' }); 69 | 70 | assert(Object.prototype.toString.call(object1), '[object Object]'); 71 | assert(Object.prototype.toString.call(object2), '[object Object]'); 72 | 73 | assert((object1).toString(), '[object Object]'); 74 | assert((object2).toString(), '[object Object]'); 75 | 76 | }); 77 | 78 | describe('Object.prototype.valueOf()', function(assert) { 79 | 80 | let object1 = { foo: 'bar' }; 81 | let object2 = new Object({ foo: 'bar' }); 82 | 83 | assert((object1).valueOf(), object1); 84 | assert((object2).valueOf(), object2); 85 | 86 | assert(JSON.stringify(object1), '{"foo":"bar"}'); 87 | assert(JSON.stringify(object2), '{"foo":"bar"}'); 88 | 89 | }); 90 | 91 | 92 | export default finish('base/Object', { 93 | internet: false, 94 | network: false 95 | }); 96 | 97 | -------------------------------------------------------------------------------- /covert/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Covert Suite 3 | 4 | The Covert Suite is a test framework and test runner that allows to test 5 | and verify implementations of peer-to-peer network services, and the 6 | simulation of network behaviours (2G, 3G, 4G). 7 | 8 | The idea of its concept is that it can help Developers understand their 9 | code, and that Reviews can be executed multiple times in a stateless 10 | manner; even in parallel while fixing bugs in an implementation. 11 | 12 | All Reviews have been implemented and maintained in a manner so that they 13 | can be used as a reference point for future third-party clients. 14 | 15 | By default, it uses the [stealth/Client](../stealth/source/Client.mjs) 16 | which is written for node.js, so no external libraries and no external 17 | programs are necessary. 18 | 19 | 20 | ## Requirements 21 | 22 | Currently, Covert runs officially only on Arch Linux, though it might be 23 | possible to run it on MacOS Mojave and later, too. 24 | 25 | In order to simulate end-to-end throttled peer-to-peer Networking correctly, 26 | Covert requires these external packages on minimalistic Unix/Linux systems: 27 | 28 | ```bash 29 | # Install necessary packages 30 | pacman -S --needed iproute2 kmod net-tools sudo tcpdump 31 | ``` 32 | 33 | Additionally, the following ports have to be allowed in the Firewall to 34 | transmit and receive of both UDP/TCP data: 35 | 36 | - `80` (`--internet=true`) for [stealth/connection/HTTP](../stealth/review/connection/HTTP.mjs) and [stealth/connection/WS](../stealth/review/connection/WS.mjs). 37 | - `443` (`--internet=true`) for [stealth/connection/HTTPS](../stealth/review/connection/HTTPS.mjs) and [stealth/connection/WSS](../stealth/review/connection/WSS.mjs). 38 | - `13337` (`--internet=true`) for [stealth/connection/HTTP](../stealth/review/connection/HTTP.mjs) and [stealth/connection/WS](../stealth/review/connection/WS.mjs). 39 | - `65432` for [stealth/Client](../stealth/review/Client.mjs) and [stealth/Server](../stealth/review/Server.mjs). 40 | 41 | 42 | ## Quickstart 43 | 44 | - Install [node.js](https://nodejs.org/en/download) latest (minimum version `12`). 45 | 46 | ```bash 47 | cd /path/to/stealth; 48 | 49 | # Build Stealth and Covert 50 | node ./covert/make.mjs; 51 | 52 | # Show Help 53 | node ./covert/covert.mjs; 54 | ``` 55 | 56 | 57 | ## ECMAScript Usage 58 | 59 | The Covert Library exports a `default` export that contains a namespaced 60 | `Object`. Due to otherwise conflicting names, there are no separately 61 | named exports available. 62 | 63 | ```javascript 64 | // Import Covert Library 65 | import covert from './covert/index.mjs'; 66 | ``` 67 | 68 | 69 | ## NPM Usage 70 | 71 | ```javascript 72 | // Import Covert Library 73 | import covert from 'stealth/covert'; 74 | ``` 75 | 76 | -------------------------------------------------------------------------------- /browser/design/appbar/Splitter.css: -------------------------------------------------------------------------------- 1 | 2 | browser-appbar-splitter { 3 | display: block; 4 | position: absolute; 5 | width: 100%; 6 | height: 34px; 7 | line-height: 32px; 8 | top: 0px; 9 | right: 32px; 10 | bottom: 0px; 11 | left: 0px; 12 | border: 0px solid transparent; 13 | border-bottom: 1px solid var(--surface-default-color); 14 | background: var(--surface-default-foreground); 15 | box-sizing: border-box; 16 | text-align: left; 17 | vertical-align: middle; 18 | pointer-events: none; 19 | transition: 200ms all ease-out; 20 | opacity: 0; 21 | } 22 | 23 | browser-appbar-splitter.active { 24 | pointer-events: all; 25 | transition: 200ms all ease-out; 26 | opacity: 1; 27 | } 28 | 29 | browser-appbar button[data-key="splitter"] { 30 | display: inline-block; 31 | position: relative; 32 | min-width: 32px; 33 | margin: 0px; 34 | padding: 0px; 35 | line-height: 32px; 36 | height: 32px; 37 | color: var(--element-default-color); 38 | font-size: 24px; 39 | text-align: center; 40 | background: var(--element-default-background); 41 | border: 1px solid transparent; 42 | border-radius: 4px; 43 | box-sizing: border-box; 44 | transition: 200ms all ease-out; 45 | vertical-align: top; 46 | user-select: none; 47 | -webkit-user-select: none; 48 | appearance: none; 49 | -moz-appearance: none; 50 | -webkit-appearance: none; 51 | outline: none; 52 | cursor: pointer; 53 | } 54 | 55 | browser-appbar button[data-key="splitter"].active { 56 | color: var(--element-active-color); 57 | } 58 | 59 | browser-appbar button[data-key="splitter"]:before { 60 | display: inline-block; 61 | width: 100%; 62 | font-family: 'icon'; 63 | text-align: center; 64 | speak: none; 65 | -webkit-font-smoothing: antialiased; 66 | content: '\e5d4'; 67 | } 68 | 69 | browser-appbar-splitter > browser-appbar-mode { 70 | width: calc(100% - 128px - 96px); 71 | } 72 | 73 | 74 | 75 | @media screen and (max-width: 640px) { 76 | 77 | browser-appbar-splitter { 78 | width: calc(100% - 32px); 79 | } 80 | 81 | } 82 | 83 | @media screen and (max-width: 412px) { 84 | 85 | browser-appbar-splitter { 86 | width: 100%; 87 | height: 64px; 88 | top: 0px; 89 | right: 0px; 90 | bottom: auto; 91 | left: 0px; 92 | transform: translate(0%, -100%); 93 | } 94 | 95 | browser-appbar-splitter.active { 96 | transform: translate(0%, 0%); 97 | } 98 | 99 | browser-appbar-splitter > browser-appbar-settings { 100 | width: calc(100% - 128px - 32px); 101 | text-align: right; 102 | } 103 | 104 | browser-appbar-splitter > browser-appbar-mode { 105 | display: block; 106 | position: absolute; 107 | width: 100%; 108 | top: 32px; 109 | right: 0px; 110 | bottom: auto; 111 | left: 0px; 112 | } 113 | 114 | } 115 | 116 | -------------------------------------------------------------------------------- /base/review/Array.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { describe, finish } from '../../covert/index.mjs'; 3 | import { Array, isArray } from '../source/Array.mjs'; 4 | 5 | 6 | 7 | describe('Array.isArray()', function(assert) { 8 | 9 | let array1 = [ 1, 3, 3, 7 ]; 10 | let array2 = new Array(1, 3, 3, 7); 11 | 12 | assert(typeof Array.isArray, 'function'); 13 | 14 | assert(Array.isArray(array1), true); 15 | assert(Array.isArray(array2), true); 16 | 17 | }); 18 | 19 | describe('isArray()', function(assert) { 20 | 21 | let array1 = [ 1, 3, 3, 7 ]; 22 | let array2 = new Array(1, 3, 3, 7); 23 | 24 | assert(typeof isArray, 'function'); 25 | 26 | assert(isArray(array1), true); 27 | assert(isArray(array2), true); 28 | 29 | }); 30 | 31 | describe('Array.prototype.remove()', function(assert) { 32 | 33 | let array1 = [ 1, 3, 3, 7 ]; 34 | let array2 = new Array(1, 3, 3, 7); 35 | 36 | assert(array1.remove(3), array1); 37 | assert(array2.remove(3), array1); 38 | 39 | assert(array1, [ 1, 7 ]); 40 | assert(array2, new Array(1, 7)); 41 | 42 | }); 43 | 44 | describe('Array.prototype.removeEvery()', function(assert) { 45 | 46 | let array1 = [ 47 | { num: 1, str: 'one' }, 48 | { num: 3, str: 'three' }, 49 | { num: 3, str: 'three' }, 50 | { num: 7, str: 'seven' } 51 | ]; 52 | 53 | let array2 = [ 54 | { num: 1, str: 'one' }, 55 | { num: 3, str: 'three' }, 56 | { num: 3, str: 'three' }, 57 | { num: 7, str: 'seven' } 58 | ]; 59 | 60 | let return1 = array1.removeEvery((cell) => cell.num === 3); 61 | let return2 = array2.removeEvery((cell) => cell.str === 'three'); 62 | 63 | assert(return1, array1); 64 | assert(return2, array2); 65 | 66 | assert(array1, [ 67 | { num: 1, str: 'one' }, 68 | { num: 7, str: 'seven' } 69 | ]); 70 | 71 | assert(array2, [ 72 | { num: 1, str: 'one' }, 73 | { num: 7, str: 'seven' } 74 | ]); 75 | 76 | }); 77 | 78 | describe('Array.prototype.toString()', function(assert) { 79 | 80 | let array1 = [ 1, 3, 3, 7 ]; 81 | let array2 = new Array(1, 3, 3, 7); 82 | 83 | assert(Object.prototype.toString.call(array1), '[object Array]'); 84 | assert(Object.prototype.toString.call(array2), '[object Array]'); 85 | 86 | assert(array1.toString(), '1,3,3,7'); 87 | assert(array2.toString(), '1,3,3,7'); 88 | 89 | }); 90 | 91 | describe('Array.prototype.valueOf()', function(assert) { 92 | 93 | let array1 = [ 1, 3, 3, 7 ]; 94 | let array2 = new Array(1, 3, 3, 7); 95 | 96 | assert(array1.valueOf(), array1); 97 | assert(array2.valueOf(), array2); 98 | 99 | assert(JSON.stringify(array1), '[1,3,3,7]'); 100 | assert(JSON.stringify(array2), '[1,3,3,7]'); 101 | 102 | }); 103 | 104 | 105 | export default finish('base/Array', { 106 | internet: false, 107 | network: false 108 | }); 109 | 110 | -------------------------------------------------------------------------------- /browser/design/widget/Audio.css: -------------------------------------------------------------------------------- 1 | 2 | browser-widget-audio { 3 | all: unset; 4 | display: block; 5 | position: relative; 6 | width: 800px; 7 | margin: 0px; 8 | padding: 16px; 9 | background: var(--surface-default-background); 10 | border: 1px solid var(--surface-default-color); 11 | border-radius: 8px; 12 | box-sizing: border-box; 13 | transition: 200ms all ease-out; 14 | overflow: hidden; 15 | cursor: default; 16 | } 17 | 18 | 19 | 20 | browser-widget-audio-article { 21 | display: block; 22 | position: relative; 23 | width: 100%; 24 | height: auto; 25 | text-align: center; 26 | overflow: hidden; 27 | z-index: 1; 28 | } 29 | 30 | 31 | 32 | browser-widget-audio-footer { 33 | display: block; 34 | height: 32px; 35 | margin: 0px; 36 | padding: 0px; 37 | text-align: left; 38 | z-index: 2; 39 | } 40 | 41 | browser-widget-audio-footer > button[data-action="play"], 42 | browser-widget-audio-footer > button[data-action="pause"] { 43 | margin: 0px 16px 0px 0px; 44 | } 45 | 46 | browser-widget-audio-footer > button[data-action="download"] { 47 | margin: 0px 0px 0px 16px; 48 | } 49 | 50 | browser-widget-audio-footer > button[data-action="download"]:before { content: '\e2c4'; } 51 | browser-widget-audio-footer > button[data-action="play"]:before { content: '\e037'; } 52 | browser-widget-audio-footer > button[data-action="pause"]:before { content: '\e034'; } 53 | 54 | browser-widget-audio-footer > progress { 55 | display: inline-block; 56 | width: calc(100% - 48px - 48px); 57 | height: 16px; 58 | line-height: 30px; 59 | border: 1px solid transparent; 60 | border-radius: 4px; 61 | box-sizing: border-box; 62 | appearance: none; 63 | -moz-appearance: none; 64 | -webkit-appearance: none; 65 | vertical-align: middle; 66 | cursor: col-resize; 67 | } 68 | 69 | browser-widget-audio-footer > progress::-moz-progress-bar { 70 | display: inline-block; 71 | height: 100%; 72 | background: var(--element-focus-color); 73 | border-radius: 4px 0px 0px 4px; 74 | vertical-align: middle; 75 | box-sizing: border-box; 76 | } 77 | 78 | browser-widget-audio-footer > progress[value="100"]::-moz-progress-bar { 79 | border-radius: 4px; 80 | } 81 | 82 | browser-widget-audio-footer > progress::-webkit-progress-bar { 83 | background: var(--element-default-color); 84 | border-radius: 4px; 85 | } 86 | 87 | browser-widget-audio-footer > progress::-webkit-progress-value { 88 | background: var(--element-focus-color); 89 | border-radius: 4px 0px 0px 4px; 90 | } 91 | 92 | browser-widget-audio-footer > progress[value="100"]::-webkit-progress-value { 93 | border-radius: 4px; 94 | } 95 | 96 | 97 | 98 | @media screen and (max-width: 800px) { 99 | 100 | browser-widget-audio { 101 | width: auto; 102 | } 103 | 104 | } 105 | 106 | -------------------------------------------------------------------------------- /browser/design/appbar/Splitter.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Element } from '../Element.mjs'; 3 | import { Widget } from '../Widget.mjs'; 4 | 5 | 6 | 7 | const update = function() { 8 | 9 | let appbar = Element.query('browser-appbar'); 10 | let element = this.element; 11 | 12 | if (appbar !== null && element !== null) { 13 | 14 | this.appbar['address'].erase(); 15 | this.appbar['history'].erase(); 16 | this.appbar['mode'].erase(); 17 | this.appbar['settings'].erase(); 18 | this.button.erase(); 19 | 20 | if (this.__state.mobile === true) { 21 | 22 | this.appbar['history'].render(element); 23 | 24 | this.appbar['address'].render(appbar); 25 | this.button.render(appbar); 26 | 27 | this.appbar['mode'].render(element); 28 | this.appbar['settings'].render(element); 29 | 30 | element.render(appbar); 31 | 32 | } else { 33 | 34 | this.appbar['history'].render(appbar); 35 | this.appbar['address'].render(appbar); 36 | this.appbar['mode'].render(appbar); 37 | this.appbar['settings'].render(appbar); 38 | 39 | element.erase(); 40 | 41 | } 42 | 43 | } 44 | 45 | }; 46 | 47 | 48 | 49 | const Splitter = function(/* browser */) { 50 | 51 | this.element = new Element('browser-appbar-splitter'); 52 | 53 | this.__state = { 54 | mobile: false 55 | }; 56 | 57 | 58 | this.appbar = { 59 | 'history': Widget.query('browser-appbar-history'), 60 | 'address': Widget.query('browser-appbar-address'), 61 | 'mode': Widget.query('browser-appbar-mode'), 62 | 'settings': Widget.query('browser-appbar-settings') 63 | }; 64 | 65 | this.button = new Element('button'); 66 | this.button.attr('data-key', 'splitter'); 67 | this.button.attr('title', 'Show additional Settings'); 68 | 69 | this.button.on('click', () => { 70 | 71 | if (this.element.state() === 'active') { 72 | this.element.emit('hide'); 73 | } else { 74 | this.element.emit('show'); 75 | } 76 | 77 | }); 78 | 79 | this.element.on('hide', () => { 80 | 81 | this.button.state(''); 82 | this.element.state(''); 83 | 84 | }); 85 | 86 | this.element.on('resize', (width /*, height */) => { 87 | 88 | let old_state = this.__state.mobile; 89 | let new_state = old_state; 90 | 91 | if (width < 640) { 92 | new_state = true; 93 | } else { 94 | new_state = false; 95 | } 96 | 97 | if (old_state !== new_state) { 98 | this.__state.mobile = new_state; 99 | update.call(this); 100 | } 101 | 102 | }); 103 | 104 | this.element.on('show', () => { 105 | 106 | this.button.state('active'); 107 | this.element.state('active'); 108 | 109 | }); 110 | 111 | 112 | Widget.call(this); 113 | 114 | }; 115 | 116 | 117 | Splitter.prototype = Object.assign({}, Widget.prototype); 118 | 119 | 120 | export { Splitter }; 121 | 122 | -------------------------------------------------------------------------------- /browser/design/common/layout.css: -------------------------------------------------------------------------------- 1 | 2 | ::selection { 3 | color: var(--element-select-color); 4 | border-radius: 4px; 5 | background: var(--element-select-background); 6 | cursor: text; 7 | } 8 | 9 | ::-moz-selection { 10 | color: var(--element-select-color); 11 | border-radius: 4px; 12 | background: var(--element-select-background); 13 | cursor: text; 14 | } 15 | 16 | ::-webkit-scrollbar { 17 | width: 16px; 18 | border: 0px solid transparent; 19 | background: var(--scrollbar-default-background); 20 | } 21 | 22 | ::-webkit-scrollbar-thumb { 23 | width: 16px; 24 | border: 0px solid transparent; 25 | border-radius: 2px; 26 | background: var(--scrollbar-default-color); 27 | } 28 | 29 | 30 | 31 | body { 32 | margin: 0px; 33 | padding: 0px; 34 | } 35 | 36 | body > article { 37 | display: block; 38 | width: 800px; 39 | margin: 64px auto 0px auto; 40 | padding: 16px; 41 | font-family: 'museo'; 42 | font-size: 16px; 43 | background: var(--surface-default-background); 44 | border: 1px solid var(--surface-default-color); 45 | border-radius: 8px; 46 | box-sizing: border-box; 47 | } 48 | 49 | body > article h3 { 50 | margin: 0px 0px 16px 0px; 51 | padding: 0px; 52 | font-family: 'crystalline'; 53 | font-size: 32px; 54 | font-weight: 500; 55 | line-height: 32px; 56 | text-transform: uppercase; 57 | border-bottom: 1px solid var(--surface-default-color); 58 | } 59 | 60 | body > article p { 61 | margin: 0px 0px 16px 0px; 62 | font-family: 'museo'; 63 | font-size: 16px; 64 | text-align: justify; 65 | } 66 | 67 | body > article code { 68 | display: block; 69 | margin: 0px; 70 | padding: 4px; 71 | font-family: 'vera-mono'; 72 | color: var(--layout-default-color); 73 | background: var(--layout-default-background); 74 | border-radius: 4px; 75 | } 76 | 77 | body > article table { 78 | width: 100%; 79 | text-align: center; 80 | } 81 | 82 | body > article p + table { 83 | margin-top: 32px; 84 | } 85 | 86 | body > article table th { 87 | margin: 0px; 88 | padding: 0px; 89 | font-family: 'crystalline'; 90 | font-size: 24px; 91 | font-weight: 500; 92 | line-height: 32px; 93 | text-align: center; 94 | text-transform: uppercase; 95 | } 96 | 97 | body > article table td { 98 | margin: 0px; 99 | padding: 8px 8px; 100 | font-size: 16px; 101 | line-height: 16px; 102 | font-family: 'museo'; 103 | border-top: 1px solid var(--surface-default-color); 104 | } 105 | 106 | body > footer { 107 | display: block; 108 | width: 100%; 109 | margin: 64px 0px 0px 0px; 110 | padding: 16px; 111 | font-family: 'museo'; 112 | text-align: center; 113 | font-size: 16px; 114 | box-sizing: border-box; 115 | } 116 | 117 | 118 | 119 | @media screen and (max-width: 800px) { 120 | 121 | body > article { 122 | width: auto; 123 | } 124 | 125 | } 126 | 127 | -------------------------------------------------------------------------------- /stealth/review/connection/WHOIS.mjs: -------------------------------------------------------------------------------- 1 | 2 | import net from 'net'; 3 | 4 | import { Buffer, isArray, isBuffer, isFunction, isObject } from '../../../base/index.mjs'; 5 | import { describe, finish } from '../../../covert/index.mjs'; 6 | import { WHOIS } from '../../../stealth/source/connection/WHOIS.mjs'; 7 | import { IP } from '../../../stealth/source/parser/IP.mjs'; 8 | import { URL } from '../../../stealth/source/parser/URL.mjs'; 9 | 10 | 11 | 12 | describe('WHOIS.connect()', function(assert) { 13 | 14 | assert(isFunction(WHOIS.connect), true); 15 | 16 | let url = Object.assign(URL.parse('whois://whois.ripe.net'), { hosts: [ IP.parse('193.0.6.135'), IP.parse('2001:067c:02e8:0022:0000:0000:c100:0687') ] }); 17 | let connection = WHOIS.connect(url); 18 | 19 | connection.once('@connect', () => { 20 | 21 | assert(true); 22 | 23 | setTimeout(() => { 24 | connection.disconnect(); 25 | }, 0); 26 | 27 | }); 28 | 29 | connection.once('@disconnect', () => { 30 | assert(true); 31 | }); 32 | 33 | }); 34 | 35 | describe('WHOIS.disconnect()', function(assert) { 36 | 37 | assert(isFunction(WHOIS.disconnect), true); 38 | 39 | let url = Object.assign(URL.parse('whois://whois.ripe.net'), { hosts: [ IP.parse('193.0.6.135'), IP.parse('2001:067c:02e8:0022:0000:0000:c100:0687') ] }); 40 | let connection = WHOIS.connect(url); 41 | 42 | connection.once('@connect', () => { 43 | 44 | assert(true); 45 | 46 | setTimeout(() => { 47 | assert(WHOIS.disconnect(connection), true); 48 | }, 0); 49 | 50 | }); 51 | 52 | connection.once('@disconnect', () => { 53 | assert(true); 54 | }); 55 | 56 | }); 57 | 58 | // TODO: WHOIS.receive() for AFRINIC format 59 | // TODO: WHOIS.receive() for APNIC format 60 | // TODO: WHOIS.receive() for ARIN format 61 | // TODO: WHOIS.receive() for LACNIC format 62 | // TODO: WHOIS.receive() for RIPE format 63 | 64 | describe('WHOIS.send()/RIPE', function(assert, console) { 65 | 66 | assert(isFunction(WHOIS.send), true); 67 | 68 | let url = Object.assign(URL.parse('whois://whois.ripe.net'), { hosts: [ IP.parse('193.0.6.135'), IP.parse('2001:067c:02e8:0022:0000:0000:c100:0687') ] }); 69 | let connection = WHOIS.connect(url); 70 | 71 | connection.once('response', (response) => { 72 | console.log(response); 73 | }); 74 | 75 | connection.once('@connect', () => { 76 | 77 | assert(true); 78 | 79 | setTimeout(() => { 80 | assert(WHOIS.disconnect(connection), true); 81 | }, 0); 82 | 83 | }); 84 | 85 | connection.once('@disconnect', () => { 86 | assert(true); 87 | }); 88 | 89 | }); 90 | 91 | 92 | export default finish('stealth/connection/WHOIS', { 93 | internet: true, 94 | network: true, 95 | ports: [ 43, 13337 ] 96 | }); 97 | 98 | -------------------------------------------------------------------------------- /browser/internal/fix-connection.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | stealth:fix-connection 6 | 14 | 15 | 16 |
17 |

Fix Connection

18 |

19 | Stealth could not request the entered URL successfully, 20 | because either the internet connection broke down or 21 | the server is a teapot. 22 |

23 |

24 | The server responded with incorrect HTTP headers with status that don't make sense. 25 |

26 |

27 | The server responded with a payload that seems to be incomplete or 28 | injected by a third-party and therefore potentially malicious. 29 |

30 |

31 | The configured Proxy does not support anonymous SOCKS5 tunnels. 32 |

33 |

34 | The socket connection is too unstable to resume the download. 35 | Please check the stability of your internet connection. 36 |

37 |

38 | The socket encryption could not be verified. Please double-check your 39 | backyard and windows, the Feds or some Blackhat might be listening. 40 |

41 |
42 |
43 |

Download via Peers

44 |

45 | TODO: Peer Download 46 |

47 |
48 |
49 |

Download via Web Archive

50 |

51 | TODO: Web Archive Download 52 |

53 |
54 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /stealth/source/client/service/Host.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Emitter, isFunction, isObject } from '../../../extern/base.mjs'; 3 | 4 | 5 | 6 | const Host = function(client) { 7 | 8 | this.client = client; 9 | Emitter.call(this); 10 | 11 | }; 12 | 13 | 14 | Host.prototype = Object.assign({}, Emitter.prototype, { 15 | 16 | toJSON: function() { 17 | 18 | let blob = Emitter.prototype.toJSON.call(this); 19 | let data = { 20 | events: blob.data.events, 21 | journal: blob.data.journal 22 | }; 23 | 24 | return { 25 | 'type': 'Host Service', 26 | 'data': data 27 | }; 28 | 29 | }, 30 | 31 | read: function(payload, callback) { 32 | 33 | payload = isObject(payload) ? payload : null; 34 | callback = isFunction(callback) ? callback : null; 35 | 36 | 37 | if (payload !== null && callback !== null) { 38 | 39 | this.once('read', (response) => callback(response)); 40 | 41 | this.client.send({ 42 | headers: { 43 | service: 'host', 44 | method: 'read' 45 | }, 46 | payload: payload 47 | }); 48 | 49 | } else if (callback !== null) { 50 | callback(null); 51 | } 52 | 53 | }, 54 | 55 | resolve: function(payload, callback) { 56 | 57 | payload = isObject(payload) ? payload : null; 58 | callback = isFunction(callback) ? callback : null; 59 | 60 | 61 | if (payload !== null && callback !== null) { 62 | 63 | this.once('resolve', (response) => callback(response)); 64 | 65 | this.client.send({ 66 | headers: { 67 | service: 'host', 68 | method: 'resolve' 69 | }, 70 | payload: payload 71 | }); 72 | 73 | } else if (callback !== null) { 74 | callback(null); 75 | } 76 | 77 | }, 78 | 79 | remove: function(payload, callback) { 80 | 81 | payload = isObject(payload) ? payload : null; 82 | callback = isFunction(callback) ? callback : null; 83 | 84 | 85 | if (payload !== null && callback !== null) { 86 | 87 | this.once('remove', (result) => callback(result)); 88 | 89 | this.client.send({ 90 | headers: { 91 | service: 'host', 92 | method: 'remove' 93 | }, 94 | payload: payload 95 | }); 96 | 97 | } else if (callback !== null) { 98 | callback(false); 99 | } 100 | 101 | }, 102 | 103 | save: function(payload, callback) { 104 | 105 | payload = isObject(payload) ? payload : null; 106 | callback = isFunction(callback) ? callback : null; 107 | 108 | 109 | if (payload !== null && callback !== null) { 110 | 111 | this.once('save', (result) => callback(result)); 112 | 113 | this.client.send({ 114 | headers: { 115 | service: 'host', 116 | method: 'save' 117 | }, 118 | payload: payload 119 | }); 120 | 121 | } else if (callback !== null) { 122 | callback(false); 123 | } 124 | 125 | } 126 | 127 | }); 128 | 129 | 130 | export { Host }; 131 | 132 | -------------------------------------------------------------------------------- /guide/concept/Service.md: -------------------------------------------------------------------------------- 1 | 2 | # Stealth Service API 3 | 4 | Stealth's Service API can be accessed via `WS/13` Sockets. 5 | 6 | 7 | ## Request Data and Response Data 8 | 9 | Both the requests and responses have the same `headers` format: 10 | 11 | - `service (String)` is the identifier of the equivalent Service. 12 | - `method (optional String)` is the name of the method on the Service's instance. 13 | - `event (optional String)` is the name of the event that is `emit()`ed on the Service's instance. 14 | 15 | The request `payload` varies from Service to Service, but the API 16 | always aims to be compatible with the `URL.parse()` syntax, and reuses 17 | the otherwise empty `headers` and `payload` property on the reference. 18 | 19 | For detailed usage, take a look at the [stealth/client](/stealth/review/client) 20 | Reviews that explain all typical API usage scenarios. 21 | 22 | The client-side and server-side Reviews can be run via Covert like this: 23 | 24 | ```bash 25 | cd ./path/to/stealth; 26 | 27 | cd ./stealth; 28 | 29 | node ../covert/covert.mjs scan stealth/server/service/*; 30 | node ../covert/covert.mjs scan stealth/client/service/*; 31 | ``` 32 | 33 | 34 | ## WebSocket API (WS/13) 35 | 36 | These services are available on port `65432` via the `WS/13` Protocol. The 37 | `WebSocket Protocol` has to be set to `stealth` in order to make a successful 38 | handshake, other `Upgrade` requests are disconnected immediately. 39 | 40 | **IMPORTANT**: Stealth's [WS](/stealth/source/connection/WS.mjs) implementation 41 | does not support Binary Frames (`0x02`) on purpose to reduce the attack surface 42 | of fragmented TCP frames. 43 | 44 | The `Buffer` API that is used to transfer `payload`s will be serialized as an `Array` 45 | with Integers (`0-255`), and therefore Text Frames (`0x01`) with encoded `JSON` 46 | as `Payload` can be used. 47 | 48 | Note that the `Payload` on `WS/13` Framing level consists of an encoded `JSON.stringify()` 49 | string of an `Object` that contains the `Service Headers` and `Service Payload` which are 50 | delegated respectively on either side. To avoid confusion, the term `Payload` is not used 51 | and the term `Request` and `Response` are used henceforth. 52 | 53 | 54 | ```javascript 55 | // Example inside a Browser VM 56 | 57 | let socket = new WebSocket('ws://stealth-service:65432', [ 'stealth' ]); 58 | 59 | socket.onmessage = (e) => { 60 | 61 | let response = null; 62 | 63 | if (typeof e.data === 'string') { 64 | response = JSON.parse(e.data); 65 | } 66 | 67 | if (response !== null) { 68 | console.log('received headers', response.headers); 69 | console.log('received payload', response.payload); 70 | } 71 | 72 | }; 73 | 74 | socket.onopen = () => { 75 | 76 | let request = { 77 | headers: { 78 | service: 'host', 79 | method: 'read' 80 | }, 81 | payload: { 82 | domain: 'cookie.engineer' 83 | } 84 | }; 85 | 86 | socket.send(JSON.stringify(request)); 87 | 88 | }; 89 | ``` 90 | 91 | -------------------------------------------------------------------------------- /stealth/source/server/service/Blocker.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Emitter, isFunction, isObject, isString } from '../../../extern/base.mjs'; 3 | 4 | 5 | 6 | const toDomain = function(payload) { 7 | 8 | let domain = null; 9 | 10 | if (isObject(payload) === true) { 11 | 12 | if (isString(payload.domain) === true) { 13 | 14 | if (isString(payload.subdomain) === true) { 15 | domain = payload.subdomain + '.' + payload.domain; 16 | } else { 17 | domain = payload.domain; 18 | } 19 | 20 | } else if (isString(payload.host) === true) { 21 | domain = payload.host; 22 | } 23 | 24 | } 25 | 26 | return domain; 27 | 28 | }; 29 | 30 | 31 | 32 | const Blocker = function(stealth) { 33 | 34 | this.stealth = stealth; 35 | Emitter.call(this); 36 | 37 | }; 38 | 39 | 40 | /* 41 | * { 42 | * "domain": "ads.example.com" 43 | * } 44 | */ 45 | 46 | Blocker.isBlocker = function(payload) { 47 | 48 | if ( 49 | isObject(payload) === true 50 | && isString(payload.domain) === true 51 | ) { 52 | return true; 53 | } 54 | 55 | 56 | return false; 57 | 58 | }; 59 | 60 | Blocker.toBlocker = function(payload) { 61 | 62 | if (isObject(payload) === true) { 63 | 64 | let domain = null; 65 | 66 | if (isString(payload.domain) === true) { 67 | 68 | if (isString(payload.subdomain) === true) { 69 | domain = payload.subdomain + '.' + payload.domain; 70 | } else { 71 | domain = payload.domain; 72 | } 73 | 74 | } else if (isString(payload.host) === true) { 75 | domain = payload.host; 76 | } 77 | 78 | if (domain !== null) { 79 | 80 | return { 81 | domain: domain 82 | }; 83 | 84 | } 85 | 86 | } 87 | 88 | 89 | return null; 90 | 91 | }; 92 | 93 | 94 | Blocker.prototype = Object.assign({}, Emitter.prototype, { 95 | 96 | toJSON: function() { 97 | 98 | let blob = Emitter.prototype.toJSON.call(this); 99 | let data = { 100 | events: blob.data.events, 101 | journal: blob.data.journal 102 | }; 103 | 104 | return { 105 | 'type': 'Blocker Service', 106 | 'data': data 107 | }; 108 | 109 | }, 110 | 111 | read: function(payload, callback) { 112 | 113 | callback = isFunction(callback) ? callback : null; 114 | 115 | 116 | let blocker = null; 117 | let domain = toDomain(payload); 118 | if (domain !== null) { 119 | blocker = this.stealth.settings.blockers.find((b) => b.domain === domain) || null; 120 | } 121 | 122 | // Ensure that subdomains are matched, too 123 | if (blocker === null) { 124 | blocker = this.stealth.settings.blockers.find((b) => domain.endsWith('.' + b.domain)) || null; 125 | } 126 | 127 | if (callback !== null) { 128 | 129 | callback({ 130 | headers: { 131 | service: 'blocker', 132 | event: 'read' 133 | }, 134 | payload: blocker 135 | }); 136 | 137 | } 138 | 139 | } 140 | 141 | }); 142 | 143 | 144 | export { Blocker }; 145 | 146 | -------------------------------------------------------------------------------- /browser/design/card/Host.css: -------------------------------------------------------------------------------- 1 | 2 | browser-card-host { 3 | all: unset; 4 | display: block; 5 | position: relative; 6 | width: 800px; 7 | max-height: 64px; 8 | margin: 0px; 9 | padding: 16px; 10 | background: var(--surface-default-background); 11 | border: 1px solid var(--surface-default-color); 12 | border-radius: 8px; 13 | box-sizing: border-box; 14 | transition: 200ms all ease-out; 15 | overflow: hidden; 16 | cursor: default; 17 | } 18 | 19 | browser-card-host.active { 20 | max-height: unset; 21 | } 22 | 23 | browser-card-host > h3 { 24 | display: block; 25 | margin: 0px 0px 16px 0px; 26 | padding: 0px; 27 | } 28 | 29 | browser-card-host > h3:before { 30 | display: inline; 31 | content: '\e80d'; 32 | margin: 0px 16px 0px 0px; 33 | padding: 0px; 34 | font-family: 'icon'; 35 | font-size: 24px; 36 | font-weight: 400; 37 | text-align: center; 38 | vertical-align: middle; 39 | speak: none; 40 | -webkit-font-smoothing: antialiased; 41 | } 42 | 43 | browser-card-host > h3 input[type="text"] { 44 | display: inline-block; 45 | width: calc(100% - 32px - 32px - 24px); 46 | font-family: 'vera-mono'; 47 | font-size: 16px; 48 | font-weight: 400; 49 | line-height: 32px; 50 | } 51 | 52 | browser-card-host > button[data-action="toggle"] { 53 | display: block; 54 | position: absolute; 55 | top: 16px; 56 | right: 16px; 57 | bottom: auto; 58 | left: auto; 59 | } 60 | 61 | browser-card-host > button[data-action="toggle"]:before { 62 | content: '\e5c5'; 63 | transition: 200ms transform ease-out; 64 | } 65 | 66 | browser-card-host > button[data-action="toggle"].active:before { 67 | transform: rotate(180deg); 68 | transition: 200ms transform ease-out; 69 | } 70 | 71 | 72 | 73 | browser-card-host-article { 74 | display: none; 75 | width: 100%; 76 | height: auto; 77 | } 78 | 79 | browser-card-host.active browser-card-host-article { 80 | display: block; 81 | } 82 | 83 | browser-card-host-article > textarea { 84 | display: block; 85 | width: 100%; 86 | height: auto; 87 | background: transparent; 88 | resize: vertical; 89 | } 90 | 91 | 92 | 93 | browser-card-host-footer { 94 | display: none; 95 | height: 32px; 96 | margin: 16px 0px 0px 0px; 97 | padding: 0px; 98 | text-align: right; 99 | } 100 | 101 | browser-card-host.active browser-card-host-footer { 102 | display: block; 103 | } 104 | 105 | browser-card-host-footer > button { 106 | margin-left: 16px; 107 | } 108 | 109 | browser-card-host-footer > button[data-action="create"]:before { content: '\e145'; } 110 | browser-card-host-footer > button[data-action="refresh"]:before { content: '\e5d5'; } 111 | browser-card-host-footer > button[data-action="remove"]:before { content: '\e14c'; } 112 | browser-card-host-footer > button[data-action="save"]:before { content: '\e876'; } 113 | 114 | 115 | 116 | @media screen and (max-width: 800px) { 117 | 118 | browser-card-host { 119 | width: auto; 120 | } 121 | 122 | } 123 | 124 | -------------------------------------------------------------------------------- /browser/design/common/theme.css: -------------------------------------------------------------------------------- 1 | 2 | body, 3 | body[data-theme="dark"] { 4 | 5 | color: #ffffff; 6 | background: #000000; 7 | 8 | --layout-default-color: #ffffff; 9 | --layout-default-background: #000000; 10 | 11 | --layout-focus-color: #ffffff; 12 | --layout-focus-background: rgba(255,255,255,0.20); 13 | 14 | --link-default-color: #22ccff; 15 | --link-default-background: transparent; 16 | 17 | --link-focus-color: #22ccff; 18 | --link-focus-background: rgba(255,255,255,0.20); 19 | 20 | --element-default-color: #aaaaaa; 21 | --element-default-background: transparent; 22 | 23 | --element-disable-color: #222222; 24 | --element-disable-background: transparent; 25 | 26 | --element-invalid-color: #ff2222; 27 | --element-invalid-background: rgba(255, 34, 34, 0.20); 28 | 29 | --element-focus-color: #ffffff; 30 | --element-focus-background: rgba(255,255,255,0.20); 31 | 32 | --element-active-color: #ffffff; 33 | --element-active-background: rgba(255,255,255,0.10); 34 | 35 | --element-select-color: #22ccff; 36 | --element-select-background: rgba(51,153,255,0.20); 37 | 38 | --scrollbar-default-color: #ffffff; 39 | --scrollbar-default-background: transparent; 40 | 41 | --surface-default-color: #666666; 42 | --surface-default-background: rgba(255, 255, 255, 0.10); 43 | --surface-default-foreground: #1a1a1a; 44 | 45 | /* Necessary image due to Browser Quirks */ 46 | --element-select-default-background-image: url('./theme-dark-element-select-default.svg'); 47 | --element-select-focus-background-image: url('./theme-dark-element-select-focus.svg'); 48 | 49 | } 50 | 51 | body[data-theme="light"] { 52 | 53 | color: #111111; 54 | background: #eeeeee; 55 | 56 | --layout-default-color: #111111; 57 | --layout-default-background: #eeeeee; 58 | 59 | --layout-focus-color: #111111; 60 | --layout-focus-background: rgba(0,0,0,0.20); 61 | 62 | --link-default-color: #22ccff; 63 | --link-default-background: transparent; 64 | 65 | --link-focus-color: #22ccff; 66 | --link-focus-background: rgba(34,204,255,0.20); 67 | 68 | --element-default-color: #666666; 69 | --element-default-background: transparent; 70 | 71 | --element-disable-color: #dddddd; 72 | --element-disable-background: transparent; 73 | 74 | --element-invalid-color: #ff2222; 75 | --element-invalid-background: rgba(255, 34, 34, 0.20); 76 | 77 | --element-focus-color: #22ccff; 78 | --element-focus-background: rgba(34,204,255,0.20); 79 | 80 | --element-active-color: #22ccff; 81 | --element-active-background: rgba(34,204,255,0.10); 82 | 83 | --element-select-color: #22ccff; 84 | --element-select-background: rgba(34,204,255,0.20); 85 | 86 | --scrollbar-default-color: #111111; 87 | --scrollbar-default-background: transparent; 88 | 89 | --surface-default-color: #666666; 90 | --surface-default-background: rgba(0, 0, 0, 0.025); 91 | --surface-default-foreground: #e8e8e8; 92 | 93 | /* Necessary image due to Browser Quirks */ 94 | --element-select-default-background-image: url('./theme-light-element-select-default.svg'); 95 | --element-select-focus-background-image: url('./theme-light-element-select-focus.svg'); 96 | 97 | } 98 | 99 | -------------------------------------------------------------------------------- /stealth/review/client/service/Mode.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { isFunction } from '../../../../base/index.mjs'; 3 | import { after, before, describe, finish } from '../../../../covert/index.mjs'; 4 | import { Mode } from '../../../../stealth/source/client/service/Mode.mjs'; 5 | import { connect as connect_stealth, disconnect as disconnect_stealth } from '../../../../stealth/review/Stealth.mjs'; 6 | import { connect as connect_client, disconnect as disconnect_client } from '../../../../stealth/review/Client.mjs'; 7 | 8 | 9 | 10 | before(connect_stealth); 11 | before(connect_client); 12 | 13 | describe('new Mode()', function(assert) { 14 | 15 | assert(this.client !== null); 16 | assert(this.client.services.mode instanceof Mode, true); 17 | 18 | }); 19 | 20 | describe('Mode.prototype.toJSON()', function(assert) { 21 | 22 | assert(this.client !== null); 23 | assert(isFunction(this.client.services.mode.toJSON), true); 24 | 25 | assert(this.client.services.mode.toJSON(), { 26 | type: 'Mode Service', 27 | data: { 28 | events: [], 29 | journal: [] 30 | } 31 | }); 32 | 33 | }); 34 | 35 | describe('Mode.prototype.save()', function(assert) { 36 | 37 | assert(this.client !== null); 38 | assert(isFunction(this.client.services.mode.save), true); 39 | 40 | this.client.services.mode.save({ 41 | domain: 'example.com', 42 | mode: { 43 | text: false, 44 | image: true, 45 | audio: false, 46 | video: true, 47 | other: false 48 | } 49 | }, (response) => { 50 | 51 | assert(response, true); 52 | 53 | }); 54 | 55 | }); 56 | 57 | describe('Mode.prototype.read()/success', function(assert) { 58 | 59 | assert(this.client !== null); 60 | assert(isFunction(this.client.services.mode.read), true); 61 | 62 | this.client.services.mode.read({ 63 | domain: 'example.com' 64 | }, (response) => { 65 | 66 | assert(response, { 67 | domain: 'example.com', 68 | mode: { 69 | text: false, 70 | image: true, 71 | audio: false, 72 | video: true, 73 | other: false 74 | } 75 | }); 76 | 77 | }); 78 | 79 | }); 80 | 81 | describe('Mode.prototype.remove()', function(assert) { 82 | 83 | assert(this.client !== null); 84 | assert(isFunction(this.client.services.mode.remove), true); 85 | 86 | this.client.services.mode.remove({ 87 | domain: 'example.com' 88 | }, (response) => { 89 | 90 | assert(response, true); 91 | 92 | }); 93 | 94 | }); 95 | 96 | describe('Mode.prototype.read()/failure', function(assert) { 97 | 98 | assert(this.client !== null); 99 | assert(isFunction(this.client.services.mode.read), true); 100 | 101 | this.client.services.mode.read({ 102 | domain: 'example.com' 103 | }, (response) => { 104 | 105 | assert(response, null); 106 | 107 | }); 108 | 109 | }); 110 | 111 | after(disconnect_client); 112 | after(disconnect_stealth); 113 | 114 | 115 | export default finish('stealth/client/service/Mode', { 116 | internet: false, 117 | network: true 118 | }); 119 | 120 | -------------------------------------------------------------------------------- /browser/internal/debug/Browser.css: -------------------------------------------------------------------------------- 1 | 2 | @import url("../../design/card/Beacon.css"); 3 | @import url("../../design/card/Host.css"); 4 | @import url("../../design/card/Mode.css"); 5 | @import url("../../design/card/Peer.css"); 6 | @import url("../../design/card/Policy.css"); 7 | @import url("../../design/card/Redirect.css"); 8 | @import url("../../design/card/Session.css"); 9 | 10 | 11 | 12 | browser-widget-browser { 13 | all: unset; 14 | display: block; 15 | position: relative; 16 | width: 800px; 17 | max-height: 64px; 18 | margin: 0px; 19 | padding: 16px; 20 | background: var(--surface-default-background); 21 | border: 1px solid var(--surface-default-color); 22 | border-radius: 8px; 23 | box-sizing: border-box; 24 | transition: 200ms all ease-out; 25 | overflow: hidden; 26 | cursor: default; 27 | } 28 | 29 | browser-widget-browser.active { 30 | max-height: unset; 31 | } 32 | 33 | browser-widget-browser > h3 { 34 | display: block; 35 | margin: 0px; 36 | padding: 0px 0px 16px 0px; 37 | font-family: 'crystalline'; 38 | font-size: 32px; 39 | font-weight: 500; 40 | line-height: 32px; 41 | text-transform: uppercase; 42 | vertical-align: middle; 43 | } 44 | 45 | browser-widget-browser > button[data-action="toggle"] { 46 | display: block; 47 | position: absolute; 48 | top: 16px; 49 | right: 16px; 50 | bottom: auto; 51 | left: auto; 52 | } 53 | 54 | browser-widget-browser > button[data-action="toggle"]:before { 55 | content: '\e5c5'; 56 | transition: 200ms transform ease-out; 57 | } 58 | 59 | browser-widget-browser > button[data-action="toggle"].active:before { 60 | transform: rotate(180deg); 61 | transition: 200ms transform ease-out; 62 | } 63 | 64 | 65 | 66 | browser-widget-browser-header { 67 | display: none; 68 | width: 100%; 69 | height: auto; 70 | } 71 | 72 | browser-widget-browser.active browser-widget-browser-header { 73 | display: block; 74 | } 75 | 76 | browser-widget-browser-header > input[data-key="domain"] { 77 | display: block; 78 | width: 100%; 79 | margin: 0px auto 16px auto; 80 | text-align: center; 81 | } 82 | 83 | browser-widget-browser-header > p { 84 | margin: 0px 0px 16px 0px; 85 | font-family: 'museo'; 86 | font-size: 16px; 87 | text-align: center; 88 | } 89 | 90 | 91 | 92 | browser-widget-browser-article { 93 | display: none; 94 | width: 100%; 95 | height: auto; 96 | } 97 | 98 | browser-widget-browser.active browser-widget-browser-article { 99 | display: block; 100 | } 101 | 102 | browser-widget-browser-article > * { 103 | width: 100% !important; 104 | margin: 0px 0px 16px 0px !important; 105 | } 106 | 107 | 108 | 109 | browser-widget-browser-footer { 110 | display: none; 111 | height: 32px; 112 | margin: 16px 0px 0px 0px; 113 | padding: 0px; 114 | text-align: right; 115 | } 116 | 117 | browser-widget-browser.active browser-widget-browser-footer { 118 | display: block; 119 | } 120 | 121 | browser-widget-browser-footer > button[data-action="refresh"]:before { content: '\e5d5'; } 122 | 123 | 124 | 125 | @media screen and (max-width: 800px) { 126 | 127 | browser-widget-browser { 128 | width: auto; 129 | } 130 | 131 | } 132 | 133 | -------------------------------------------------------------------------------- /stealth/review/server/service/Blocker.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { isFunction } from '../../../../base/index.mjs'; 3 | import { after, before, describe, finish } from '../../../../covert/index.mjs'; 4 | import { Blocker } from '../../../../stealth/source/server/service/Blocker.mjs'; 5 | import { connect, disconnect } from '../../../../stealth/review/Server.mjs'; 6 | 7 | 8 | 9 | before(connect); 10 | 11 | describe('new Blocker()', function(assert) { 12 | 13 | assert(this.server !== null); 14 | assert(this.server.services.blocker instanceof Blocker, true); 15 | 16 | }); 17 | 18 | describe('Blocker.prototype.toJSON()', function(assert) { 19 | 20 | assert(this.server !== null); 21 | assert(isFunction(this.server.services.blocker.toJSON), true); 22 | 23 | assert(this.server.services.blocker.toJSON(), { 24 | type: 'Blocker Service', 25 | data: { 26 | events: [], 27 | journal: [] 28 | } 29 | }); 30 | 31 | }); 32 | 33 | describe('Blocker.isBlocker()', function(assert) { 34 | 35 | assert(isFunction(Blocker.isBlocker), true); 36 | 37 | assert(Blocker.isBlocker(null), false); 38 | assert(Blocker.isBlocker({}), false); 39 | 40 | assert(Blocker.isBlocker({ 41 | domain: 'example.com' 42 | }), true); 43 | 44 | }); 45 | 46 | describe('Blocker.toBlocker()', function(assert) { 47 | 48 | assert(isFunction(Blocker.toBlocker), true); 49 | 50 | assert(Blocker.toBlocker(null), null); 51 | assert(Blocker.toBlocker({}), null); 52 | 53 | assert(Blocker.toBlocker({ 54 | domain: 'example.com', 55 | another: 'property' 56 | }), { 57 | domain: 'example.com' 58 | }); 59 | 60 | }); 61 | 62 | describe('Blocker.prototype.read()/success', function(assert) { 63 | 64 | assert(this.server !== null); 65 | assert(isFunction(this.server.services.blocker.read), true); 66 | 67 | this.server.services.blocker.read({ 68 | domain: 'ads.quantserve.com' 69 | }, (response) => { 70 | 71 | assert(response, { 72 | headers: { 73 | service: 'blocker', 74 | event: 'read' 75 | }, 76 | payload: { 77 | domain: 'ads.quantserve.com' 78 | } 79 | }); 80 | 81 | }); 82 | 83 | this.server.services.blocker.read({ 84 | domain: 'subdomain.ads.quantserve.com' 85 | }, (response) => { 86 | 87 | assert(response, { 88 | headers: { 89 | service: 'blocker', 90 | event: 'read' 91 | }, 92 | payload: { 93 | domain: 'ads.quantserve.com' 94 | } 95 | }); 96 | 97 | }); 98 | 99 | }); 100 | 101 | describe('Blocker.prototype.read()/failure', function(assert) { 102 | 103 | assert(this.server !== null); 104 | assert(isFunction(this.server.services.blocker.read), true); 105 | 106 | this.server.services.blocker.read({ 107 | domain: 'quantserve.com' 108 | }, (response) => { 109 | 110 | assert(response, { 111 | headers: { 112 | service: 'blocker', 113 | event: 'read' 114 | }, 115 | payload: null 116 | }); 117 | 118 | }); 119 | 120 | }); 121 | 122 | after(disconnect); 123 | 124 | 125 | export default finish('stealth/server/service/Blocker', { 126 | internet: false, 127 | network: true 128 | }); 129 | 130 | -------------------------------------------------------------------------------- /stealth/review/client/service/Beacon.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { isFunction } from '../../../../base/index.mjs'; 3 | import { after, before, describe, finish } from '../../../../covert/index.mjs'; 4 | import { Beacon } from '../../../../stealth/source/client/service/Beacon.mjs'; 5 | import { connect as connect_stealth, disconnect as disconnect_stealth } from '../../../../stealth/review/Stealth.mjs'; 6 | import { connect as connect_client, disconnect as disconnect_client } from '../../../../stealth/review/Client.mjs'; 7 | 8 | 9 | 10 | before(connect_stealth); 11 | before(connect_client); 12 | 13 | describe('new Beacon()', function(assert) { 14 | 15 | assert(this.client !== null); 16 | assert(this.client.services.beacon instanceof Beacon, true); 17 | 18 | }); 19 | 20 | describe('Beacon.prototype.toJSON()', function(assert) { 21 | 22 | assert(this.client !== null); 23 | assert(isFunction(this.client.services.beacon.toJSON), true); 24 | 25 | assert(this.client.services.beacon.toJSON(), { 26 | type: 'Beacon Service', 27 | data: { 28 | events: [], 29 | journal: [] 30 | } 31 | }); 32 | 33 | }); 34 | 35 | describe('Beacon.prototype.save()', function(assert) { 36 | 37 | assert(this.client !== null); 38 | assert(isFunction(this.client.services.beacon.save), true); 39 | 40 | this.client.services.beacon.save({ 41 | domain: 'example.com', 42 | beacons: [{ 43 | path: '/index.html', 44 | query: null, 45 | select: 'body > article > h3', 46 | term: 'title' 47 | }] 48 | }, (response) => { 49 | 50 | assert(response, true); 51 | 52 | }); 53 | 54 | }); 55 | 56 | describe('Beacon.prototype.read()/success', function(assert) { 57 | 58 | assert(this.client !== null); 59 | assert(isFunction(this.client.services.beacon.read), true); 60 | 61 | this.client.services.beacon.read({ 62 | domain: 'example.com' 63 | }, (response) => { 64 | 65 | assert(response, { 66 | domain: 'example.com', 67 | beacons: [{ 68 | path: '/index.html', 69 | query: null, 70 | select: 'body > article > h3', 71 | term: 'title' 72 | }] 73 | }); 74 | 75 | }); 76 | 77 | }); 78 | 79 | describe('Beacon.prototype.remove()', function(assert) { 80 | 81 | assert(this.client !== null); 82 | assert(isFunction(this.client.services.beacon.remove), true); 83 | 84 | this.client.services.beacon.remove({ 85 | domain: 'example.com' 86 | }, (response) => { 87 | 88 | assert(response, true); 89 | 90 | }); 91 | 92 | }); 93 | 94 | describe('Beacon.prototype.read()/failure', function(assert) { 95 | 96 | assert(this.client !== null); 97 | assert(isFunction(this.client.services.beacon.read), true); 98 | 99 | this.client.services.beacon.read({ 100 | domain: 'example.com' 101 | }, (response) => { 102 | 103 | assert(response, null); 104 | 105 | }); 106 | 107 | }); 108 | 109 | after(disconnect_client); 110 | after(disconnect_stealth); 111 | 112 | 113 | export default finish('stealth/client/service/Beacon', { 114 | internet: false, 115 | network: true 116 | }); 117 | 118 | -------------------------------------------------------------------------------- /browser/design/widget/Video.css: -------------------------------------------------------------------------------- 1 | 2 | browser-widget-video { 3 | all: unset; 4 | display: block; 5 | position: relative; 6 | width: 800px; 7 | margin: 0px; 8 | padding: 16px; 9 | background: var(--surface-default-background); 10 | border: 1px solid var(--surface-default-color); 11 | border-radius: 8px; 12 | box-sizing: border-box; 13 | transition: 200ms all ease-out; 14 | overflow: hidden; 15 | cursor: default; 16 | } 17 | 18 | 19 | 20 | browser-widget-video-article { 21 | display: block; 22 | position: relative; 23 | width: 100%; 24 | height: auto; 25 | text-align: center; 26 | overflow: hidden; 27 | z-index: 1; 28 | } 29 | 30 | browser-widget-video-article video { 31 | display: block; 32 | width: 100%; 33 | height: auto; 34 | max-height: 100%; 35 | margin: 0px auto; 36 | padding: 0px; 37 | } 38 | 39 | 40 | 41 | browser-widget-video-footer { 42 | display: block; 43 | height: 32px; 44 | margin: 16px 0px 0px 0px; 45 | padding: 0px; 46 | text-align: left; 47 | z-index: 2; 48 | } 49 | 50 | browser-widget-video-footer > button[data-action="play"], 51 | browser-widget-video-footer > button[data-action="pause"] { 52 | margin: 0px 16px 0px 0px; 53 | } 54 | 55 | browser-widget-video-footer > button[data-action="fullscreen"] { 56 | margin: 0px 0px 0px 16px; 57 | } 58 | 59 | browser-widget-video-footer > button[data-action="download"] { 60 | margin: 0px 0px 0px 16px; 61 | } 62 | 63 | browser-widget-video-footer > button[data-action="download"]:before { content: '\e2c4'; } 64 | browser-widget-video-footer > button[data-action="fullscreen"]:before { content: '\e5d0'; } 65 | browser-widget-video-footer > button[data-action="play"]:before { content: '\e037'; } 66 | browser-widget-video-footer > button[data-action="pause"]:before { content: '\e034'; } 67 | 68 | browser-widget-video-footer > progress { 69 | display: inline-block; 70 | width: calc(100% - 48px - 48px - 48px); 71 | height: 16px; 72 | line-height: 30px; 73 | border: 1px solid transparent; 74 | border-radius: 4px; 75 | box-sizing: border-box; 76 | appearance: none; 77 | -moz-appearance: none; 78 | -webkit-appearance: none; 79 | vertical-align: middle; 80 | cursor: col-resize; 81 | } 82 | 83 | browser-widget-video-footer > progress::-moz-progress-bar { 84 | display: inline-block; 85 | height: 100%; 86 | background: var(--element-focus-color); 87 | border-radius: 4px 0px 0px 4px; 88 | vertical-align: middle; 89 | box-sizing: border-box; 90 | } 91 | 92 | browser-widget-video-footer > progress[value="100"]::-moz-progress-bar { 93 | border-radius: 4px; 94 | } 95 | 96 | browser-widget-video-footer > progress::-webkit-progress-bar { 97 | background: var(--element-default-color); 98 | border-radius: 4px; 99 | } 100 | 101 | browser-widget-video-footer > progress::-webkit-progress-value { 102 | background: var(--element-focus-color); 103 | border-radius: 4px 0px 0px 4px; 104 | } 105 | 106 | browser-widget-video-footer > progress[value="100"]::-webkit-progress-value { 107 | border-radius: 4px; 108 | } 109 | 110 | 111 | 112 | @media screen and (max-width: 800px) { 113 | 114 | browser-widget-video { 115 | width: auto; 116 | } 117 | 118 | } 119 | 120 | -------------------------------------------------------------------------------- /stealth/source/client/service/Cache.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { Buffer, Emitter, isFunction, isObject } from '../../../extern/base.mjs'; 3 | 4 | 5 | 6 | const Cache = function(client) { 7 | 8 | this.client = client; 9 | Emitter.call(this); 10 | 11 | }; 12 | 13 | 14 | Cache.prototype = Object.assign({}, Emitter.prototype, { 15 | 16 | toJSON: function() { 17 | 18 | let blob = Emitter.prototype.toJSON.call(this); 19 | let data = { 20 | events: blob.data.events, 21 | journal: blob.data.journal 22 | }; 23 | 24 | return { 25 | 'type': 'Cache Service', 26 | 'data': data 27 | }; 28 | 29 | }, 30 | 31 | info: function(payload, callback) { 32 | 33 | payload = isObject(payload) ? payload : null; 34 | callback = isFunction(callback) ? callback : null; 35 | 36 | 37 | if (payload !== null && callback !== null) { 38 | 39 | this.once('info', (response) => callback(response)); 40 | 41 | this.client.send({ 42 | headers: { 43 | service: 'cache', 44 | method: 'info' 45 | }, 46 | payload: payload 47 | }); 48 | 49 | } else if (callback !== null) { 50 | callback(null); 51 | } 52 | 53 | }, 54 | 55 | read: function(payload, callback) { 56 | 57 | payload = isObject(payload) ? payload : null; 58 | callback = isFunction(callback) ? callback : null; 59 | 60 | 61 | if (payload !== null && callback !== null) { 62 | 63 | this.once('read', (response) => { 64 | 65 | if (isObject(response) === true) { 66 | 67 | if (isObject(response.payload) === true) { 68 | 69 | if (response.payload.type === 'Buffer') { 70 | response.payload = Buffer.from(response.payload.data); 71 | } 72 | 73 | } 74 | 75 | } 76 | 77 | callback(response); 78 | 79 | }); 80 | 81 | this.client.send({ 82 | headers: { 83 | service: 'cache', 84 | method: 'read' 85 | }, 86 | payload: payload 87 | }); 88 | 89 | } else if (callback !== null) { 90 | callback(null); 91 | } 92 | 93 | }, 94 | 95 | remove: function(payload, callback) { 96 | 97 | payload = isObject(payload) ? payload : null; 98 | callback = isFunction(callback) ? callback : null; 99 | 100 | 101 | if (payload !== null && callback !== null) { 102 | 103 | this.once('remove', (result) => callback(result)); 104 | 105 | this.client.send({ 106 | headers: { 107 | service: 'cache', 108 | method: 'remove' 109 | }, 110 | payload: payload 111 | }); 112 | 113 | } else if (callback !== null) { 114 | callback(false); 115 | } 116 | 117 | }, 118 | 119 | save: function(payload, callback) { 120 | 121 | payload = isObject(payload) ? payload : null; 122 | callback = isFunction(callback) ? callback : null; 123 | 124 | 125 | if (payload !== null && callback !== null) { 126 | 127 | this.once('save', (result) => callback(result)); 128 | 129 | this.client.send({ 130 | headers: { 131 | service: 'cache', 132 | method: 'save' 133 | }, 134 | payload: payload 135 | }); 136 | 137 | } else if (callback !== null) { 138 | callback(false); 139 | } 140 | 141 | } 142 | 143 | }); 144 | 145 | 146 | export { Cache }; 147 | 148 | -------------------------------------------------------------------------------- /browser/source/Session.mjs: -------------------------------------------------------------------------------- 1 | 2 | import { isArray, isNumber, isObject, isString } from '../extern/base.mjs'; 3 | import { UA } from '../source/parser/UA.mjs'; 4 | import { Tab } from '../source/Tab.mjs'; 5 | 6 | 7 | 8 | export const isSession = function(obj) { 9 | return Object.prototype.toString.call(obj) === '[object Session]'; 10 | }; 11 | 12 | 13 | 14 | const Session = function() { 15 | 16 | this.agent = null; 17 | this.domain = Date.now() + '.tholian.network'; 18 | this.stealth = null; 19 | this.tabs = []; 20 | this.warning = 0; 21 | 22 | }; 23 | 24 | 25 | Session.from = function(json) { 26 | 27 | json = isObject(json) ? json : null; 28 | 29 | 30 | if (json !== null) { 31 | 32 | let type = json.type === 'Session' ? json.type : null; 33 | let data = isObject(json.data) ? json.data : null; 34 | 35 | if (type !== null && data !== null) { 36 | 37 | let session = new Session(); 38 | 39 | if (UA.isUA(data.agent) === true) { 40 | session.agent = data.agent; 41 | } 42 | 43 | if (isString(data.domain) === true) { 44 | session.domain = data.domain; 45 | } 46 | 47 | if (isNumber(data.warning) === true) { 48 | session.warning = data.warning; 49 | } 50 | 51 | if (isArray(data.tabs) === true) { 52 | session.tabs = data.tabs.map((data) => Tab.from(data)).filter((tab) => tab !== null); 53 | } 54 | 55 | return session; 56 | 57 | } 58 | 59 | } 60 | 61 | 62 | return null; 63 | 64 | }; 65 | 66 | 67 | Session.isSession = isSession; 68 | 69 | 70 | Session.merge = function(target, source) { 71 | 72 | target = isSession(target) ? target : null; 73 | source = isSession(source) ? source : null; 74 | 75 | 76 | if (target !== null && source !== null) { 77 | 78 | if (UA.isUA(source.agent) === true) { 79 | target.agent = source.agent; 80 | } 81 | 82 | if (isString(source.domain) === true) { 83 | target.domain = source.domain; 84 | } 85 | 86 | if (isArray(source.tabs) === true) { 87 | 88 | source.tabs.forEach((tab) => { 89 | 90 | let other = target.tabs.find((t) => t.id === tab.id) || null; 91 | if (other !== null) { 92 | Tab.merge(other, tab); 93 | } else { 94 | target.tabs.push(tab); 95 | } 96 | 97 | }); 98 | 99 | } 100 | 101 | } 102 | 103 | 104 | return target; 105 | 106 | }; 107 | 108 | 109 | Session.prototype = { 110 | 111 | [Symbol.toStringTag]: 'Session', 112 | 113 | toJSON: function() { 114 | 115 | let data = { 116 | agent: this.agent, 117 | domain: this.domain, 118 | tabs: this.tabs.map((tab) => tab.toJSON()), 119 | warning: this.warning 120 | }; 121 | 122 | return { 123 | 'type': 'Session', 124 | 'data': data 125 | }; 126 | 127 | }, 128 | 129 | destroy: function() { 130 | 131 | this.tabs.forEach((tab) => { 132 | tab.destroy(); 133 | }); 134 | 135 | 136 | this.agent = null; 137 | this.domain = Date.now() + '.tholian.network'; 138 | this.stealth = null; 139 | this.tabs = []; 140 | this.warning = 0; 141 | 142 | 143 | return true; 144 | 145 | } 146 | 147 | }; 148 | 149 | 150 | export { Session }; 151 | 152 | -------------------------------------------------------------------------------- /browser/design/card/Session.css: -------------------------------------------------------------------------------- 1 | 2 | @import url("./Tab.css"); 3 | 4 | 5 | 6 | browser-card-session { 7 | all: unset; 8 | display: block; 9 | position: relative; 10 | width: 800px; 11 | max-height: 64px; 12 | margin: 0px; 13 | padding: 16px; 14 | background: var(--surface-default-background); 15 | border: 1px solid var(--surface-default-color); 16 | border-radius: 8px; 17 | box-sizing: border-box; 18 | transition: 200ms all ease-out; 19 | overflow: hidden; 20 | cursor: default; 21 | } 22 | 23 | browser-card-session.active { 24 | max-height: unset; 25 | } 26 | 27 | browser-card-session > h3 { 28 | display: block; 29 | height: 32px; 30 | margin: 0px 0px 16px 0px; 31 | padding: 0px; 32 | line-height: 32px; 33 | font-family: 'vera-mono'; 34 | font-size: 16px; 35 | font-weight: 400; 36 | text-transform: uppercase; 37 | vertical-align: middle; 38 | } 39 | 40 | browser-card-session > h3:before { 41 | display: inline; 42 | content: '\e335'; 43 | margin: 0px 16px 0px 0px; 44 | padding: 0px; 45 | font-family: 'icon'; 46 | font-size: 24px; 47 | font-weight: 400; 48 | text-align: center; 49 | vertical-align: top; 50 | speak: none; 51 | -webkit-font-smoothing: antialiased; 52 | } 53 | 54 | browser-card-session > h3 span { 55 | font-size: 16px; 56 | text-transform: none; 57 | } 58 | 59 | browser-card-session > button[data-action="toggle"] { 60 | display: block; 61 | position: absolute; 62 | top: 16px; 63 | right: 16px; 64 | bottom: auto; 65 | left: auto; 66 | } 67 | 68 | browser-card-session > button[data-action="toggle"]:before { 69 | content: '\e5c5'; 70 | transition: 200ms transform ease-out; 71 | } 72 | 73 | browser-card-session > button[data-action="toggle"].active:before { 74 | transform: rotate(180deg); 75 | transition: 200ms transform ease-out; 76 | } 77 | 78 | 79 | 80 | browser-card-session-header { 81 | display: none; 82 | width: 100%; 83 | height: auto; 84 | margin: 0px 0px 16px 0px; 85 | } 86 | 87 | browser-card-session.active browser-card-session-header { 88 | display: block; 89 | } 90 | 91 | browser-card-session-header > code { 92 | display: inline-block; 93 | margin: 0px; 94 | padding: 4px; 95 | font-family: 'vera-mono'; 96 | color: var(--layout-default-color); 97 | background: var(--layout-default-background); 98 | border-radius: 4px; 99 | } 100 | 101 | 102 | 103 | browser-card-session-article { 104 | display: none; 105 | width: 100%; 106 | height: auto; 107 | } 108 | 109 | browser-card-session.active browser-card-session-article { 110 | display: block; 111 | } 112 | 113 | browser-card-session-article > browser-card-tab { 114 | width: 100%; 115 | margin: 0px 0px 16px 0px; 116 | } 117 | 118 | 119 | 120 | browser-card-session-footer { 121 | display: none; 122 | height: 32px; 123 | margin: 16px 0px 0px 0px; 124 | padding: 0px; 125 | text-align: right; 126 | } 127 | 128 | browser-card-session.active browser-card-session-footer { 129 | display: block; 130 | } 131 | 132 | browser-card-session-footer > button[data-action="remove"]:before { content: '\e14c'; } 133 | 134 | 135 | 136 | @media screen and (max-width: 800px) { 137 | 138 | browser-card-settings { 139 | width: auto; 140 | } 141 | 142 | } 143 | 144 | -------------------------------------------------------------------------------- /browser/design/index.css: -------------------------------------------------------------------------------- 1 | 2 | @import url("./common/index.css"); 3 | @import url("./common/theme.css"); 4 | 5 | @import url("./appbar/Address.css"); 6 | @import url("./appbar/History.css"); 7 | @import url("./appbar/Mode.css"); 8 | @import url("./appbar/Settings.css"); 9 | @import url("./appbar/Splitter.css"); 10 | 11 | @import url("./backdrop/Tabs.css"); 12 | @import url("./backdrop/Webview.css"); 13 | 14 | @import url("./sheet/Console.css"); 15 | @import url("./sheet/Session.css"); 16 | @import url("./sheet/Site.css"); 17 | 18 | @import url("./menu/Context.css"); 19 | @import url("./menu/Help.css"); 20 | 21 | 22 | 23 | /* 24 | * App Layers 25 | */ 26 | 27 | base-console { z-index: 255 !important; } 28 | base-console.active ~ browser-appbar { z-index: 254 !important; } 29 | 30 | browser-appbar { z-index: 512; } 31 | browser-appbar browser-appbar-history { z-index: unset; } 32 | browser-appbar browser-appbar-address { z-index: unset; } 33 | browser-appbar browser-appbar-mode { z-index: unset; } 34 | browser-appbar browser-appbar-settings { z-index: unset; } 35 | browser-appbar button[data-key="splitter"] { z-index: 256; } /* only on mobile */ 36 | browser-appbar browser-appbar-splitter { z-index: 255; } /* only on mobile */ 37 | browser-backdrop { z-index: 0; } 38 | browser-backdrop browser-backdrop-webview { z-index: 1; } 39 | browser-backdrop browser-backdrop-tabs { z-index: 128; } 40 | browser-sheet { z-index: 256; } 41 | browser-sheet browser-sheet-session { z-index: 257; } 42 | browser-sheet browser-sheet-site { z-index: 257; } 43 | browser-sheet browser-sheet-console { z-index: 511; } /* only on ?debug=true */ 44 | browser-menu { z-index: 1024; } 45 | browser-menu browser-menu-context { z-index: 1025; } 46 | browser-menu browser-menu-help { z-index: 1026; } 47 | 48 | 49 | body { 50 | display: block; 51 | margin: 0px; 52 | padding: 0px; 53 | color: var(--layout-default-color); 54 | background: var(--layout-default-background); 55 | } 56 | 57 | browser-appbar { 58 | display: block; 59 | position: fixed; 60 | width: auto; 61 | height: auto; 62 | height: 33px; 63 | line-height: 32px; 64 | top: 0px; 65 | right: 0px; 66 | bottom: auto; 67 | left: 0px; 68 | margin: 0px; 69 | padding: 0px; 70 | border: 0px solid transparent; 71 | border-bottom: 1px solid var(--surface-default-color); 72 | background: var(--surface-default-background); 73 | box-sizing: border-box; 74 | vertical-align: middle; 75 | } 76 | 77 | browser-backdrop { 78 | display: block; 79 | position: relative; 80 | } 81 | 82 | browser-backdrop-tabs { 83 | top: calc(32px + 1px); 84 | } 85 | 86 | browser-backdrop-webview { 87 | top: calc(32px + 1px); 88 | } 89 | 90 | browser-sheet { 91 | display: block; 92 | position: relative; 93 | } 94 | 95 | browser-menu { 96 | display: block; 97 | position: relative; 98 | } 99 | 100 | 101 | 102 | @media screen and (max-width: 412px) { 103 | 104 | browser-backdrop-tabs { 105 | top: calc(64px + 1px); 106 | } 107 | 108 | } 109 | --------------------------------------------------------------------------------