diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index dfe0770..0000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -# Auto detect text files and perform LF normalization -* text=auto diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index d9005f2..26f78a6 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +data/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/html/login.html b/html/login.html new file mode 100755 index 0000000..73fed11 --- /dev/null +++ b/html/login.html @@ -0,0 +1,42 @@ + +
+
+

- Log in

+ + + + + + + +
+ +
+ + \ No newline at end of file diff --git a/html/nav.html b/html/nav.html new file mode 100755 index 0000000..6952e34 --- /dev/null +++ b/html/nav.html @@ -0,0 +1,30 @@ + + + + + + <!--Place title here!!!--> + + + + +
+ +
+ + + \ No newline at end of file diff --git a/html/src/UbuntuMono-R.ttf b/html/src/UbuntuMono-R.ttf new file mode 100755 index 0000000..fdd309d Binary files /dev/null and b/html/src/UbuntuMono-R.ttf differ diff --git a/html/src/auth.js b/html/src/auth.js new file mode 100755 index 0000000..41d2f0c --- /dev/null +++ b/html/src/auth.js @@ -0,0 +1,55 @@ +import * as utils from '/src/utils.js' +import * as jsonpack from '/src/jsonpack.js' + +let c = null + +export function authClient(rawClient) { + c = this + this.rawClient = rawClient + + this.login = (username, password)=>{ + const salt = utils.getUnixTime() + const key = (utils.sha256( + username + + utils.sha256(password) + + salt + )) + //console.log(c.rawClient.send) + c.rawClient.send('login', { + data: key, + salt: salt + }) + } + + this.send = (data)=>{ + + } + + this.clidata = (data)=>{ + // const session = utils.getCookie('session') + // if(session != ''){ + // this.rawClient.send('reauth', { + // session: session + // }) + // } + } + + this.loginSuccess = (data)=>{ + utils.popupSuccess('Connection', 'Successfully logged in!') + utils.iconauth() + console.log(data.data.session) + if(data.data.session != null){ + utils.setCookie('session', data.data.session) + } + window.location = data.data.redir + } + + this.reauth = (data)=>{ + utils.popupSuccess('Connection', 'Successfully logged in!') + utils.iconauth() + } + + this.rawClient.addRawTypeListener('clidata', this.clidata) + this.rawClient.addRawTypeListener('loginSuccess', this.loginSuccess) + this.rawClient.addRawTypeListener('reauth', this.reauth) +} \ No newline at end of file diff --git a/html/src/jsonpack.js b/html/src/jsonpack.js new file mode 100755 index 0000000..7569abc --- /dev/null +++ b/html/src/jsonpack.js @@ -0,0 +1,559 @@ +/* + Copyright (c) 2013, Rodrigo González, Sapienlab All Rights Reserved. + Available via MIT LICENSE. See https://github.com/roro89/jsonpack/blob/master/LICENSE.md for details. +*/ + + +var TOKEN_TRUE = -1; +var TOKEN_FALSE = -2; +var TOKEN_NULL = -3; +var TOKEN_EMPTY_STRING = -4; +var TOKEN_UNDEFINED = -5; + +export function pack(json, options) { + + // Canonizes the options + options = options || {}; + + // A shorthand for debugging + var verbose = options.verbose || false; + + verbose && console.log('Normalize the JSON Object'); + + // JSON as Javascript Object (Not string representation) + json = typeof json === 'string' ? this.JSON.parse(json) : json; + + verbose && console.log('Creating a empty dictionary'); + + // The dictionary + var dictionary = { + strings : [], + integers : [], + floats : [] + }; + + verbose && console.log('Creating the AST'); + + // The AST + var ast = (function recursiveAstBuilder(item) { + + verbose && console.log('Calling recursiveAstBuilder with ' + this.JSON.stringify(item)); + + // The type of the item + var type = typeof item; + + // Case 7: The item is null + if (item === null) { + return { + type : 'null', + index : TOKEN_NULL + }; + } + + //add undefined + if (typeof item === 'undefined') { + return { + type : 'undefined', + index : TOKEN_UNDEFINED + }; + } + + // Case 1: The item is Array Object + if ( item instanceof Array) { + + // Create a new sub-AST of type Array (@) + var ast = ['@']; + + // Add each items + for (var i in item) { + + if (!item.hasOwnProperty(i)) continue; + + ast.push(recursiveAstBuilder(item[i])); + } + + // And return + return ast; + + } + + // Case 2: The item is Object + if (type === 'object') { + + // Create a new sub-AST of type Object ($) + var ast = ['$']; + + // Add each items + for (var key in item) { + + if (!item.hasOwnProperty(key)) + continue; + + ast.push(recursiveAstBuilder(key)); + ast.push(recursiveAstBuilder(item[key])); + } + + // And return + return ast; + + } + + // Case 3: The item empty string + if (item === '') { + return { + type : 'empty', + index : TOKEN_EMPTY_STRING + }; + } + + // Case 4: The item is String + if (type === 'string') { + + // The index of that word in the dictionary + var index = _indexOf.call(dictionary.strings, item); + + // If not, add to the dictionary and actualize the index + if (index == -1) { + dictionary.strings.push(_encode(item)); + index = dictionary.strings.length - 1; + } + + // Return the token + return { + type : 'strings', + index : index + }; + } + + // Case 5: The item is integer + if (type === 'number' && item % 1 === 0) { + + // The index of that number in the dictionary + var index = _indexOf.call(dictionary.integers, item); + + // If not, add to the dictionary and actualize the index + if (index == -1) { + dictionary.integers.push(_base10To36(item)); + index = dictionary.integers.length - 1; + } + + // Return the token + return { + type : 'integers', + index : index + }; + } + + // Case 6: The item is float + if (type === 'number') { + // The index of that number in the dictionary + var index = _indexOf.call(dictionary.floats, item); + + // If not, add to the dictionary and actualize the index + if (index == -1) { + // Float not use base 36 + dictionary.floats.push(item); + index = dictionary.floats.length - 1; + } + + // Return the token + return { + type : 'floats', + index : index + }; + } + + // Case 7: The item is boolean + if (type === 'boolean') { + return { + type : 'boolean', + index : item ? TOKEN_TRUE : TOKEN_FALSE + }; + } + + // Default + throw new Error('Unexpected argument of type ' + typeof (item)); + + })(json); + + // A set of shorthands proxies for the length of the dictionaries + var stringLength = dictionary.strings.length; + var integerLength = dictionary.integers.length; + var floatLength = dictionary.floats.length; + + verbose && console.log('Parsing the dictionary'); + + // Create a raw dictionary + var packed = dictionary.strings.join('|'); + packed += '^' + dictionary.integers.join('|'); + packed += '^' + dictionary.floats.join('|'); + + verbose && console.log('Parsing the structure'); + + // And add the structure + packed += '^' + (function recursiveParser(item) { + + verbose && console.log('Calling a recursiveParser with ' + this.JSON.stringify(item)); + + // If the item is Array, then is a object of + // type [object Object] or [object Array] + if ( item instanceof Array) { + + // The packed resulting + var packed = item.shift(); + + for (var i in item) { + + if (!item.hasOwnProperty(i)) + continue; + + packed += recursiveParser(item[i]) + '|'; + } + + return (packed[packed.length - 1] === '|' ? packed.slice(0, -1) : packed) + ']'; + + } + + // A shorthand proxies + var type = item.type, index = item.index; + + if (type === 'strings') { + // Just return the base 36 of index + return _base10To36(index); + } + + if (type === 'integers') { + // Return a base 36 of index plus stringLength offset + return _base10To36(stringLength + index); + } + + if (type === 'floats') { + // Return a base 36 of index plus stringLength and integerLength offset + return _base10To36(stringLength + integerLength + index); + } + + if (type === 'boolean') { + return item.index; + } + + if (type === 'null') { + return TOKEN_NULL; + } + + if (type === 'undefined') { + return TOKEN_UNDEFINED; + } + + if (type === 'empty') { + return TOKEN_EMPTY_STRING; + } + + throw new TypeError('The item is alien!'); + + })(ast); + + verbose && console.log('Ending parser'); + + // If debug, return a internal representation of dictionary and stuff + if (options.debug) + return { + dictionary : dictionary, + ast : ast, + packed : packed + }; + + return packed; + +}; + +export function unpack(packed, options) { + + // Canonizes the options + options = options || {}; + + // A raw buffer + var rawBuffers = packed.split('^'); + + // Create a dictionary + options.verbose && console.log('Building dictionary'); + var dictionary = []; + + // Add the strings values + var buffer = rawBuffers[0]; + if (buffer !== '') { + buffer = buffer.split('|'); + options.verbose && console.log('Parse the strings dictionary'); + for (var i=0, n=buffer.length; i{} + this.onclose = ()=>{} + + this.cID = null + + this.evtSource.onmessage = (event) => { + console.log(`Data: ${event.data}`) + processData(event.data) + } + + this.evtSource.onerror = (event) => { + console.log('Error!') + this.connected = false + utils.icondisconnect() + utils.popupError('Connection', 'Disconnected from server') + this.onclose() + } + + this.evtSource.onopen = (event) => { + console.log('Connected!') + this.connected = true + utils.iconunauth() + this.onopen() + } + + this.send = (type, data)=>{ + //console.log({type, data}) + fetch(this.location, { + method: "post", + headers: { + 'Accept': '*', + 'Content-Type': 'text/plain' + }, + body: jsonpack.pack({ + type: type, + data: data, + cid: this.cID + }) + }) + } + + this.addRawTypeListener = (type, func)=>{ + evListeners.push({ + type: type, + func: func + }) + } +} \ No newline at end of file diff --git a/html/src/pico.min.css b/html/src/pico.min.css new file mode 100644 index 0000000..d0d5ba4 --- /dev/null +++ b/html/src/pico.min.css @@ -0,0 +1,5 @@ +@charset "UTF-8";/*! + * Pico CSS v1.5.11 (https://picocss.com) + * Copyright 2019-2023 - Licensed under MIT + */:root{--font-family:system-ui,-apple-system,"Segoe UI","Roboto","Ubuntu","Cantarell","Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--line-height:1.5;--font-weight:400;--font-size:16px;--border-radius:0.25rem;--border-width:1px;--outline-width:3px;--spacing:1rem;--typography-spacing-vertical:1.5rem;--block-spacing-vertical:calc(var(--spacing) * 2);--block-spacing-horizontal:var(--spacing);--grid-spacing-vertical:0;--grid-spacing-horizontal:var(--spacing);--form-element-spacing-vertical:0.75rem;--form-element-spacing-horizontal:1rem;--nav-element-spacing-vertical:1rem;--nav-element-spacing-horizontal:0.5rem;--nav-link-spacing-vertical:0.5rem;--nav-link-spacing-horizontal:0.5rem;--form-label-font-weight:var(--font-weight);--transition:0.2s ease-in-out;--modal-overlay-backdrop-filter:blur(0.25rem)}@media (min-width:576px){:root{--font-size:17px}}@media (min-width:768px){:root{--font-size:18px}}@media (min-width:992px){:root{--font-size:19px}}@media (min-width:1200px){:root{--font-size:20px}}@media (min-width:576px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 2.5)}}@media (min-width:768px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 3)}}@media (min-width:992px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 3.5)}}@media (min-width:1200px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 4)}}@media (min-width:576px){article{--block-spacing-horizontal:calc(var(--spacing) * 1.25)}}@media (min-width:768px){article{--block-spacing-horizontal:calc(var(--spacing) * 1.5)}}@media (min-width:992px){article{--block-spacing-horizontal:calc(var(--spacing) * 1.75)}}@media (min-width:1200px){article{--block-spacing-horizontal:calc(var(--spacing) * 2)}}dialog>article{--block-spacing-vertical:calc(var(--spacing) * 2);--block-spacing-horizontal:var(--spacing)}@media (min-width:576px){dialog>article{--block-spacing-vertical:calc(var(--spacing) * 2.5);--block-spacing-horizontal:calc(var(--spacing) * 1.25)}}@media (min-width:768px){dialog>article{--block-spacing-vertical:calc(var(--spacing) * 3);--block-spacing-horizontal:calc(var(--spacing) * 1.5)}}a{--text-decoration:none}a.contrast,a.secondary{--text-decoration:underline}small{--font-size:0.875em}h1,h2,h3,h4,h5,h6{--font-weight:700}h1{--font-size:2rem;--typography-spacing-vertical:3rem}h2{--font-size:1.75rem;--typography-spacing-vertical:2.625rem}h3{--font-size:1.5rem;--typography-spacing-vertical:2.25rem}h4{--font-size:1.25rem;--typography-spacing-vertical:1.874rem}h5{--font-size:1.125rem;--typography-spacing-vertical:1.6875rem}[type=checkbox],[type=radio]{--border-width:2px}[type=checkbox][role=switch]{--border-width:3px}tfoot td,tfoot th,thead td,thead th{--border-width:3px}:not(thead,tfoot)>*>td{--font-size:0.875em}code,kbd,pre,samp{--font-family:"Menlo","Consolas","Roboto Mono","Ubuntu Monospace","Noto Mono","Oxygen Mono","Liberation Mono",monospace,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"}kbd{--font-weight:bolder}:root:not([data-theme=dark]),[data-theme=light]{--background-color:#fff;--color:hsl(205, 20%, 32%);--h1-color:hsl(205, 30%, 15%);--h2-color:#24333e;--h3-color:hsl(205, 25%, 23%);--h4-color:#374956;--h5-color:hsl(205, 20%, 32%);--h6-color:#4d606d;--muted-color:hsl(205, 10%, 50%);--muted-border-color:hsl(205, 20%, 94%);--primary:hsl(195, 85%, 41%);--primary-hover:hsl(195, 90%, 32%);--primary-focus:rgba(16, 149, 193, 0.125);--primary-inverse:#fff;--secondary:hsl(205, 15%, 41%);--secondary-hover:hsl(205, 20%, 32%);--secondary-focus:rgba(89, 107, 120, 0.125);--secondary-inverse:#fff;--contrast:hsl(205, 30%, 15%);--contrast-hover:#000;--contrast-focus:rgba(89, 107, 120, 0.125);--contrast-inverse:#fff;--mark-background-color:#fff2ca;--mark-color:#543a26;--ins-color:#388e3c;--del-color:#c62828;--blockquote-border-color:var(--muted-border-color);--blockquote-footer-color:var(--muted-color);--button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--form-element-background-color:transparent;--form-element-border-color:hsl(205, 14%, 68%);--form-element-color:var(--color);--form-element-placeholder-color:var(--muted-color);--form-element-active-background-color:transparent;--form-element-active-border-color:var(--primary);--form-element-focus-color:var(--primary-focus);--form-element-disabled-background-color:hsl(205, 18%, 86%);--form-element-disabled-border-color:hsl(205, 14%, 68%);--form-element-disabled-opacity:0.5;--form-element-invalid-border-color:#c62828;--form-element-invalid-active-border-color:#d32f2f;--form-element-invalid-focus-color:rgba(211, 47, 47, 0.125);--form-element-valid-border-color:#388e3c;--form-element-valid-active-border-color:#43a047;--form-element-valid-focus-color:rgba(67, 160, 71, 0.125);--switch-background-color:hsl(205, 16%, 77%);--switch-color:var(--primary-inverse);--switch-checked-background-color:var(--primary);--range-border-color:hsl(205, 18%, 86%);--range-active-border-color:hsl(205, 16%, 77%);--range-thumb-border-color:var(--background-color);--range-thumb-color:var(--secondary);--range-thumb-hover-color:var(--secondary-hover);--range-thumb-active-color:var(--primary);--table-border-color:var(--muted-border-color);--table-row-stripped-background-color:#f6f8f9;--code-background-color:hsl(205, 20%, 94%);--code-color:var(--muted-color);--code-kbd-background-color:var(--contrast);--code-kbd-color:var(--contrast-inverse);--code-tag-color:hsl(330, 40%, 50%);--code-property-color:hsl(185, 40%, 40%);--code-value-color:hsl(40, 20%, 50%);--code-comment-color:hsl(205, 14%, 68%);--accordion-border-color:var(--muted-border-color);--accordion-close-summary-color:var(--color);--accordion-open-summary-color:var(--muted-color);--card-background-color:var(--background-color);--card-border-color:var(--muted-border-color);--card-box-shadow:0.0145rem 0.029rem 0.174rem rgba(27, 40, 50, 0.01698),0.0335rem 0.067rem 0.402rem rgba(27, 40, 50, 0.024),0.0625rem 0.125rem 0.75rem rgba(27, 40, 50, 0.03),0.1125rem 0.225rem 1.35rem rgba(27, 40, 50, 0.036),0.2085rem 0.417rem 2.502rem rgba(27, 40, 50, 0.04302),0.5rem 1rem 6rem rgba(27, 40, 50, 0.06),0 0 0 0.0625rem rgba(27, 40, 50, 0.015);--card-sectionning-background-color:#fbfbfc;--dropdown-background-color:#fbfbfc;--dropdown-border-color:#e1e6eb;--dropdown-box-shadow:var(--card-box-shadow);--dropdown-color:var(--color);--dropdown-hover-background-color:hsl(205, 20%, 94%);--modal-overlay-background-color:rgba(213, 220, 226, 0.7);--progress-background-color:hsl(205, 18%, 86%);--progress-color:var(--primary);--loading-spinner-opacity:0.5;--tooltip-background-color:var(--contrast);--tooltip-color:var(--contrast-inverse);--icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button-inverse:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(198, 40, 40)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");--icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(56, 142, 60)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");color-scheme:light}@media only screen and (prefers-color-scheme:dark){:root:not([data-theme]){--background-color:#11191f;--color:hsl(205, 16%, 77%);--h1-color:hsl(205, 20%, 94%);--h2-color:#e1e6eb;--h3-color:hsl(205, 18%, 86%);--h4-color:#c8d1d8;--h5-color:hsl(205, 16%, 77%);--h6-color:#afbbc4;--muted-color:hsl(205, 10%, 50%);--muted-border-color:#1f2d38;--primary:hsl(195, 85%, 41%);--primary-hover:hsl(195, 80%, 50%);--primary-focus:rgba(16, 149, 193, 0.25);--primary-inverse:#fff;--secondary:hsl(205, 15%, 41%);--secondary-hover:hsl(205, 10%, 50%);--secondary-focus:rgba(115, 130, 140, 0.25);--secondary-inverse:#fff;--contrast:hsl(205, 20%, 94%);--contrast-hover:#fff;--contrast-focus:rgba(115, 130, 140, 0.25);--contrast-inverse:#000;--mark-background-color:#d1c284;--mark-color:#11191f;--ins-color:#388e3c;--del-color:#c62828;--blockquote-border-color:var(--muted-border-color);--blockquote-footer-color:var(--muted-color);--button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--form-element-background-color:#11191f;--form-element-border-color:#374956;--form-element-color:var(--color);--form-element-placeholder-color:var(--muted-color);--form-element-active-background-color:var(--form-element-background-color);--form-element-active-border-color:var(--primary);--form-element-focus-color:var(--primary-focus);--form-element-disabled-background-color:hsl(205, 25%, 23%);--form-element-disabled-border-color:hsl(205, 20%, 32%);--form-element-disabled-opacity:0.5;--form-element-invalid-border-color:#b71c1c;--form-element-invalid-active-border-color:#c62828;--form-element-invalid-focus-color:rgba(198, 40, 40, 0.25);--form-element-valid-border-color:#2e7d32;--form-element-valid-active-border-color:#388e3c;--form-element-valid-focus-color:rgba(56, 142, 60, 0.25);--switch-background-color:#374956;--switch-color:var(--primary-inverse);--switch-checked-background-color:var(--primary);--range-border-color:#24333e;--range-active-border-color:hsl(205, 25%, 23%);--range-thumb-border-color:var(--background-color);--range-thumb-color:var(--secondary);--range-thumb-hover-color:var(--secondary-hover);--range-thumb-active-color:var(--primary);--table-border-color:var(--muted-border-color);--table-row-stripped-background-color:rgba(115, 130, 140, 0.05);--code-background-color:#18232c;--code-color:var(--muted-color);--code-kbd-background-color:var(--contrast);--code-kbd-color:var(--contrast-inverse);--code-tag-color:hsl(330, 30%, 50%);--code-property-color:hsl(185, 30%, 50%);--code-value-color:hsl(40, 10%, 50%);--code-comment-color:#4d606d;--accordion-border-color:var(--muted-border-color);--accordion-active-summary-color:var(--primary);--accordion-close-summary-color:var(--color);--accordion-open-summary-color:var(--muted-color);--card-background-color:#141e26;--card-border-color:var(--card-background-color);--card-box-shadow:0.0145rem 0.029rem 0.174rem rgba(0, 0, 0, 0.01698),0.0335rem 0.067rem 0.402rem rgba(0, 0, 0, 0.024),0.0625rem 0.125rem 0.75rem rgba(0, 0, 0, 0.03),0.1125rem 0.225rem 1.35rem rgba(0, 0, 0, 0.036),0.2085rem 0.417rem 2.502rem rgba(0, 0, 0, 0.04302),0.5rem 1rem 6rem rgba(0, 0, 0, 0.06),0 0 0 0.0625rem rgba(0, 0, 0, 0.015);--card-sectionning-background-color:#18232c;--dropdown-background-color:hsl(205, 30%, 15%);--dropdown-border-color:#24333e;--dropdown-box-shadow:var(--card-box-shadow);--dropdown-color:var(--color);--dropdown-hover-background-color:rgba(36, 51, 62, 0.75);--modal-overlay-background-color:rgba(36, 51, 62, 0.8);--progress-background-color:#24333e;--progress-color:var(--primary);--loading-spinner-opacity:0.5;--tooltip-background-color:var(--contrast);--tooltip-color:var(--contrast-inverse);--icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button-inverse:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(0, 0, 0)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(183, 28, 28)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");--icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(46, 125, 50)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");color-scheme:dark}}[data-theme=dark]{--background-color:#11191f;--color:hsl(205, 16%, 77%);--h1-color:hsl(205, 20%, 94%);--h2-color:#e1e6eb;--h3-color:hsl(205, 18%, 86%);--h4-color:#c8d1d8;--h5-color:hsl(205, 16%, 77%);--h6-color:#afbbc4;--muted-color:hsl(205, 10%, 50%);--muted-border-color:#1f2d38;--primary:hsl(195, 85%, 41%);--primary-hover:hsl(195, 80%, 50%);--primary-focus:rgba(16, 149, 193, 0.25);--primary-inverse:#fff;--secondary:hsl(205, 15%, 41%);--secondary-hover:hsl(205, 10%, 50%);--secondary-focus:rgba(115, 130, 140, 0.25);--secondary-inverse:#fff;--contrast:hsl(205, 20%, 94%);--contrast-hover:#fff;--contrast-focus:rgba(115, 130, 140, 0.25);--contrast-inverse:#000;--mark-background-color:#d1c284;--mark-color:#11191f;--ins-color:#388e3c;--del-color:#c62828;--blockquote-border-color:var(--muted-border-color);--blockquote-footer-color:var(--muted-color);--button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--form-element-background-color:#11191f;--form-element-border-color:#374956;--form-element-color:var(--color);--form-element-placeholder-color:var(--muted-color);--form-element-active-background-color:var(--form-element-background-color);--form-element-active-border-color:var(--primary);--form-element-focus-color:var(--primary-focus);--form-element-disabled-background-color:hsl(205, 25%, 23%);--form-element-disabled-border-color:hsl(205, 20%, 32%);--form-element-disabled-opacity:0.5;--form-element-invalid-border-color:#b71c1c;--form-element-invalid-active-border-color:#c62828;--form-element-invalid-focus-color:rgba(198, 40, 40, 0.25);--form-element-valid-border-color:#2e7d32;--form-element-valid-active-border-color:#388e3c;--form-element-valid-focus-color:rgba(56, 142, 60, 0.25);--switch-background-color:#374956;--switch-color:var(--primary-inverse);--switch-checked-background-color:var(--primary);--range-border-color:#24333e;--range-active-border-color:hsl(205, 25%, 23%);--range-thumb-border-color:var(--background-color);--range-thumb-color:var(--secondary);--range-thumb-hover-color:var(--secondary-hover);--range-thumb-active-color:var(--primary);--table-border-color:var(--muted-border-color);--table-row-stripped-background-color:rgba(115, 130, 140, 0.05);--code-background-color:#18232c;--code-color:var(--muted-color);--code-kbd-background-color:var(--contrast);--code-kbd-color:var(--contrast-inverse);--code-tag-color:hsl(330, 30%, 50%);--code-property-color:hsl(185, 30%, 50%);--code-value-color:hsl(40, 10%, 50%);--code-comment-color:#4d606d;--accordion-border-color:var(--muted-border-color);--accordion-active-summary-color:var(--primary);--accordion-close-summary-color:var(--color);--accordion-open-summary-color:var(--muted-color);--card-background-color:#141e26;--card-border-color:var(--card-background-color);--card-box-shadow:0.0145rem 0.029rem 0.174rem rgba(0, 0, 0, 0.01698),0.0335rem 0.067rem 0.402rem rgba(0, 0, 0, 0.024),0.0625rem 0.125rem 0.75rem rgba(0, 0, 0, 0.03),0.1125rem 0.225rem 1.35rem rgba(0, 0, 0, 0.036),0.2085rem 0.417rem 2.502rem rgba(0, 0, 0, 0.04302),0.5rem 1rem 6rem rgba(0, 0, 0, 0.06),0 0 0 0.0625rem rgba(0, 0, 0, 0.015);--card-sectionning-background-color:#18232c;--dropdown-background-color:hsl(205, 30%, 15%);--dropdown-border-color:#24333e;--dropdown-box-shadow:var(--card-box-shadow);--dropdown-color:var(--color);--dropdown-hover-background-color:rgba(36, 51, 62, 0.75);--modal-overlay-background-color:rgba(36, 51, 62, 0.8);--progress-background-color:#24333e;--progress-color:var(--primary);--loading-spinner-opacity:0.5;--tooltip-background-color:var(--contrast);--tooltip-color:var(--contrast-inverse);--icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button-inverse:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(0, 0, 0)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(183, 28, 28)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");--icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(46, 125, 50)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");color-scheme:dark}[type=checkbox],[type=radio],[type=range],progress{accent-color:var(--primary)}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--background-color);color:var(--color);font-weight:var(--font-weight);font-size:var(--font-size);line-height:var(--line-height);font-family:var(--font-family);text-rendering:optimizeLegibility;overflow-wrap:break-word;cursor:default;-moz-tab-size:4;-o-tab-size:4;tab-size:4}main{display:block}body{width:100%;margin:0}body>footer,body>header,body>main{width:100%;margin-right:auto;margin-left:auto;padding:var(--block-spacing-vertical) 0}.container,.container-fluid{width:100%;margin-right:auto;margin-left:auto;padding-right:var(--spacing);padding-left:var(--spacing)}@media (min-width:576px){.container{max-width:510px;padding-right:0;padding-left:0}}@media (min-width:768px){.container{max-width:700px}}@media (min-width:992px){.container{max-width:920px}}@media (min-width:1200px){.container{max-width:1130px}}section{margin-bottom:var(--block-spacing-vertical)}.grid{grid-column-gap:var(--grid-spacing-horizontal);grid-row-gap:var(--grid-spacing-vertical);display:grid;grid-template-columns:1fr;margin:0}@media (min-width:992px){.grid{grid-template-columns:repeat(auto-fit,minmax(0%,1fr))}}.grid>*{min-width:0}figure{display:block;margin:0;padding:0;overflow-x:auto}figure figcaption{padding:calc(var(--spacing) * .5) 0;color:var(--muted-color)}b,strong{font-weight:bolder}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}address,blockquote,dl,figure,form,ol,p,pre,table,ul{margin-top:0;margin-bottom:var(--typography-spacing-vertical);color:var(--color);font-style:normal;font-weight:var(--font-weight);font-size:var(--font-size)}[role=link],a{--color:var(--primary);--background-color:transparent;outline:0;background-color:var(--background-color);color:var(--color);-webkit-text-decoration:var(--text-decoration);text-decoration:var(--text-decoration);transition:background-color var(--transition),color var(--transition),box-shadow var(--transition),-webkit-text-decoration var(--transition);transition:background-color var(--transition),color var(--transition),text-decoration var(--transition),box-shadow var(--transition);transition:background-color var(--transition),color var(--transition),text-decoration var(--transition),box-shadow var(--transition),-webkit-text-decoration var(--transition)}[role=link]:is([aria-current],:hover,:active,:focus),a:is([aria-current],:hover,:active,:focus){--color:var(--primary-hover);--text-decoration:underline}[role=link]:focus,a:focus{--background-color:var(--primary-focus)}[role=link].secondary,a.secondary{--color:var(--secondary)}[role=link].secondary:is([aria-current],:hover,:active,:focus),a.secondary:is([aria-current],:hover,:active,:focus){--color:var(--secondary-hover)}[role=link].secondary:focus,a.secondary:focus{--background-color:var(--secondary-focus)}[role=link].contrast,a.contrast{--color:var(--contrast)}[role=link].contrast:is([aria-current],:hover,:active,:focus),a.contrast:is([aria-current],:hover,:active,:focus){--color:var(--contrast-hover)}[role=link].contrast:focus,a.contrast:focus{--background-color:var(--contrast-focus)}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--typography-spacing-vertical);color:var(--color);font-weight:var(--font-weight);font-size:var(--font-size);font-family:var(--font-family)}h1{--color:var(--h1-color)}h2{--color:var(--h2-color)}h3{--color:var(--h3-color)}h4{--color:var(--h4-color)}h5{--color:var(--h5-color)}h6{--color:var(--h6-color)}:where(address,blockquote,dl,figure,form,ol,p,pre,table,ul)~:is(h1,h2,h3,h4,h5,h6){margin-top:var(--typography-spacing-vertical)}.headings,hgroup{margin-bottom:var(--typography-spacing-vertical)}.headings>*,hgroup>*{margin-bottom:0}.headings>:last-child,hgroup>:last-child{--color:var(--muted-color);--font-weight:unset;font-size:1rem;font-family:unset}p{margin-bottom:var(--typography-spacing-vertical)}small{font-size:var(--font-size)}:where(dl,ol,ul){padding-right:0;padding-left:var(--spacing);padding-inline-start:var(--spacing);padding-inline-end:0}:where(dl,ol,ul) li{margin-bottom:calc(var(--typography-spacing-vertical) * .25)}:where(dl,ol,ul) :is(dl,ol,ul){margin:0;margin-top:calc(var(--typography-spacing-vertical) * .25)}ul li{list-style:square}mark{padding:.125rem .25rem;background-color:var(--mark-background-color);color:var(--mark-color);vertical-align:baseline}blockquote{display:block;margin:var(--typography-spacing-vertical) 0;padding:var(--spacing);border-right:none;border-left:.25rem solid var(--blockquote-border-color);border-inline-start:0.25rem solid var(--blockquote-border-color);border-inline-end:none}blockquote footer{margin-top:calc(var(--typography-spacing-vertical) * .5);color:var(--blockquote-footer-color)}abbr[title]{border-bottom:1px dotted;text-decoration:none;cursor:help}ins{color:var(--ins-color);text-decoration:none}del{color:var(--del-color)}::-moz-selection{background-color:var(--primary-focus)}::selection{background-color:var(--primary-focus)}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}:where(iframe){border-style:none}img{max-width:100%;height:auto;border-style:none}:where(svg:not([fill])){fill:currentColor}svg:not(:root){overflow:hidden}button{margin:0;overflow:visible;font-family:inherit;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}button{display:block;width:100%;margin-bottom:var(--spacing)}[role=button]{display:inline-block;text-decoration:none}[role=button],button,input[type=button],input[type=reset],input[type=submit]{--background-color:var(--primary);--border-color:var(--primary);--color:var(--primary-inverse);--box-shadow:var(--button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));padding:var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[role=button]:is([aria-current],:hover,:active,:focus),button:is([aria-current],:hover,:active,:focus),input[type=button]:is([aria-current],:hover,:active,:focus),input[type=reset]:is([aria-current],:hover,:active,:focus),input[type=submit]:is([aria-current],:hover,:active,:focus){--background-color:var(--primary-hover);--border-color:var(--primary-hover);--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));--color:var(--primary-inverse)}[role=button]:focus,button:focus,input[type=button]:focus,input[type=reset]:focus,input[type=submit]:focus{--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--outline-width) var(--primary-focus)}:is(button,input[type=submit],input[type=button],[role=button]).secondary,input[type=reset]{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);cursor:pointer}:is(button,input[type=submit],input[type=button],[role=button]).secondary:is([aria-current],:hover,:active,:focus),input[type=reset]:is([aria-current],:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover);--color:var(--secondary-inverse)}:is(button,input[type=submit],input[type=button],[role=button]).secondary:focus,input[type=reset]:focus{--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--outline-width) var(--secondary-focus)}:is(button,input[type=submit],input[type=button],[role=button]).contrast{--background-color:var(--contrast);--border-color:var(--contrast);--color:var(--contrast-inverse)}:is(button,input[type=submit],input[type=button],[role=button]).contrast:is([aria-current],:hover,:active,:focus){--background-color:var(--contrast-hover);--border-color:var(--contrast-hover);--color:var(--contrast-inverse)}:is(button,input[type=submit],input[type=button],[role=button]).contrast:focus{--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--outline-width) var(--contrast-focus)}:is(button,input[type=submit],input[type=button],[role=button]).outline,input[type=reset].outline{--background-color:transparent;--color:var(--primary)}:is(button,input[type=submit],input[type=button],[role=button]).outline:is([aria-current],:hover,:active,:focus),input[type=reset].outline:is([aria-current],:hover,:active,:focus){--background-color:transparent;--color:var(--primary-hover)}:is(button,input[type=submit],input[type=button],[role=button]).outline.secondary,input[type=reset].outline{--color:var(--secondary)}:is(button,input[type=submit],input[type=button],[role=button]).outline.secondary:is([aria-current],:hover,:active,:focus),input[type=reset].outline:is([aria-current],:hover,:active,:focus){--color:var(--secondary-hover)}:is(button,input[type=submit],input[type=button],[role=button]).outline.contrast{--color:var(--contrast)}:is(button,input[type=submit],input[type=button],[role=button]).outline.contrast:is([aria-current],:hover,:active,:focus){--color:var(--contrast-hover)}:where(button,[type=submit],[type=button],[type=reset],[role=button])[disabled],:where(fieldset[disabled]) :is(button,[type=submit],[type=button],[type=reset],[role=button]),a[role=button]:not([href]){opacity:.5;pointer-events:none}input,optgroup,select,textarea{margin:0;font-size:1rem;line-height:var(--line-height);font-family:inherit;letter-spacing:inherit}input{overflow:visible}select{text-transform:none}legend{max-width:100%;padding:0;color:inherit;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{padding:0}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{padding:0;border-style:none}:-moz-focusring{outline:0}:-moz-ui-invalid{box-shadow:none}::-ms-expand{display:none}[type=file],[type=range]{padding:0;border-width:0}input:not([type=checkbox],[type=radio],[type=range]){height:calc(1rem * var(--line-height) + var(--form-element-spacing-vertical) * 2 + var(--border-width) * 2)}fieldset{margin:0;margin-bottom:var(--spacing);padding:0;border:0}fieldset legend,label{display:block;margin-bottom:calc(var(--spacing) * .25);font-weight:var(--form-label-font-weight,var(--font-weight))}input:not([type=checkbox],[type=radio]),select,textarea{width:100%}input:not([type=checkbox],[type=radio],[type=range],[type=file]),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal)}input,select,textarea{--background-color:var(--form-element-background-color);--border-color:var(--form-element-border-color);--color:var(--form-element-color);--box-shadow:none;border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}:where(select,textarea):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[readonly]):is(:active,:focus){--background-color:var(--form-element-active-background-color)}:where(select,textarea):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[role=switch],[readonly]):is(:active,:focus){--border-color:var(--form-element-active-border-color)}input:not([type=submit],[type=button],[type=reset],[type=range],[type=file],[readonly]):focus,select:focus,textarea:focus{--box-shadow:0 0 0 var(--outline-width) var(--form-element-focus-color)}:where(fieldset[disabled]) :is(input:not([type=submit],[type=button],[type=reset]),select,textarea),input:not([type=submit],[type=button],[type=reset])[disabled],select[disabled],textarea[disabled]{--background-color:var(--form-element-disabled-background-color);--border-color:var(--form-element-disabled-border-color);opacity:var(--form-element-disabled-opacity);pointer-events:none}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week])[aria-invalid]{padding-right:calc(var(--form-element-spacing-horizontal) + 1.5rem)!important;padding-left:var(--form-element-spacing-horizontal);padding-inline-start:var(--form-element-spacing-horizontal)!important;padding-inline-end:calc(var(--form-element-spacing-horizontal) + 1.5rem)!important;background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week])[aria-invalid=false]{background-image:var(--icon-valid)}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week])[aria-invalid=true]{background-image:var(--icon-invalid)}:where(input,select,textarea)[aria-invalid=false]{--border-color:var(--form-element-valid-border-color)}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus){--border-color:var(--form-element-valid-active-border-color)!important;--box-shadow:0 0 0 var(--outline-width) var(--form-element-valid-focus-color)!important}:where(input,select,textarea)[aria-invalid=true]{--border-color:var(--form-element-invalid-border-color)}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus){--border-color:var(--form-element-invalid-active-border-color)!important;--box-shadow:0 0 0 var(--outline-width) var(--form-element-invalid-focus-color)!important}[dir=rtl] :where(input,select,textarea):not([type=checkbox],[type=radio]):is([aria-invalid],[aria-invalid=true],[aria-invalid=false]){background-position:center left .75rem}input::-webkit-input-placeholder,input::placeholder,select:invalid,textarea::-webkit-input-placeholder,textarea::placeholder{color:var(--form-element-placeholder-color);opacity:1}input:not([type=checkbox],[type=radio]),select,textarea{margin-bottom:var(--spacing)}select::-ms-expand{border:0;background-color:transparent}select:not([multiple],[size]){padding-right:calc(var(--form-element-spacing-horizontal) + 1.5rem);padding-left:var(--form-element-spacing-horizontal);padding-inline-start:var(--form-element-spacing-horizontal);padding-inline-end:calc(var(--form-element-spacing-horizontal) + 1.5rem);background-image:var(--icon-chevron);background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}[dir=rtl] select:not([multiple],[size]){background-position:center left .75rem}:where(input,select,textarea,.grid)+small{display:block;width:100%;margin-top:calc(var(--spacing) * -.75);margin-bottom:var(--spacing);color:var(--muted-color)}label>:where(input,select,textarea){margin-top:calc(var(--spacing) * .25)}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:1.25em;height:1.25em;margin-top:-.125em;margin-right:.375em;margin-left:0;margin-inline-start:0;margin-inline-end:.375em;border-width:var(--border-width);font-size:inherit;vertical-align:middle;cursor:pointer}[type=checkbox]::-ms-check,[type=radio]::-ms-check{display:none}[type=checkbox]:checked,[type=checkbox]:checked:active,[type=checkbox]:checked:focus,[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--background-color:var(--primary);--border-color:var(--primary);background-image:var(--icon-checkbox);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=checkbox]~label,[type=radio]~label{display:inline-block;margin-right:.375em;margin-bottom:0;cursor:pointer}[type=checkbox]:indeterminate{--background-color:var(--primary);--border-color:var(--primary);background-image:var(--icon-minus);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=radio]{border-radius:50%}[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--background-color:var(--primary-inverse);border-width:.35em;background-image:none}[type=checkbox][role=switch]{--background-color:var(--switch-background-color);--border-color:var(--switch-background-color);--color:var(--switch-color);width:2.25em;height:1.25em;border:var(--border-width) solid var(--border-color);border-radius:1.25em;background-color:var(--background-color);line-height:1.25em}[type=checkbox][role=switch]:focus{--background-color:var(--switch-background-color);--border-color:var(--switch-background-color)}[type=checkbox][role=switch]:checked{--background-color:var(--switch-checked-background-color);--border-color:var(--switch-checked-background-color)}[type=checkbox][role=switch]:before{display:block;width:calc(1.25em - (var(--border-width) * 2));height:100%;border-radius:50%;background-color:var(--color);content:"";transition:margin .1s ease-in-out}[type=checkbox][role=switch]:checked{background-image:none}[type=checkbox][role=switch]:checked::before{margin-left:calc(1.125em - var(--border-width));margin-inline-start:calc(1.125em - var(--border-width))}[type=checkbox]:checked[aria-invalid=false],[type=checkbox][aria-invalid=false],[type=checkbox][role=switch]:checked[aria-invalid=false],[type=checkbox][role=switch][aria-invalid=false],[type=radio]:checked[aria-invalid=false],[type=radio][aria-invalid=false]{--border-color:var(--form-element-valid-border-color)}[type=checkbox]:checked[aria-invalid=true],[type=checkbox][aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true],[type=checkbox][role=switch][aria-invalid=true],[type=radio]:checked[aria-invalid=true],[type=radio][aria-invalid=true]{--border-color:var(--form-element-invalid-border-color)}[type=color]::-webkit-color-swatch-wrapper{padding:0}[type=color]::-moz-focus-inner{padding:0}[type=color]::-webkit-color-swatch{border:0;border-radius:calc(var(--border-radius) * .5)}[type=color]::-moz-color-swatch{border:0;border-radius:calc(var(--border-radius) * .5)}input:not([type=checkbox],[type=radio],[type=range],[type=file]):is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){--icon-position:0.75rem;--icon-width:1rem;padding-right:calc(var(--icon-width) + var(--icon-position));background-image:var(--icon-date);background-position:center right var(--icon-position);background-size:var(--icon-width) auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=time]{background-image:var(--icon-time)}[type=date]::-webkit-calendar-picker-indicator,[type=datetime-local]::-webkit-calendar-picker-indicator,[type=month]::-webkit-calendar-picker-indicator,[type=time]::-webkit-calendar-picker-indicator,[type=week]::-webkit-calendar-picker-indicator{width:var(--icon-width);margin-right:calc(var(--icon-width) * -1);margin-left:var(--icon-position);opacity:0}[dir=rtl] :is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){text-align:right}@-moz-document url-prefix(){[type=date],[type=datetime-local],[type=month],[type=time],[type=week]{padding-right:var(--form-element-spacing-horizontal)!important;background-image:none!important}}[type=file]{--color:var(--muted-color);padding:calc(var(--form-element-spacing-vertical) * .5) 0;border:0;border-radius:0;background:0 0}[type=file]::file-selector-button{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);margin-right:calc(var(--spacing)/ 2);margin-left:0;margin-inline-start:0;margin-inline-end:calc(var(--spacing)/ 2);padding:calc(var(--form-element-spacing-vertical) * .5) calc(var(--form-element-spacing-horizontal) * .5);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[type=file]::file-selector-button:is(:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover)}[type=file]::-webkit-file-upload-button{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);margin-right:calc(var(--spacing)/ 2);margin-left:0;margin-inline-start:0;margin-inline-end:calc(var(--spacing)/ 2);padding:calc(var(--form-element-spacing-vertical) * .5) calc(var(--form-element-spacing-horizontal) * .5);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;-webkit-transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[type=file]::-webkit-file-upload-button:is(:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover)}[type=file]::-ms-browse{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);margin-right:calc(var(--spacing)/ 2);margin-left:0;margin-inline-start:0;margin-inline-end:calc(var(--spacing)/ 2);padding:calc(var(--form-element-spacing-vertical) * .5) calc(var(--form-element-spacing-horizontal) * .5);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;-ms-transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[type=file]::-ms-browse:is(:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover)}[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:1.25rem;background:0 0}[type=range]::-webkit-slider-runnable-track{width:100%;height:.25rem;border-radius:var(--border-radius);background-color:var(--range-border-color);-webkit-transition:background-color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),box-shadow var(--transition)}[type=range]::-moz-range-track{width:100%;height:.25rem;border-radius:var(--border-radius);background-color:var(--range-border-color);-moz-transition:background-color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),box-shadow var(--transition)}[type=range]::-ms-track{width:100%;height:.25rem;border-radius:var(--border-radius);background-color:var(--range-border-color);-ms-transition:background-color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),box-shadow var(--transition)}[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.5rem;border:2px solid var(--range-thumb-border-color);border-radius:50%;background-color:var(--range-thumb-color);cursor:pointer;-webkit-transition:background-color var(--transition),transform var(--transition);transition:background-color var(--transition),transform var(--transition)}[type=range]::-moz-range-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.5rem;border:2px solid var(--range-thumb-border-color);border-radius:50%;background-color:var(--range-thumb-color);cursor:pointer;-moz-transition:background-color var(--transition),transform var(--transition);transition:background-color var(--transition),transform var(--transition)}[type=range]::-ms-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.5rem;border:2px solid var(--range-thumb-border-color);border-radius:50%;background-color:var(--range-thumb-color);cursor:pointer;-ms-transition:background-color var(--transition),transform var(--transition);transition:background-color var(--transition),transform var(--transition)}[type=range]:focus,[type=range]:hover{--range-border-color:var(--range-active-border-color);--range-thumb-color:var(--range-thumb-hover-color)}[type=range]:active{--range-thumb-color:var(--range-thumb-active-color)}[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}[type=range]:active::-moz-range-thumb{transform:scale(1.25)}[type=range]:active::-ms-thumb{transform:scale(1.25)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{padding-inline-start:calc(var(--form-element-spacing-horizontal) + 1.75rem);border-radius:5rem;background-image:var(--icon-search);background-position:center left 1.125rem;background-size:1rem auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{padding-inline-start:calc(var(--form-element-spacing-horizontal) + 1.75rem)!important;background-position:center left 1.125rem,center right .75rem}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=false]{background-image:var(--icon-search),var(--icon-valid)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=true]{background-image:var(--icon-search),var(--icon-invalid)}[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;display:none}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{background-position:center right 1.125rem}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{background-position:center right 1.125rem,center left .75rem}:where(table){width:100%;border-collapse:collapse;border-spacing:0;text-indent:0}td,th{padding:calc(var(--spacing)/ 2) var(--spacing);border-bottom:var(--border-width) solid var(--table-border-color);color:var(--color);font-weight:var(--font-weight);font-size:var(--font-size);text-align:left;text-align:start}tfoot td,tfoot th{border-top:var(--border-width) solid var(--table-border-color);border-bottom:0}table[role=grid] tbody tr:nth-child(odd){background-color:var(--table-row-stripped-background-color)}code,kbd,pre,samp{font-size:.875em;font-family:var(--font-family)}pre{-ms-overflow-style:scrollbar;overflow:auto}code,kbd,pre{border-radius:var(--border-radius);background:var(--code-background-color);color:var(--code-color);font-weight:var(--font-weight);line-height:initial}code,kbd{display:inline-block;padding:.375rem .5rem}pre{display:block;margin-bottom:var(--spacing);overflow-x:auto}pre>code{display:block;padding:var(--spacing);background:0 0;font-size:14px;line-height:var(--line-height)}code b{color:var(--code-tag-color);font-weight:var(--font-weight)}code i{color:var(--code-property-color);font-style:normal}code u{color:var(--code-value-color);text-decoration:none}code em{color:var(--code-comment-color);font-style:normal}kbd{background-color:var(--code-kbd-background-color);color:var(--code-kbd-color);vertical-align:baseline}hr{height:0;border:0;border-top:1px solid var(--muted-border-color);color:inherit}[hidden],template{display:none!important}canvas{display:inline-block}details{display:block;margin-bottom:var(--spacing);padding-bottom:var(--spacing);border-bottom:var(--border-width) solid var(--accordion-border-color)}details summary{line-height:1rem;list-style-type:none;cursor:pointer;transition:color var(--transition)}details summary:not([role]){color:var(--accordion-close-summary-color)}details summary::-webkit-details-marker{display:none}details summary::marker{display:none}details summary::-moz-list-bullet{list-style-type:none}details summary::after{display:block;width:1rem;height:1rem;margin-inline-start:calc(var(--spacing,1rem) * .5);float:right;transform:rotate(-90deg);background-image:var(--icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:"";transition:transform var(--transition)}details summary:focus{outline:0}details summary:focus:not([role=button]){color:var(--accordion-active-summary-color)}details summary[role=button]{width:100%;text-align:left}details summary[role=button]::after{height:calc(1rem * var(--line-height,1.5));background-image:var(--icon-chevron-button)}details summary[role=button]:not(.outline).contrast::after{background-image:var(--icon-chevron-button-inverse)}details[open]>summary{margin-bottom:calc(var(--spacing))}details[open]>summary:not([role]):not(:focus){color:var(--accordion-open-summary-color)}details[open]>summary::after{transform:rotate(0)}[dir=rtl] details summary{text-align:right}[dir=rtl] details summary::after{float:left;background-position:left center}article{margin:var(--block-spacing-vertical) 0;padding:var(--block-spacing-vertical) var(--block-spacing-horizontal);border-radius:var(--border-radius);background:var(--card-background-color);box-shadow:var(--card-box-shadow)}article>footer,article>header{margin-right:calc(var(--block-spacing-horizontal) * -1);margin-left:calc(var(--block-spacing-horizontal) * -1);padding:calc(var(--block-spacing-vertical) * .66) var(--block-spacing-horizontal);background-color:var(--card-sectionning-background-color)}article>header{margin-top:calc(var(--block-spacing-vertical) * -1);margin-bottom:var(--block-spacing-vertical);border-bottom:var(--border-width) solid var(--card-border-color);border-top-right-radius:var(--border-radius);border-top-left-radius:var(--border-radius)}article>footer{margin-top:var(--block-spacing-vertical);margin-bottom:calc(var(--block-spacing-vertical) * -1);border-top:var(--border-width) solid var(--card-border-color);border-bottom-right-radius:var(--border-radius);border-bottom-left-radius:var(--border-radius)}:root{--scrollbar-width:0px}dialog{display:flex;z-index:999;position:fixed;top:0;right:0;bottom:0;left:0;align-items:center;justify-content:center;width:inherit;min-width:100%;height:inherit;min-height:100%;padding:var(--spacing);border:0;-webkit-backdrop-filter:var(--modal-overlay-backdrop-filter);backdrop-filter:var(--modal-overlay-backdrop-filter);background-color:var(--modal-overlay-background-color);color:var(--color)}dialog article{max-height:calc(100vh - var(--spacing) * 2);overflow:auto}@media (min-width:576px){dialog article{max-width:510px}}@media (min-width:768px){dialog article{max-width:700px}}dialog article>footer,dialog article>header{padding:calc(var(--block-spacing-vertical) * .5) var(--block-spacing-horizontal)}dialog article>header .close{margin:0;margin-left:var(--spacing);float:right}dialog article>footer{text-align:right}dialog article>footer [role=button]{margin-bottom:0}dialog article>footer [role=button]:not(:first-of-type){margin-left:calc(var(--spacing) * .5)}dialog article p:last-of-type{margin:0}dialog article .close{display:block;width:1rem;height:1rem;margin-top:calc(var(--block-spacing-vertical) * -.5);margin-bottom:var(--typography-spacing-vertical);margin-left:auto;background-image:var(--icon-close);background-position:center;background-size:auto 1rem;background-repeat:no-repeat;opacity:.5;transition:opacity var(--transition)}dialog article .close:is([aria-current],:hover,:active,:focus){opacity:1}dialog:not([open]),dialog[open=false]{display:none}.modal-is-open{padding-right:var(--scrollbar-width,0);overflow:hidden;pointer-events:none;touch-action:none}.modal-is-open dialog{pointer-events:auto}:where(.modal-is-opening,.modal-is-closing) dialog,:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-duration:.2s;animation-timing-function:ease-in-out;animation-fill-mode:both}:where(.modal-is-opening,.modal-is-closing) dialog{animation-duration:.8s;animation-name:modal-overlay}:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-delay:.2s;animation-name:modal}.modal-is-closing dialog,.modal-is-closing dialog>article{animation-delay:0s;animation-direction:reverse}@keyframes modal-overlay{from{-webkit-backdrop-filter:none;backdrop-filter:none;background-color:transparent}}@keyframes modal{from{transform:translateY(-100%);opacity:0}}:where(nav li)::before{float:left;content:"​"}nav,nav ul{display:flex}nav{justify-content:space-between}nav ol,nav ul{align-items:center;margin-bottom:0;padding:0;list-style:none}nav ol:first-of-type,nav ul:first-of-type{margin-left:calc(var(--nav-element-spacing-horizontal) * -1)}nav ol:last-of-type,nav ul:last-of-type{margin-right:calc(var(--nav-element-spacing-horizontal) * -1)}nav li{display:inline-block;margin:0;padding:var(--nav-element-spacing-vertical) var(--nav-element-spacing-horizontal)}nav li>*{--spacing:0}nav :where(a,[role=link]){display:inline-block;margin:calc(var(--nav-link-spacing-vertical) * -1) calc(var(--nav-link-spacing-horizontal) * -1);padding:var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal);border-radius:var(--border-radius);text-decoration:none}nav :where(a,[role=link]):is([aria-current],:hover,:active,:focus){text-decoration:none}nav[aria-label=breadcrumb]{align-items:center;justify-content:start}nav[aria-label=breadcrumb] ul li:not(:first-child){margin-inline-start:var(--nav-link-spacing-horizontal)}nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{position:absolute;width:calc(var(--nav-link-spacing-horizontal) * 2);margin-inline-start:calc(var(--nav-link-spacing-horizontal)/ 2);content:"/";color:var(--muted-color);text-align:center}nav[aria-label=breadcrumb] a[aria-current]{background-color:transparent;color:inherit;text-decoration:none;pointer-events:none}nav [role=button]{margin-right:inherit;margin-left:inherit;padding:var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal)}aside li,aside nav,aside ol,aside ul{display:block}aside li{padding:calc(var(--nav-element-spacing-vertical) * .5) var(--nav-element-spacing-horizontal)}aside li a{display:block}aside li [role=button]{margin:inherit}[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{content:"\\"}progress{display:inline-block;vertical-align:baseline}progress{-webkit-appearance:none;-moz-appearance:none;display:inline-block;appearance:none;width:100%;height:.5rem;margin-bottom:calc(var(--spacing) * .5);overflow:hidden;border:0;border-radius:var(--border-radius);background-color:var(--progress-background-color);color:var(--progress-color)}progress::-webkit-progress-bar{border-radius:var(--border-radius);background:0 0}progress[value]::-webkit-progress-value{background-color:var(--progress-color)}progress::-moz-progress-bar{background-color:var(--progress-color)}@media (prefers-reduced-motion:no-preference){progress:indeterminate{background:var(--progress-background-color) linear-gradient(to right,var(--progress-color) 30%,var(--progress-background-color) 30%) top left/150% 150% no-repeat;animation:progress-indeterminate 1s linear infinite}progress:indeterminate[value]::-webkit-progress-value{background-color:transparent}progress:indeterminate::-moz-progress-bar{background-color:transparent}}@media (prefers-reduced-motion:no-preference){[dir=rtl] progress:indeterminate{animation-direction:reverse}}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}details[role=list],li[role=list]{position:relative}details[role=list] summary+ul,li[role=list]>ul{display:flex;z-index:99;position:absolute;top:auto;right:0;left:0;flex-direction:column;margin:0;padding:0;border:var(--border-width) solid var(--dropdown-border-color);border-radius:var(--border-radius);border-top-right-radius:0;border-top-left-radius:0;background-color:var(--dropdown-background-color);box-shadow:var(--card-box-shadow);color:var(--dropdown-color);white-space:nowrap}details[role=list] summary+ul li,li[role=list]>ul li{width:100%;margin-bottom:0;padding:calc(var(--form-element-spacing-vertical) * .5) var(--form-element-spacing-horizontal);list-style:none}details[role=list] summary+ul li:first-of-type,li[role=list]>ul li:first-of-type{margin-top:calc(var(--form-element-spacing-vertical) * .5)}details[role=list] summary+ul li:last-of-type,li[role=list]>ul li:last-of-type{margin-bottom:calc(var(--form-element-spacing-vertical) * .5)}details[role=list] summary+ul li a,li[role=list]>ul li a{display:block;margin:calc(var(--form-element-spacing-vertical) * -.5) calc(var(--form-element-spacing-horizontal) * -1);padding:calc(var(--form-element-spacing-vertical) * .5) var(--form-element-spacing-horizontal);overflow:hidden;color:var(--dropdown-color);text-decoration:none;text-overflow:ellipsis}details[role=list] summary+ul li a:hover,li[role=list]>ul li a:hover{background-color:var(--dropdown-hover-background-color)}details[role=list] summary::after,li[role=list]>a::after{display:block;width:1rem;height:calc(1rem * var(--line-height,1.5));margin-inline-start:.5rem;float:right;transform:rotate(0);background-image:var(--icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:""}details[role=list]{padding:0;border-bottom:none}details[role=list] summary{margin-bottom:0}details[role=list] summary:not([role]){height:calc(1rem * var(--line-height) + var(--form-element-spacing-vertical) * 2 + var(--border-width) * 2);padding:var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal);border:var(--border-width) solid var(--form-element-border-color);border-radius:var(--border-radius);background-color:var(--form-element-background-color);color:var(--form-element-placeholder-color);line-height:inherit;cursor:pointer;transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}details[role=list] summary:not([role]):active,details[role=list] summary:not([role]):focus{border-color:var(--form-element-active-border-color);background-color:var(--form-element-active-background-color)}details[role=list] summary:not([role]):focus{box-shadow:0 0 0 var(--outline-width) var(--form-element-focus-color)}details[role=list][open] summary{border-bottom-right-radius:0;border-bottom-left-radius:0}details[role=list][open] summary::before{display:block;z-index:1;position:fixed;top:0;right:0;bottom:0;left:0;background:0 0;content:"";cursor:default}nav details[role=list] summary,nav li[role=list] a{display:flex;direction:ltr}nav details[role=list] summary+ul,nav li[role=list]>ul{min-width:-moz-fit-content;min-width:fit-content;border-radius:var(--border-radius)}nav details[role=list] summary+ul li a,nav li[role=list]>ul li a{border-radius:0}nav details[role=list] summary,nav details[role=list] summary:not([role]){height:auto;padding:var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal)}nav details[role=list][open] summary{border-radius:var(--border-radius)}nav details[role=list] summary+ul{margin-top:var(--outline-width);margin-inline-start:0}nav details[role=list] summary[role=link]{margin-bottom:calc(var(--nav-link-spacing-vertical) * -1);line-height:var(--line-height)}nav details[role=list] summary[role=link]+ul{margin-top:calc(var(--nav-link-spacing-vertical) + var(--outline-width));margin-inline-start:calc(var(--nav-link-spacing-horizontal) * -1)}li[role=list] a:active~ul,li[role=list] a:focus~ul,li[role=list]:hover>ul{display:flex}li[role=list]>ul{display:none;margin-top:calc(var(--nav-link-spacing-vertical) + var(--outline-width));margin-inline-start:calc(var(--nav-element-spacing-horizontal) - var(--nav-link-spacing-horizontal))}li[role=list]>a::after{background-image:var(--icon-chevron)}label>details[role=list]{margin-top:calc(var(--spacing) * .25);margin-bottom:var(--spacing)}[aria-busy=true]{cursor:progress}[aria-busy=true]:not(input,select,textarea,html)::before{display:inline-block;width:1em;height:1em;border:.1875em solid currentColor;border-radius:1em;border-right-color:transparent;content:"";vertical-align:text-bottom;vertical-align:-.125em;animation:spinner .75s linear infinite;opacity:var(--loading-spinner-opacity)}[aria-busy=true]:not(input,select,textarea,html):not(:empty)::before{margin-right:calc(var(--spacing) * .5);margin-left:0;margin-inline-start:0;margin-inline-end:calc(var(--spacing) * .5)}[aria-busy=true]:not(input,select,textarea,html):empty{text-align:center}a[aria-busy=true],button[aria-busy=true],input[type=button][aria-busy=true],input[type=reset][aria-busy=true],input[type=submit][aria-busy=true]{pointer-events:none}@keyframes spinner{to{transform:rotate(360deg)}}[data-tooltip]{position:relative}[data-tooltip]:not(a,button,input){border-bottom:1px dotted;text-decoration:none;cursor:help}[data-tooltip]::after,[data-tooltip]::before,[data-tooltip][data-placement=top]::after,[data-tooltip][data-placement=top]::before{display:block;z-index:99;position:absolute;bottom:100%;left:50%;padding:.25rem .5rem;overflow:hidden;transform:translate(-50%,-.25rem);border-radius:var(--border-radius);background:var(--tooltip-background-color);content:attr(data-tooltip);color:var(--tooltip-color);font-style:normal;font-weight:var(--font-weight);font-size:.875rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none}[data-tooltip]::after,[data-tooltip][data-placement=top]::after{padding:0;transform:translate(-50%,0);border-top:.3rem solid;border-right:.3rem solid transparent;border-left:.3rem solid transparent;border-radius:0;background-color:transparent;content:"";color:var(--tooltip-background-color)}[data-tooltip][data-placement=bottom]::after,[data-tooltip][data-placement=bottom]::before{top:100%;bottom:auto;transform:translate(-50%,.25rem)}[data-tooltip][data-placement=bottom]:after{transform:translate(-50%,-.3rem);border:.3rem solid transparent;border-bottom:.3rem solid}[data-tooltip][data-placement=left]::after,[data-tooltip][data-placement=left]::before{top:50%;right:100%;bottom:auto;left:auto;transform:translate(-.25rem,-50%)}[data-tooltip][data-placement=left]:after{transform:translate(.3rem,-50%);border:.3rem solid transparent;border-left:.3rem solid}[data-tooltip][data-placement=right]::after,[data-tooltip][data-placement=right]::before{top:50%;right:auto;bottom:auto;left:100%;transform:translate(.25rem,-50%)}[data-tooltip][data-placement=right]:after{transform:translate(-.3rem,-50%);border:.3rem solid transparent;border-right:.3rem solid}[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}@media (hover:hover) and (pointer:fine){[data-tooltip]:hover::after,[data-tooltip]:hover::before,[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover [data-tooltip]:focus::after,[data-tooltip][data-placement=bottom]:hover [data-tooltip]:focus::before{animation-duration:.2s;animation-name:tooltip-slide-top}[data-tooltip]:hover::after,[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover [data-tooltip]:focus::after{animation-name:tooltip-caret-slide-top}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover::after,[data-tooltip][data-placement=bottom]:hover::before{animation-duration:.2s;animation-name:tooltip-slide-bottom}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover::after{animation-name:tooltip-caret-slide-bottom}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:focus::before,[data-tooltip][data-placement=left]:hover::after,[data-tooltip][data-placement=left]:hover::before{animation-duration:.2s;animation-name:tooltip-slide-left}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:hover::after{animation-name:tooltip-caret-slide-left}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:focus::before,[data-tooltip][data-placement=right]:hover::after,[data-tooltip][data-placement=right]:hover::before{animation-duration:.2s;animation-name:tooltip-slide-right}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:hover::after{animation-name:tooltip-caret-slide-right}}@keyframes tooltip-slide-top{from{transform:translate(-50%,.75rem);opacity:0}to{transform:translate(-50%,-.25rem);opacity:1}}@keyframes tooltip-caret-slide-top{from{opacity:0}50%{transform:translate(-50%,-.25rem);opacity:0}to{transform:translate(-50%,0);opacity:1}}@keyframes tooltip-slide-bottom{from{transform:translate(-50%,-.75rem);opacity:0}to{transform:translate(-50%,.25rem);opacity:1}}@keyframes tooltip-caret-slide-bottom{from{opacity:0}50%{transform:translate(-50%,-.5rem);opacity:0}to{transform:translate(-50%,-.3rem);opacity:1}}@keyframes tooltip-slide-left{from{transform:translate(.75rem,-50%);opacity:0}to{transform:translate(-.25rem,-50%);opacity:1}}@keyframes tooltip-caret-slide-left{from{opacity:0}50%{transform:translate(.05rem,-50%);opacity:0}to{transform:translate(.3rem,-50%);opacity:1}}@keyframes tooltip-slide-right{from{transform:translate(-.75rem,-50%);opacity:0}to{transform:translate(.25rem,-50%);opacity:1}}@keyframes tooltip-caret-slide-right{from{opacity:0}50%{transform:translate(-.05rem,-50%);opacity:0}to{transform:translate(-.3rem,-50%);opacity:1}}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation}[dir=rtl]{direction:rtl}@media (prefers-reduced-motion:reduce){:not([aria-busy=true]),:not([aria-busy=true])::after,:not([aria-busy=true])::before{background-attachment:initial!important;animation-duration:1ms!important;animation-delay:-1ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important}} +/*# sourceMappingURL=pico.min.css.map */ \ No newline at end of file diff --git a/html/src/style.css b/html/src/style.css new file mode 100755 index 0000000..31e2e96 --- /dev/null +++ b/html/src/style.css @@ -0,0 +1,270 @@ +@font-face { + font-family: "UbuntuMono"; + src: url(/src/UbuntuMono-R.ttf) format("truetype"); +} + +:root { + --topnavheight: 45px; + --sidenavwidth: 150px; + --popupBoxWidth: 300px; + + --background-0: #212529; + --background-1: #404040; + + --text-0: #242424; + --text-1: #d3d3d3; + + --text-black: #242424; + --text-white: #d3d3d3; + + --success-1:#059100; + --warning-1:#ffdc3e; + --error-1: #b60f0f; + + --font: "UbuntuMono"; +} + +* { + font-family: var(--font); +} + +body { + position:fixed; + right: 0; + height: 0; + overflow: scroll; + width: calc( 100vw - var(--sidenavwidth) + 32px ); + height: calc( 100vh - var(--topnavheight) + 32px ); +} + +.maindiv { + position: fixed; + top: var(--topnavheight); + left: var(--sidenavwidth); + width: calc(100% - var(--sidenavwidth)); + height: calc(100% - var(--topnavheight)); + padding: 5px; +} + +.text-white { + color: var(--text-white) !important; + font-family: var(--font); +} + +.text-black { + color: var(--text-black) !important; + font-family: var(--font); +} + +.navbar { + position: fixed; + top:0; + left:0; + right:0; + height:var(--topnavheight); + padding:5px; + padding-left: calc(var(--sidenavwidth) + 10px); + z-index: 10; + background-color: var(--background-0); +} + +.navbar ul { + position: fixed; + top: calc(var(--topnavheight) * 0.1); + padding: 0px; + /* height: var(--topnavheight); */ + height: calc(var(--topnavheight) * 0.8); + padding-right: calc(var(--topnavheight) * 2.8); + left: var(--sidenavwidth); + width: calc(100% - var(--sidenavwidth)); + overflow-x: scroll; + overflow-y: hidden; +} + +.navpanel { + display: flex; + flex-direction: column; + flex-shrink: 0; + font-size: 16px; + font-weight: 400; + line-height: 24px; + overflow: hidden; + padding-left: 14px; + + + position:fixed; + top: var(--topnavheight); + bottom:0; + left:0; + width: var(--sidenavwidth); + height:100%; + resize: left; + z-index: 20; + background-color: var(--background-0); +} + +.navpanel details { + padding-bottom: 0px; + padding-left: 5px; + margin-bottom: 0px; +} + +.navpanel summary { + font-size: 15px; + padding-top: 10px; + padding-bottom: 10px; + margin-bottom: 0px !important; +} + +.navpanel summary:hover { + color: var(--primary); + background-color: rgba(0,0,0,0.1); +} +.navpanel ul { + margin-top: 10px; + margin-bottom: 10px; + padding-left: 5px; +} + + +.navpanel li { + font-size: 14px; + margin-left: -5px; + margin-bottom: 0; + cursor: pointer; +} + +.navpanel li:hover { + color: var(--primary); + background-color: rgba(0,0,0,0.1); +} + +li::marker { + content: "- "; + padding-left: -20px; + padding-right: 5px; + } + + +.navconntext { + position: fixed; + font-size: calc(var(--topnavheight) * 0.3); + margin: calc((var(--topnavheight) * 0.25 * 0.75) - 5px); + background-color: rgba(255, 0, 0, 0.2); + border-style: solid; + border-width: 1px !important; + border-color: #ff0000; + padding: 8px; + right:0; + top:0; +} + +.sidenav-button { + width:100%; + margin-bottom: 10px; +} + +.topnav-button { + margin-left: 13px !important; + height: 100% !important; + + font-size: calc( var(--topnavheight) * ( 3 / 9 ) ); + line-height: calc( var(--topnavheight) * ( 3 / 9 ) ); + +} + +.navTitle { + position: fixed; + margin-bottom: 1px; + top: 0; + left: 0; + width: var(--sidenavwidth); + height: var(--topnavheight); + + text-align:center; + line-height: var(--topnavheight); + + text-decoration:none; +} + +.navTitle:hover { + background-color: rgba(0,0,0,0.1); +} + + +.popupBox { + position: fixed; + + /* display: flex; + box-sizing: content-box; + justify-content: flex-end; + flex-direction: column; */ + + width: var(--popupBoxWidth); + height: calc(100% - var(--topnavheight) - 10px); + bottom: 0; + right: 20px; + overflow: auto; +} + +.popupBox dialog { + display: flex; + z-index: inherit; + position: relative; + top: inherit; + right: inherit; + bottom: inherit; + left: inherit; + align-items: inherit; + justify-content: inherit; + width: inherit; + min-width: inherit; + height: auto; + min-height: inherit; + padding: 0; + background-color: inherit; + + left: 0; + margin: 0; + margin-bottom: 20px; + + justify-content:right; + flex-direction: column; +} + +.popupBox article { + margin: 0; + opacity: 0.5; + padding-bottom: 10px; +} + +.popupBox header { + border: 0; + padding-left: 15px; + padding-right: 15px; + padding-top: 10px; + padding-bottom: 10px; + margin-bottom: 10px; +} + +.popupBox article:hover { + opacity: 1; +} + +.popupBox article a { + cursor: pointer; +} + +.popupBox article p { + font-size: 15px; +} + +.noselect { + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Old versions of Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + supported by Chrome, Edge, Opera and Firefox */ + } \ No newline at end of file diff --git a/html/src/utils.js b/html/src/utils.js new file mode 100755 index 0000000..aa2c2fc --- /dev/null +++ b/html/src/utils.js @@ -0,0 +1,242 @@ +export const setCookie = (name, value, hours = 1, path = '/') => { + const expires = new Date(Date.now() + hours * 6e4).toUTCString() + document.cookie = `${name}=${encodeURIComponent(value)}; path=${path}; SameSite=None; secure=True; session=True` +} + +export const getCookie = (name) => { + return document.cookie.split('; ').reduce((r, v) => { + const parts = v.split('=') + return parts[0] === name ? decodeURIComponent(parts[1]) : r + }, '') +} + +export function genID(length = 8){ + // Declare all characters + let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + // Pick characers randomly + let str = ''; + for (let i = 0; i < length; i++) { + str += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return str; +} + + +export function icondisconnect() { + let icon = document.getElementById('connecticon') + icon.style.backgroundColor = "rgba(255, 0, 0, 0.2)" + icon.style.borderColor = "#ff0000" + icon.innerText = "Disconnected" +} + +export function iconunauth() { + let icon = document.getElementById('connecticon') + icon.style.backgroundColor = "rgba(255, 255, 0, 0.2)" + icon.style.borderColor = "#ffff00" + icon.innerText = "Unauthenticated" +} + +export function iconauth() { + let icon = document.getElementById('connecticon') + icon.style.backgroundColor = "rgba(0, 255, 0, 0.2)" + icon.style.borderColor = "#00ff00" + icon.innerText = "Authenticated" +} + +// function addPopup(bgcolor, fgcolor, innerHTML) { +// const elem = document.getElementById('popupBox') +// const id = 'popup-'+genID(16) +// elem.innerHTML = `` + +// elem.innerHTML + +// setTimeout(()=>{ +// elem.parentNode.removeChild(elem) +// }, 30000) + +// } + +function addPopup(bgcolor, isDark, title, content) { + const elem = document.getElementById('popupBox') + let header + let textColor + if(isDark){ + header = 'rgba(255,255,255,0.05)' + textColor = 'text-white' + }else{ + header = 'rgba(0,0,0,0.2)' + textColor = 'text-black' + } + elem.innerHTML += ` + +
+
+

${formatTime(getUnixTime())}

+ + ${title} +
+

${content}

+
+
+ ` +} + +export function popupInfo(title, text){ + addPopup('var(--card-sectionning-background-color)', true, title, text) +} + +export function popupSuccess(title, text){ + addPopup('#005000', true, title, text) +} + +export function popupWarning(title, text){ + addPopup('#393900', true, title, text) +} + +export function popupError(title, text){ + addPopup('#500000', true, title, text) +} + +export function getUnixTime() { + return (+ new Date()) +} + +function formatTime(Millis){ + const date = new Date(Millis) + + if(date.getDate() != (new Date()).getDate()){ + return date.getMonth()+1 + "/" + date.getDate() + "/" + date.getFullYear() + }else{ + var Hour = "" + var Minute = "" + var AmPm = "" + + if(date.getHours() == 0){ + Hour = "12" + AmPm = "AM" + }else if(date.getHours() < 12){ + Hour = date.getHours() + AmPm = "AM" + }else if(date.getHours() == 12){ + Hour = "12" + AmPm = "PM" + }else{ + Hour = date.getHours() - 12 + AmPm = "PM" + } + + if(date.getMinutes() < 10){ + Minute = "0" + date.getMinutes() + }else{ + Minute = date.getMinutes() + } + + return Hour + ":" + Minute + " " + AmPm + + } +} + +export function sha256(ascii) { + function rightRotate(value, amount) { + return (value>>>amount) | (value<<(32 - amount)); + }; + + var mathPow = Math.pow; + var maxWord = mathPow(2, 32); + var lengthProperty = 'length' + var i, j; // Used as a counter across the whole file + var result = '' + + var words = []; + var asciiBitLength = ascii[lengthProperty]*8; + + //* caching results is optional - remove/add slash from front of this line to toggle + // Initial hash value: first 32 bits of the fractional parts of the square roots of the first 8 primes + // (we actually calculate the first 64, but extra values are just ignored) + var hash = sha256.h = sha256.h || []; + // Round constants: first 32 bits of the fractional parts of the cube roots of the first 64 primes + var k = sha256.k = sha256.k || []; + var primeCounter = k[lengthProperty]; + /*/ + var hash = [], k = []; + var primeCounter = 0; + //*/ + + var isComposite = {}; + for (var candidate = 2; primeCounter < 64; candidate++) { + if (!isComposite[candidate]) { + for (i = 0; i < 313; i += candidate) { + isComposite[i] = candidate; + } + hash[primeCounter] = (mathPow(candidate, .5)*maxWord)|0; + k[primeCounter++] = (mathPow(candidate, 1/3)*maxWord)|0; + } + } + + ascii += '\x80' // Append Ƈ' bit (plus zero padding) + while (ascii[lengthProperty]%64 - 56) ascii += '\x00' // More zero padding + for (i = 0; i < ascii[lengthProperty]; i++) { + j = ascii.charCodeAt(i); + if (j>>8) return; // ASCII check: only accept characters in range 0-255 + words[i>>2] |= j << ((3 - i)%4)*8; + } + words[words[lengthProperty]] = ((asciiBitLength/maxWord)|0); + words[words[lengthProperty]] = (asciiBitLength) + + // process each chunk + for (j = 0; j < words[lengthProperty];) { + var w = words.slice(j, j += 16); // The message is expanded into 64 words as part of the iteration + var oldHash = hash; + // This is now the undefinedworking hash", often labelled as variables a...g + // (we have to truncate as well, otherwise extra entries at the end accumulate + hash = hash.slice(0, 8); + + for (i = 0; i < 64; i++) { + var i2 = i + j; + // Expand the message into 64 words + // Used below if + var w15 = w[i - 15], w2 = w[i - 2]; + + // Iterate + var a = hash[0], e = hash[4]; + var temp1 = hash[7] + + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) // S1 + + ((e&hash[5])^((~e)&hash[6])) // ch + + k[i] + // Expand the message schedule if needed + + (w[i] = (i < 16) ? w[i] : ( + w[i - 16] + + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15>>>3)) // s0 + + w[i - 7] + + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2>>>10)) // s1 + )|0 + ); + // This is only used once, so *could* be moved below, but it only saves 4 bytes and makes things unreadble + var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) // S0 + + ((a&hash[1])^(a&hash[2])^(hash[1]&hash[2])); // maj + + hash = [(temp1 + temp2)|0].concat(hash); // We don't bother trimming off the extra ones, they're harmless as long as we're truncating when we do the slice() + hash[4] = (hash[4] + temp1)|0; + } + + for (i = 0; i < 8; i++) { + hash[i] = (hash[i] + oldHash[i])|0; + } + } + + for (i = 0; i < 8; i++) { + for (j = 3; j + 1; j--) { + var b = (hash[i]>>(j*8))&255; + result += ((b < 16) ? 0 : '') + b.toString(16); + } + } + return result.toUpperCase(); +}; diff --git a/main.py b/main.py new file mode 100755 index 0000000..1baf077 --- /dev/null +++ b/main.py @@ -0,0 +1,108 @@ +import src.web as web +import src.utils as utils +import src.modules as modules +from sys import argv + +webserv = web.webserv() +moduleMaster = modules.moduleMaster() + +def main(): + + if not utils.pathExists('data'): + utils.makeDir('data') + + if not utils.pathExists('data/creds.json'): + if input("No credentials file was found, \nwould you like to create one? (Y/n): ").lower() in ["yes", "y", ""]: + utils.writeFile('data/creds.json', utils.genDefaultAccounts()) + + if webserv.secure and ( not utils.pathExists('data/selfsign.crt') or not utils.pathExists('data/selfsign.key') ): + if input("No ssl key/cert was found, \nwould you like to create them? (Y/n): ").lower() in ["yes", "y", ""]: + + if not utils.pathExists('data/selfsign.key'): + utils.genKey(utils.getRoot('data/')) + + if not utils.pathExists('data/selfsign.crt'): + utils.genCert(utils.getRoot('data/')) + + moduleMaster.initModules(webserv) + + webserv.start() + moduleMaster.runModules() + + # for m in modules: + # m.module.main() + +def printHelp(): + print(""" + Modulator usage: + + -h -? --help - Print this help information + -v --verbose - Print verbose information, default: false + -u --unsecure - Use http instead of https, default: false + -p --port - Set port of the webserver, default: 80 or 44 + -h --host - Set host of the webserver, default: 0.0.0.0 + + --defaultTab - Set the default tab on visit, default: 'main' + --title - Set the title of the html pages, default: 'Modulator' + + Examples: + + $ python3 ./main.py + $ python3 ./main.py -vo 127.0.0.1 + $ python3 ./main.py -p 12345 -h 192.168.0.123 + """) + +if __name__ == '__main__': + + i = 1 + + while i < len(argv): + arg = argv[i] + sargs = list(arg) + if sargs[0] == '-' and sargs[1] == '-': + match arg: + case '--help': + printHelp() + exit() + case '--verbose': + webserv.verbose = True + i+=1; continue + case '--unsecure': + webserv.secure = False + if webserv.port == 443: + webserv.port = 80 + i+=1; continue + case '--port': + webserv.port = int(argv[i+1]) + i+=2; continue + case '--host': + webserv.host = str(argv[i+1]) + i+=2; continue + case '--defaultTab': + webserv.defaultTab = str(argv[i+1]) + i+=2; continue + case '--title': + webserv.title = str(argv[i+1]) + i+=2; continue + elif sargs[0] == '-': + for sarg in sargs: + match sarg: + case 'h' | '?': + printHelp() + exit() + case 'v': + webserv.verbose = True + case 'u': + webserv.secure = False + if webserv.port == 443: + webserv.port = 80 + case 'p': + webserv.port = int(argv[i+1]) + i+=1; continue + case 'o': + webserv.host = str(argv[i+1]) + i+=1; continue + + i += 1 + + main() diff --git a/modules/main/Dashboard-copy.html b/modules/main/Dashboard-copy.html new file mode 100644 index 0000000..caa5726 --- /dev/null +++ b/modules/main/Dashboard-copy.html @@ -0,0 +1 @@ +

Test123123!

\ No newline at end of file diff --git a/modules/main/Dashboard.html b/modules/main/Dashboard.html new file mode 100644 index 0000000..1f00026 --- /dev/null +++ b/modules/main/Dashboard.html @@ -0,0 +1,11 @@ +

Test!

+ + \ No newline at end of file diff --git a/modules/main/main.py b/modules/main/main.py new file mode 100644 index 0000000..d6fcf34 --- /dev/null +++ b/modules/main/main.py @@ -0,0 +1,10 @@ +from modules.main import test as test + +def test1(ac, data): + print(ac) + print(data) + +def main(mm): + # mm.addAuthEventListener('test1', test1) + print(mm.rawServer.addEventListener('test1', test1)) + #mm.rawServer.addEventListener('login', test1) \ No newline at end of file diff --git a/modules/main/module.json b/modules/main/module.json new file mode 100644 index 0000000..b6f540c --- /dev/null +++ b/modules/main/module.json @@ -0,0 +1,41 @@ +{ + "name": "main", + "creators": ["ASTATIN3"], + "version": "1.0", + "entrypoint": "modules/main/main.py", + "tabs": [ + { + "name": "main", + "defaultPage": "dashboard", + "pages": [ + { + "type": "page", + "name": "dashboard", + "location": "modules/main/Dashboard.html" + }, + { + "type": "folder", + "name": "folder 1", + "pages": [ + { + "type": "page", + "name": "dashboardcopy", + "location": "modules/main/Dashboard-copy.html" + }, + { + "type": "folder", + "name": "folder 2", + "pages": [ + { + "type": "page", + "name": "dashboardcopy", + "location": "modules/main/Dashboard-copy.html" + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/main/test.py b/modules/main/test.py new file mode 100644 index 0000000..bdcaa13 --- /dev/null +++ b/modules/main/test.py @@ -0,0 +1,2 @@ +def test(): + return "testsjdhgkjrhsgkhjertestsjdhgkjrhsgkhjertestsjdhgkjrhsgkhjer" \ No newline at end of file diff --git a/src/auth.py b/src/auth.py new file mode 100755 index 0000000..79fbea8 --- /dev/null +++ b/src/auth.py @@ -0,0 +1,142 @@ +import os +import base64 +import json +from hashlib import sha256 +from flask import Flask, render_template, Response +from flask import request, redirect, url_for, make_response + +import src.packets as packets +import src.utils as utils + +class authUser: + def __init__(self): + self.username = None + self.permGroups = [] + self.sha256passwordhash = None + +class authClient: + def __init__(self): + self.username = None + self.session = utils.randID(32) + self.userData = None + + self.timeout = utils.getUnixTime() + (60 * 60 * 1000) + self.loginTime = utils.getUnixTime() + self.lastReauth = utils.getUnixTime() + + self.rawClient = None + def send(type, data): + self.rawClient.send(type, data) + +class authServer: + def __init__(self): + self.rawServer = None + self.app = None + self.clients = [] + self.users = [] + + def login(self, c, data): + if c.clientid != data['cid']: + c.send('error', 'invalidLoginRequest') + return + + if int(data['data']['salt']) > (utils.getUnixTime() + 5000): + c.send('error', 'invalidLoginRequest') + return + + logins = json.loads(utils.readFile(utils.getRoot('data/')+'creds.json')) + isValid = False + validAcc = None + for acc in self.users: + hash = utils.hash(str(acc.username)+str(acc.sha256passwordhash)+str(data['data']['salt'])) + if hash == str(data['data']['data']): + isValid = True + validAcc = acc + break + + if isValid: + if utils.getatribinarr(self.clients, 'rawClient', c): + c.send('error', 'prelogin') + return + + ac = authClient() + ac.username = validAcc.username + ac.userData = validAcc + ac.rawClient = c + + self.clients.append(ac) + + c.send('loginSuccess', { + 'username': ac.username, + 'session': ac.session, + 'redir': f'/{self.app.defaultTab}/{self.app.defaultPage}', + 'timeout': ac.timeout + }) + + return + else: + c.send('error', 'invalidLogin') + return + + def reauth(self, c, data): + session = data['data']['session'] + ac = utils.getatribinarr(self.clients, 'session', session) + + if ac == None: + c.send('error', 'invalidLoginRequest') + return + if ac.rawClient.address != request.remote_addr: + c.send('error', 'invalidLoginRequest') + return + if utils.getUnixTime() > ac.timeout: + c.send('error', 'invalidLoginRequest') + return + + ac.rawClient = c + ac.lastReauth = utils.getUnixTime() + + c.send('reauth', { + 'username': ac.username + }) + + def cookieLogin(self, request): + session = request.cookies.get('session') + if session == None: + return None + + ac = utils.getatribinarr(self.clients, 'session', session) + + if ac == None: + return False + if ac.rawClient.address != request.remote_addr: + return False + if utils.getUnixTime() > ac.timeout: + return False + + return True + + def initRawServer(self): + self.app.rawServer.addEventListener('login', self.login) + self.app.rawServer.addEventListener('reauth', self.reauth) + + def reloadUsers(self): + data = json.loads(utils.readFile(utils.getRoot('data/')+'creds.json')) + + self.users = [] + + for acc in data: + user = authUser() + user.username = acc['username'] + user.sha256passwordhash = acc['sha256passwordhash'] + user.permGroups = acc['permGroups'] + self.users.append(user) + +def startAuthListener(app): + global authServer + authServer = authServer() + authServer.app = app + authServer.reloadUsers() + packets.startRawListener(app) + authServer.initRawServer() + + return authServer diff --git a/src/jsonpack.py b/src/jsonpack.py new file mode 100755 index 0000000..4840b57 --- /dev/null +++ b/src/jsonpack.py @@ -0,0 +1,273 @@ +# Copyright (c) 2013, Rodrigo González, Sapienlab All Rights Reserved. +# Available via MIT LICENSE. See https://github.com/roro89/jsonpack/blob/master/LICENSE.md for details. + +TOKEN_TRUE = -1 +TOKEN_FALSE = -2 +TOKEN_NULL = -3 +TOKEN_EMPTY_STRING = -4 +TOKEN_UNDEFINED = -5 + +def pack(json): + json = json if isinstance(json, str) else json + dictionary = { + 'strings': [], + 'integers': [], + 'floats': [] + } + + def recursiveAstBuilder(item): + if item is None: + return { + 'type': 'null', + 'index': TOKEN_NULL + } + if item == '': + return { + 'type': '', + 'index': TOKEN_UNDEFINED + } + if isinstance(item, list): + ast = ['@'] + for i in item: + ast.append(recursiveAstBuilder(i)) + return ast + if isinstance(item, dict): + ast = ['$'] + for key, value in item.items(): + ast.append(recursiveAstBuilder(key)) + ast.append(recursiveAstBuilder(value)) + return ast + if item == '': + return { + 'type': 'empty', + 'index': TOKEN_EMPTY_STRING + } + if type(item) == type(True): + return { + 'type': 'boolean', + 'index': TOKEN_TRUE if item else TOKEN_FALSE + } + if isinstance(item, str): + index = dictionary['strings'].index(item) if item in dictionary['strings'] else -1 + if index == -1: + dictionary['strings'].append((item)) + index = len(dictionary['strings']) - 1 + return { + 'type': 'strings', + 'index': index + } + if isinstance(item, int): + index = dictionary['integers'].index(item) if item in dictionary['integers'] else -1 + if index == -1: + dictionary['integers'].append(_base10To36(item)) + index = len(dictionary['integers']) - 1 + return { + 'type': 'integers', + 'index': index + } + if isinstance(item, float): + index = dictionary['floats'].index(item) if item in dictionary['floats'] else -1 + if index == -1: + dictionary['floats'].append(item) + index = len(dictionary['floats']) - 1 + return { + 'type': 'floats', + 'index': index + } + raise TypeError('Unexpected argument of type ' + str(type(item))) + + def recursiveParser(item): + if isinstance(item, list): + packed = item[0] + for i in item[1:]: + packed += recursiveParser(i) + '|' + return (packed[:-1] if packed[-1] == '|' else packed) + ']' + type = item['type'] + index = item['index'] + if type == 'strings': + return _base10To36(index) + if type == 'integers': + return _base10To36(stringLength + index) + if type == 'floats': + return _base10To36(stringLength + integerLength + index) + if type == 'boolean': + return str(index) + if type == 'null': + return str(TOKEN_NULL) + if type == '': + return str(TOKEN_UNDEFINED) + if type == 'empty': + return str(TOKEN_EMPTY_STRING) + raise TypeError('The item is alien!') + + ast = recursiveAstBuilder(json) + + stringLength = len(dictionary['strings']) + integerLength = len(dictionary['integers']) + floatLength = len(dictionary['floats']) + packed = '|'.join(dictionary['strings']) + packed += '^' + '|'.join(dictionary['integers']) + dictionary['floats'] = [str(n) for n in dictionary['floats']] + packed += '^' + '|'.join(dictionary['floats']) + packed += '^' + recursiveParser(ast) + + return packed + +def unpack(packed): + rawBuffers = packed.split('^') + dictionary = [] + buffer = rawBuffers[0] + if buffer != '': + buffer = buffer.split('|') + for i in buffer: + dictionary.append(_decode(i)) + buffer = rawBuffers[1] + if buffer != '': + buffer = buffer.split('|') + for i in buffer: + dictionary.append(_base36To10(i)) + buffer = rawBuffers[2] + if buffer != '': + buffer = buffer.split('|') + for i in buffer: + dictionary.append(float(i)) + tokens = [] + number36 = '' + for i in rawBuffers[3]: + if i in ['|', '$', '@', ']']: + if number36: + tokens.append(_base36To10(number36)) + number36 = '' + if i != '|': + tokens.append(i) + else: + number36 += i + tokensLength = len(tokens) + tokensIndex = 0 + + def recursiveUnpackerParser(): + nonlocal tokensIndex + # Maybe '$' (object) or '@' (array) + type = tokens[tokensIndex] + tokensIndex += 1 + # Parse an array + if type == '@': + node = [] + while tokensIndex < tokensLength: + value = tokens[tokensIndex] + if value == ']': + return node + if value == '@' or value == '$': + node.append(recursiveUnpackerParser()) + else: + if value == TOKEN_TRUE: + node.append(True) + elif value == TOKEN_FALSE: + node.append(False) + elif value == TOKEN_NULL: + node.append(None) + elif value == TOKEN_UNDEFINED: + node.append() + elif value == TOKEN_EMPTY_STRING: + node.append('') + else: + node.append(dictionary[value]) + tokensIndex += 1 + return node + # Parse an object + if type == '$': + node = {} + while tokensIndex < tokensLength: + key = tokens[tokensIndex] + if key == ']': + return node + if key == TOKEN_EMPTY_STRING: + key = '' + else: + key = dictionary[key] + tokensIndex += 1 + value = tokens[tokensIndex] + if value == '@' or value == '$': + node[key] = recursiveUnpackerParser() + else: + if value == TOKEN_TRUE: + node[key] = True + elif value == TOKEN_FALSE: + node[key] = False + elif value == TOKEN_NULL: + node[key] = None + elif value == TOKEN_UNDEFINED: + node[key] = None + elif value == TOKEN_EMPTY_STRING: + node[key] = '' + else: + node[key] = dictionary[value] + tokensIndex += 1 + return node + raise TypeError('Bad token ' + str(type) + ' isn\'t a type') + + return recursiveUnpackerParser() + +def _indexOfDictionary(dictionary, value): + if isinstance(value, bool): + return TOKEN_TRUE if value else TOKEN_FALSE + if value is None: + return TOKEN_NULL + if value == '': + return TOKEN_UNDEFINED + if value == '': + return TOKEN_EMPTY_STRING + if isinstance(value, str): + value = _encode(value) + index = dictionary['strings'].index(value) if value in dictionary['strings'] else -1 + if index == -1: + dictionary['strings'].append(value) + index = len(dictionary['strings']) - 1 + if type(value) not in [str, int]: + raise Error('The type is not a JSON type') + if isinstance(value, str): + value = _encode(value) + elif isinstance(value, int) and value % 1 == 0: + value = _base10To36(value) + value = _encode(value) if isinstance(value, str) else _base10To36(value) + index = dictionary[type(value)].index(value) if value in dictionary[type(value)] else -1 + if index == -1: + dictionary[type(value)].append(value) + index = len(dictionary[type(value)]) - 1 + return '+' + str(index) if type(value) == 'number' else index + +def _encode(string): + if not isinstance(string, str): + return string + return string.replace(' ', '+').replace('+', '%2B').replace('|', '%7C').replace('^', '%5E').replace('%', '%25') + +def _decode(string): + if not isinstance(string, str): + return string + return string.replace('+', ' ').replace('%2B', '+').replace('%7C', '|').replace('%5E', '^').replace('%25', '%') + +def _base10To36(number): + if not isinstance(number, (int, float)): + raise TypeError('number must be an integer') + is_negative = number < 0 + number = abs(number) + + alphabet, base36 = ['0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ', ''] + + while number: + number, i = divmod(number, 36) + base36 = alphabet[i] + base36 + if is_negative: + base36 = '-' + base36 + + return base36 or alphabet[0] + + +def _base36To10(number): + return int(number, 36) + +def _indexOf(array, obj, start=0): + for i in range(start, len(array)): + if array[i] == obj: + return i + return -1 \ No newline at end of file diff --git a/src/modules.py b/src/modules.py new file mode 100644 index 0000000..7c25549 --- /dev/null +++ b/src/modules.py @@ -0,0 +1,111 @@ +import json +import importlib +import sys + +import src.web as web +import src.utils as utils + +class module(): + def __init__(self): + self.name = None + self.module = None + self.rootdir = None + self.tabs = [] + + def initSelf(self): + spec = importlib.util.spec_from_file_location(self.name, utils.getRoot(self.entrypoint)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + self.module = module + + def run(self, moduleMaster): + self.module.main(moduleMaster) + +class moduleMaster(): + def __init__(self): + self.modules = [] + self.webserv = None + + self.app = None + self.rawServer = None + self.authServer = None + + def initModules(self, webserv): + self.webserv = webserv + + mdirs = utils.listSubdirs(utils.getRoot('modules/')) + + for mname in mdirs: + mjson = json.loads(open(utils.getRoot(f'modules/{mname}/module.json')).read()) + m = module() + m.name = mjson['name'] + m.entrypoint = mjson['entrypoint'] + + for tab in mjson['tabs']: + mtab = utils.getatribinarr(self.webserv.webtabs, 'name', tab['name']) + + if mtab == None: + mtab = web.webtab() + mtab.name = tab['name'] + m.tabs.append(mtab) + + def recursiveAdder(objs): + tmpPages = [] + for obj in objs: + if obj['type'] == 'folder': + folder = web.webpagefolder() + folder.name = obj['name'] + tmpTmpPages = recursiveAdder(obj['pages']) + for tmpobj in tmpTmpPages: + folder.pages.append(tmpobj) + tmpPages.append(folder) + else: + mpage = web.webpage() + mpage.name = obj['name'] + mpage.location = obj['location'] + tmpPages.append(mpage) + return tmpPages + + tmpPages = recursiveAdder(tab['pages']) + + for obj in tmpPages: + mtab.pages.append(obj) + + mtab.defaultPage = tab['defaultPage'] + + self.webserv.webtabs.append(mtab) + + m.initSelf() + self.modules.append(m) + + for tab in webserv.webtabs: + tab.addHtml() + + def runModules(self): + self.app = self.webserv.app + self.rawServer = self.app.rawServer + self.authServer = self.app.authServer + + for module in self.modules: + module.run(self) + + def getRawClients(self): + return self.rawServer.clients + + def addRawEventListener(self, eventName, func): + self.rawServer.addEventListener(eventName, func) + + def getAuthClients(self): + return self.authServer.clients + + def addAuthEventListener(self, eventName, func): + def tmpfunc(self, c, data): + if not c in self.rawServer.clients: + return + ac = utils.getatribinarr(self.authServer.clients, 'rawClient', c) + if ac == None: + return + + func(ac, data) + + self.rawServer.addEventListener(eventName, tmpfunc) \ No newline at end of file diff --git a/src/packets.py b/src/packets.py new file mode 100755 index 0000000..66d8279 --- /dev/null +++ b/src/packets.py @@ -0,0 +1,112 @@ +import os +import base64 +from hashlib import sha256 +from flask import Flask, render_template, Response +from flask import request, redirect, url_for, make_response + +import src.jsonpack as jsonpack +import src.utils as utils + +import queue + +class rawClient: + def __init__(self, app): + self.clientid = utils.randID(32) + self.messages = queue.Queue() + self.app = app + self.address = None + def send(self, type, msg): + self.app.rawServer.sendClient(self, jsonpack.pack({ + 'type': type, + 'data': msg, + 'cid': self.clientid + })) + +#Credit to https://github.com/MaxHalford/flask-sse-no-deps +class rawServer: + + def __init__(self, app): + self.eventListeners = [] + self.clients = [] + self.app = app + app.rawServer = self.app + + def listen(self): + c = rawClient(self.app) + self.clients.append(c) + return self.clients[-1] + + def broadcast(self, msg): + # We go in reverse order because we might have to delete an element, which will shift the + # indices backward + ssedata = format_sse(msg) + for i in reversed(range(len(clients))): + try: + self.clients[i].messages.put_nowait(ssedata) + except queue.Full: + del self.clients[i] + + def sendClient(self, c, msg): + if c not in self.clients: + return + ssedata = format_sse(msg) + c.messages.put_nowait(ssedata) + + def clientByCID(self, cid): + for c in self.clients: + if c.clientid == cid: + return c + return None + + def addEventListener(self, eventName, func): + self.eventListeners.append({ + 'name': eventName, + 'func': func + }) + + +def format_sse(data: str, event=None) -> str: + #Formats a string and an event name in order to follow the event stream convention. + msg = f'data: {data}\n\n' + if event is not None: + msg = f'event: {event}\n{msg}' + return msg + + +def startRawListener(app): + app.rawServer = rawServer(app) + + @app.route('/listen', methods=['GET', 'POST']) + def listen(): + + if request.method == 'GET': + + c = app.rawServer.listen() # returns a queue.Queue + c.address = request.remote_addr + c.send('clidata', { + 'cid': c.clientid + }) + + def stream(): + while True: + msg = c.messages.get() # blocks until a new message arrives + yield msg + + return Response(stream(), mimetype='text/event-stream') + if request.method == 'POST': + + data = jsonpack.unpack(request.data.decode("utf-8")) + + if data['cid'] == None: + return {}, 400 + c = utils.getatribinarr(app.rawServer.clients, 'clientid', data['cid']) + if c == None: + return {}, 400 + + for event in app.rawServer.eventListeners: + if event['name'] == data['type']: + event['func'](c, data) + + return {}, 200 + + #return rawServer \ No newline at end of file diff --git a/src/utils.py b/src/utils.py new file mode 100755 index 0000000..5350188 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,124 @@ +import os +import json +import string +import random +from hashlib import sha256 +from datetime import datetime + +from sys import platform + +import subprocess + +def getRoot(path): + rootdir = os.getcwd() + f'/{path}' + + if platform in ['nt', 'win32', 'win64']: + rootdir = rootdir.split(':')[1].replace('\\', '/') + + return rootdir + +def pathExists(path): + return os.path.exists(path) + +def makeDir(path): + if not os.path.exists(path): + os.makedirs(path) + +def listSubdirs(folder): + if os.path.exists(folder): + return os.listdir(folder) + else: + return [] + +def writeFile(path, data): + if not os.path.exists(path): + with open(path, mode='a'): pass + with open(path, 'w') as f: + f.write(data) + +def readFile(path): + if not os.path.exists(path): + return '' + try: + with open(path) as f: + return f.read() + except: + return '' + +def delFile(path): + if os.path.exists(path): + os.remove(path) + +def getUnixTime(): + return round((datetime.utcnow() - datetime(1970, 1, 1)).total_seconds() * 1000) + +def hash(data): + return sha256(data.encode('utf-8')).hexdigest().upper() + +def getatribinarr(arr, atribname, atrib): + for i in range(0,len(arr),1): + if getattr(arr[i], atribname) == atrib: + return arr[i] + return None + +def randID(length): + letters = string.hexdigits + return ''.join(random.choice(letters) for i in range(length)) + +def genDefaultAccounts(): + userPassword = randID(16) + adminPassword = randID(16) + + print('#################################################') + print('New Credentials - THESE ONLY WILL BE PRINTED ONCE') + print('########') + print('Username: User') + print(f'Password: {userPassword}') + print('########') + print('Username: Admin') + print(f'Password: {adminPassword}') + print('#################################################') + + return json.dumps( + [ + { + 'username': 'User', + 'permGroups': [ + 'Users' + ], + 'sha256passwordhash': hash(userPassword) + }, + { + 'username': 'Admin', + 'permGroups': [ + 'Users', + 'Admins' + ], + 'sha256passwordhash': hash(adminPassword) + } + ], sort_keys=True, indent=2) + +def genKey(path): + subprocess.run(['openssl', 'genrsa', '-out', f'{path}selfsign.key', '2048']) + +def genCert(path): + writeFile(f'{path}/selfsign.cnf', +"""[req] +default_bits = 2048 +prompt = no +default_md = sha256 +encrypt_key = no +distinguished_name = dn + +[dn] +C = ID +O = Local Digital Cert Authority +OU = www.ca.local +CN = Self-Signed Root CA +""") + + subprocess.run(['openssl', 'req', '-new', '-key', f'{path}selfsign.key', '-out', f'{path}selfsign.csr', '-config', f'{path}selfsign.cnf']) + subprocess.run(['openssl', 'x509', '-req', '-days', '36500', '-in', f'{path}selfsign.csr', '-signkey', f'{path}selfsign.key', '-out', f'{path}selfsign.crt']) + + delFile(f'{path}selfsign.cnf') + delFile(f'{path}selfsign.csr') \ No newline at end of file diff --git a/src/web.py b/src/web.py new file mode 100755 index 0000000..84d9edc --- /dev/null +++ b/src/web.py @@ -0,0 +1,172 @@ +import os +import src.utils as utils +import multiprocessing as mupr + +from flask import Flask, render_template, Response +from flask import request, redirect, url_for, make_response + +import src.jsonpack as jsonpack +import src.auth as auth + +webroot = utils.getRoot('html/') + +app = Flask(__name__, + static_url_path=webroot, + static_folder=webroot, + template_folder=webroot) + +class webtab(): + def __init__(self): + self.name = None + self.pages = [] + self.defaultPage = '' + self.html = '' + + def recursiveAdder(self, objs): + html = '' + for obj in objs: + if isinstance(obj, webpagefolder): + html += '
' +\ + obj.name +\ + '
    \n' +\ + self.recursiveAdder(obj.pages) +\ + '
\n' + else: + html += f'
  • ' +\ + obj.name +\ + '
  • \n' + return html + + def addHtml(self): + self.html = self.recursiveAdder(self.pages) + + def addPage(self, page): + self.pages.append(page) + +class webpagefolder(): + def __init__(self): + self.name = None + self.pages = [] + +class webpage(): + def __init__(self): + self.name = None + self.location = None + +@app.route('/') +def index(): + isValid = app.authServer.cookieLogin(request) + if not isValid: + return redirect("/login", code=302) + else: + return redirect(f'/{app.defaultTab}/{app.defaultPage}', code=302) + +@app.route('/login') +def loginPage(): + isValid = app.authServer.cookieLogin(request) + if isValid: + return redirect(f'/{app.defaultTab}/{app.defaultPage}', code=302) + + return make_response(open(f'{webroot}nav.html', 'r').read() + .replace('', open(f'{webroot}login.html', 'r').read()) + .replace('', 'Login') + .replace('', app.title) + .replace('', '/login')) + +def recursivePageLocationFinder(pagename, objs): + returnVal = None + for obj in objs: + if isinstance(obj, webpagefolder): + tmp = recursivePageLocationFinder(pagename, obj.pages) + if tmp != None: + returnVal = tmp + else: + if obj.name == pagename: + returnVal = obj.location + return returnVal + +@app.route('//') +def page(tabname, pagename): + + isValid = app.authServer.cookieLogin(request) + if not isValid: + return redirect("/login", code=302) + + try: + + tab = utils.getatribinarr(app.webtabs, 'name', tabname) + pageloc = recursivePageLocationFinder(pagename, tab.pages) + + return make_response(open(utils.getRoot('html/nav.html'), 'r').read() + .replace('', open(utils.getRoot(pageloc), 'r').read()) + .replace('', app.tabHtml) + .replace('', tab.html) + .replace('', app.title) + .replace('', f'/{app.defaultTab}/{app.defaultPage}')) + except: + return redirect("/login", code=302) + +@app.route('/src/') +def src(file): + return app.send_static_file(f'src/{file}') + +@app.errorhandler(404) +def err404(err): + return redirect("/", code=302) + +class webserv(): + def __init__(self): + self.title = 'Modulator' + self.port = 443 + self.host = '0.0.0.0' + self.verbose = False + self.secure = True + self.tabHtml = '' + self.webtabs = [] + self.defaultTab = 'main' + self.defaultPage = '' + + self.app = None + + def start(self): + if not self.verbose: + import logging + log = logging.getLogger('werkzeug') + log.setLevel(logging.ERROR) + + if self.secure: + dataroot = utils.getRoot("data/") + sslcontext = (f'{dataroot}selfsign.crt', f'{dataroot}selfsign.key') + else: + sslcontext = None + + self.proc = mupr.Process(target=app.run, kwargs=dict(debug=self.verbose, port=self.port, host=self.host, ssl_context=sslcontext)) + + def tabHtml(path, name): + return f'{name}' + + for tab in self.webtabs: + self.tabHtml += tabHtml(f'/{tab.name}/{tab.defaultPage}', tab.name) + if tab.name == self.defaultTab: + self.defaultPage = tab.defaultPage + + app.authServer = auth.startAuthListener(app) + + app.defaultTab = self.defaultTab + app.defaultPage = self.defaultPage + app.webtabs = self.webtabs + app.tabHtml = self.tabHtml + + app.title = self.title + self.app = app + self.proc.start() + + def stop(self): + self.proc.terminate() + + # def sendfatal(self, err): + # self.rawServer.broadcast(jsonpack.pack({ + # 'type': 'error', + # 'severity': 'fatal', + # 'error': err + # })) \ No newline at end of file