From a13bd8caed55615a957c3d9e78156c4e3aac8ceb Mon Sep 17 00:00:00 2001 From: greg Date: Fri, 8 Dec 2023 10:01:20 +0100 Subject: [PATCH] push for test --- DEPENDENCIES.md | 41 + Dockerfile | 79 + LICENSE.md | 660 + README.md | 26 + Screenshot.png | Bin 0 -> 44438 bytes application/application.go | 192 + application/command/commander.go | 68 + application/command/commands.go | 128 + application/command/fsm.go | 176 + application/command/handler.go | 369 + application/command/handler_echo_test.go | 104 + application/command/handler_stream_test.go | 262 + application/command/handler_test.go | 18 + application/command/header.go | 143 + application/command/streams.go | 447 + application/command/streams_test.go | 97 + application/commands/address.go | 275 + application/commands/address_test.go | 138 + application/commands/commands.go | 30 + application/commands/integer.go | 120 + application/commands/integer_test.go | 124 + application/commands/ssh.go | 783 + application/commands/string.go | 103 + application/commands/string_test.go | 83 + application/commands/telnet.go | 229 + application/configuration/common.go | 26 + application/configuration/config.go | 253 + application/configuration/loader.go | 28 + application/configuration/loader_direct.go | 34 + application/configuration/loader_enviro.go | 139 + application/configuration/loader_file.go | 270 + application/configuration/loader_redundant.go | 52 + application/configuration/string.go | 79 + application/configuration/string_test.go | 94 + application/controller/base.go | 132 + application/controller/common.go | 61 + application/controller/common_test.go | 54 + application/controller/controller.go | 172 + application/controller/error.go | 44 + application/controller/failure.go | 33 + application/controller/home.go | 33 + application/controller/socket.go | 393 + application/controller/socket_verify.go | 180 + application/controller/static.go | 125 + .../controller/static_page_generater/main.go | 414 + application/log/ditch.go | 48 + application/log/log.go | 28 + application/log/writer.go | 80 + application/log/writer_nodebug.go | 54 + application/network/conn.go | 26 + application/network/conn_timeout.go | 221 + application/network/dial.go | 38 + application/network/dial_ac.go | 60 + application/network/dial_socks5.go | 94 + application/plate.go | 36 + application/rw/fetch.go | 146 + application/rw/fetch_test.go | 59 + application/rw/limited.go | 130 + application/rw/rw.go | 41 + application/server/conn.go | 85 + application/server/server.go | 180 + babel.config.js | 25 + docker-compose.yml | 73 + go.mod | 28 + go.sum | 170 + package-lock.json | 12230 ++++++++++++++++ package.json | 68 + sshwifty.conf.example.json | 55 + sshwifty.go | 54 + traefik-forward-auth.env | 8 + ui/app.css | 73 + ui/app.js | 459 + ui/auth.vue | 126 + ui/commands/address.js | 230 + ui/commands/address_test.js | 102 + ui/commands/color.js | 107 + ui/commands/commands.js | 880 ++ ui/commands/common.js | 409 + ui/commands/common_test.js | 246 + ui/commands/controls.js | 60 + ui/commands/events.js | 106 + ui/commands/exception.js | 28 + ui/commands/history.js | 311 + ui/commands/integer.js | 90 + ui/commands/integer_test.js | 60 + ui/commands/presets.js | 327 + ui/commands/ssh.js | 1124 ++ ui/commands/string.js | 72 + ui/commands/string_test.js | 265 + ui/commands/telnet.js | 648 + ui/common.css | 694 + ui/control/ssh.js | 152 + ui/control/telnet.js | 513 + ui/crypto.js | 118 + ui/error.html | 37 + ui/history.js | 54 + ui/home.css | 422 + ui/home.vue | 616 + ui/home_historyctl.js | 58 + ui/home_socketctl.js | 223 + ui/index.html | 62 + ui/landing.css | 132 + ui/loading.vue | 45 + ui/robots.txt | 4 + ui/socket.js | 397 + ui/sshwifty.svg | 1 + ui/stream/common.js | 109 + ui/stream/common_test.js | 41 + ui/stream/exception.js | 31 + ui/stream/header.js | 264 + ui/stream/header_test.js | 56 + ui/stream/reader.js | 587 + ui/stream/reader_test.js | 197 + ui/stream/sender.js | 225 + ui/stream/sender_test.js | 121 + ui/stream/stream.js | 363 + ui/stream/streams.js | 436 + ui/stream/streams_test.js | 20 + ui/stream/subscribe.js | 129 + ui/widgets/busy.svg | 7 + ui/widgets/chart.vue | 316 + ui/widgets/connect.css | 120 + ui/widgets/connect.vue | 190 + ui/widgets/connect_known.css | 245 + ui/widgets/connect_known.vue | 352 + ui/widgets/connect_new.css | 58 + ui/widgets/connect_new.vue | 53 + ui/widgets/connect_switch.css | 56 + ui/widgets/connect_switch.vue | 52 + ui/widgets/connecting.svg | 132 + ui/widgets/connector.css | 124 + ui/widgets/connector.vue | 834 ++ ui/widgets/connector_field_builder.js | 222 + ui/widgets/screen_console.css | 246 + ui/widgets/screen_console.vue | 578 + ui/widgets/screen_console_keys.js | 677 + ui/widgets/screens.css | 75 + ui/widgets/screens.vue | 107 + ui/widgets/status.css | 221 + ui/widgets/status.vue | 251 + ui/widgets/tab_list.vue | 116 + ui/widgets/tab_window.css | 148 + ui/widgets/tab_window.vue | 80 + ui/widgets/tabs.vue | 77 + ui/widgets/window.css | 20 + ui/widgets/window.vue | 77 + ui/xhr.js | 54 + webpack.config.js | 456 + 148 files changed, 39122 insertions(+) create mode 100644 DEPENDENCIES.md create mode 100644 Dockerfile create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 Screenshot.png create mode 100644 application/application.go create mode 100644 application/command/commander.go create mode 100644 application/command/commands.go create mode 100644 application/command/fsm.go create mode 100644 application/command/handler.go create mode 100644 application/command/handler_echo_test.go create mode 100644 application/command/handler_stream_test.go create mode 100644 application/command/handler_test.go create mode 100644 application/command/header.go create mode 100644 application/command/streams.go create mode 100644 application/command/streams_test.go create mode 100644 application/commands/address.go create mode 100644 application/commands/address_test.go create mode 100644 application/commands/commands.go create mode 100644 application/commands/integer.go create mode 100644 application/commands/integer_test.go create mode 100644 application/commands/ssh.go create mode 100644 application/commands/string.go create mode 100644 application/commands/string_test.go create mode 100644 application/commands/telnet.go create mode 100644 application/configuration/common.go create mode 100644 application/configuration/config.go create mode 100644 application/configuration/loader.go create mode 100644 application/configuration/loader_direct.go create mode 100644 application/configuration/loader_enviro.go create mode 100644 application/configuration/loader_file.go create mode 100644 application/configuration/loader_redundant.go create mode 100644 application/configuration/string.go create mode 100644 application/configuration/string_test.go create mode 100644 application/controller/base.go create mode 100644 application/controller/common.go create mode 100644 application/controller/common_test.go create mode 100644 application/controller/controller.go create mode 100644 application/controller/error.go create mode 100644 application/controller/failure.go create mode 100644 application/controller/home.go create mode 100644 application/controller/socket.go create mode 100644 application/controller/socket_verify.go create mode 100644 application/controller/static.go create mode 100644 application/controller/static_page_generater/main.go create mode 100644 application/log/ditch.go create mode 100644 application/log/log.go create mode 100644 application/log/writer.go create mode 100644 application/log/writer_nodebug.go create mode 100644 application/network/conn.go create mode 100644 application/network/conn_timeout.go create mode 100644 application/network/dial.go create mode 100644 application/network/dial_ac.go create mode 100644 application/network/dial_socks5.go create mode 100644 application/plate.go create mode 100644 application/rw/fetch.go create mode 100644 application/rw/fetch_test.go create mode 100644 application/rw/limited.go create mode 100644 application/rw/rw.go create mode 100644 application/server/conn.go create mode 100644 application/server/server.go create mode 100644 babel.config.js create mode 100644 docker-compose.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 sshwifty.conf.example.json create mode 100644 sshwifty.go create mode 100644 traefik-forward-auth.env create mode 100644 ui/app.css create mode 100644 ui/app.js create mode 100644 ui/auth.vue create mode 100644 ui/commands/address.js create mode 100644 ui/commands/address_test.js create mode 100644 ui/commands/color.js create mode 100644 ui/commands/commands.js create mode 100644 ui/commands/common.js create mode 100644 ui/commands/common_test.js create mode 100644 ui/commands/controls.js create mode 100644 ui/commands/events.js create mode 100644 ui/commands/exception.js create mode 100644 ui/commands/history.js create mode 100644 ui/commands/integer.js create mode 100644 ui/commands/integer_test.js create mode 100644 ui/commands/presets.js create mode 100644 ui/commands/ssh.js create mode 100644 ui/commands/string.js create mode 100644 ui/commands/string_test.js create mode 100644 ui/commands/telnet.js create mode 100644 ui/common.css create mode 100644 ui/control/ssh.js create mode 100644 ui/control/telnet.js create mode 100644 ui/crypto.js create mode 100644 ui/error.html create mode 100644 ui/history.js create mode 100644 ui/home.css create mode 100644 ui/home.vue create mode 100644 ui/home_historyctl.js create mode 100644 ui/home_socketctl.js create mode 100644 ui/index.html create mode 100644 ui/landing.css create mode 100644 ui/loading.vue create mode 100644 ui/robots.txt create mode 100644 ui/socket.js create mode 100644 ui/sshwifty.svg create mode 100644 ui/stream/common.js create mode 100644 ui/stream/common_test.js create mode 100644 ui/stream/exception.js create mode 100644 ui/stream/header.js create mode 100644 ui/stream/header_test.js create mode 100644 ui/stream/reader.js create mode 100644 ui/stream/reader_test.js create mode 100644 ui/stream/sender.js create mode 100644 ui/stream/sender_test.js create mode 100644 ui/stream/stream.js create mode 100644 ui/stream/streams.js create mode 100644 ui/stream/streams_test.js create mode 100644 ui/stream/subscribe.js create mode 100644 ui/widgets/busy.svg create mode 100644 ui/widgets/chart.vue create mode 100644 ui/widgets/connect.css create mode 100644 ui/widgets/connect.vue create mode 100644 ui/widgets/connect_known.css create mode 100644 ui/widgets/connect_known.vue create mode 100644 ui/widgets/connect_new.css create mode 100644 ui/widgets/connect_new.vue create mode 100644 ui/widgets/connect_switch.css create mode 100644 ui/widgets/connect_switch.vue create mode 100644 ui/widgets/connecting.svg create mode 100644 ui/widgets/connector.css create mode 100644 ui/widgets/connector.vue create mode 100644 ui/widgets/connector_field_builder.js create mode 100644 ui/widgets/screen_console.css create mode 100644 ui/widgets/screen_console.vue create mode 100644 ui/widgets/screen_console_keys.js create mode 100644 ui/widgets/screens.css create mode 100644 ui/widgets/screens.vue create mode 100644 ui/widgets/status.css create mode 100644 ui/widgets/status.vue create mode 100644 ui/widgets/tab_list.vue create mode 100644 ui/widgets/tab_window.css create mode 100644 ui/widgets/tab_window.vue create mode 100644 ui/widgets/tabs.vue create mode 100644 ui/widgets/window.css create mode 100644 ui/widgets/window.vue create mode 100644 ui/xhr.js create mode 100644 webpack.config.js diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md new file mode 100644 index 0000000..7f2d408 --- /dev/null +++ b/DEPENDENCIES.md @@ -0,0 +1,41 @@ +# Dependencies used by Sshwifty + +Sshwifty uses many third-party components. Those components is required in order +for Sshwifty to function. + +A list of used components can be found inside `package.json` and `go.mod` file. + +Major dependencies includes: + +## For front-end application + +- [Vue](https://vuejs.org), Licensed under MIT license +- [Babel](https://babeljs.io/), Licensed under MIT license +- [XTerm.js](https://xtermjs.org/), Licensed under MIT license +- [normalize.css](https://github.com/necolas/normalize.css), Licensed under MIT license +- [Roboto font](https://en.wikipedia.org/wiki/Roboto), Licensed under Apache license + Packaged by [Christian Hoffmeister](https://github.com/choffmeister/roboto-fontface-bower), Licensed under Apache 2.0 +- [iconv-lite](https://github.com/ashtuchkin/iconv-lite), Licensed under MIT license +- [buffer](https://github.com/feross/buffer), Licensed under MIT license +- [fontfaceobserver](https://github.com/bramstein/fontfaceobserver), [View license](https://github.com/bramstein/fontfaceobserver/blob/master/LICENSE) +- [Hack Font](https://github.com/source-foundry/Hack), [View license](https://github.com/source-foundry/Hack/blob/master/LICENSE.md) +- [Nerd Fonts](https://www.nerdfonts.com/), packaged by [@azurity/pure-nerd-font](http://github.com/azurity/pure-nerd-font) + includes icons from following fonts: + - [Powerline Extra Symbols](https://github.com/ryanoasis/powerline-extra-symbols), Licensed under MIT license + - [Font Awesome](https://github.com/FortAwesome/Font-Awesome), [View license](https://github.com/FortAwesome/Font-Awesome/blob/6.x/LICENSE.txt) + - [Font Awesome Extension](https://github.com/AndreLZGava/font-awesome-extension), Licensed under MIT license + - [Material Design Icons](https://github.com/Templarian/MaterialDesign), [View license](https://github.com/Templarian/MaterialDesign/blob/master/LICENSE) + - [Weather Icons](https://github.com/erikflowers/weather-icons), Licensed under SIL OFL 1.1 + - [Devicons](https://github.com/vorillaz/devicons), Licensed under MIT license + - [Octicons](https://github.com/primer/octicons), Licensed under MIT license + - [Codicons](https://github.com/microsoft/vscode-codicons), Licensed under MIT License + - [Font Logos (Formerly Font Linux)](https://github.com/Lukas-W/font-logos), Licensed under Unlicense license + - [Pomicons](https://github.com/gabrielelana/pomicons), Licensed under OFL-1.1 license + - ... and more, see [full list](https://github.com/ryanoasis/nerd-fonts/tree/master/src/glyphs) + +## For back-end application + +- [Go programming language](https://golang.org), [View license](https://github.com/golang/go/blob/master/LICENSE) +- `github.com/gorilla/websocket`, Licensed under BSD-2-Cause license +- `golang.org/x/net/proxy` [View license](https://github.com/golang/net/blob/master/LICENSE) +- `golang.org/x/crypto`, [View license](https://github.com/golang/crypto/blob/master/LICENSE) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..22a7eb3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,79 @@ +# Build the build base environment +FROM ubuntu:devel AS base +RUN set -ex && \ + cd / && \ + echo '#!/bin/sh' > /try.sh && echo 'res=1; for i in $(seq 0 36); do $@; res=$?; [ $res -eq 0 ] && exit $res || sleep 10; done; exit $res' >> /try.sh && chmod +x /try.sh && \ + echo '#!/bin/sh' > /child.sh && echo 'cpid=""; ret=0; i=0; for c in "$@"; do ( (((((eval "$c"; echo $? >&3) | sed "s/^/|-($i) /" >&4) 2>&1 | sed "s/^/|-($i)!/" >&2) 3>&1) | (read xs; exit $xs)) 4>&1) & ppid=$!; cpid="$cpid $ppid"; echo "+ Child $i (PID $ppid): $c ..."; i=$((i+1)); done; for c in $cpid; do wait $c; cret=$?; [ $cret -eq 0 ] && continue; echo "* Child PID $c has failed." >&2; ret=$cret; done; exit $ret' >> /child.sh && chmod +x /child.sh && \ + export PATH=$PATH:/ && \ + export DEBIAN_FRONTEND=noninteractive && \ + ([ -z "$HTTP_PROXY" ] || (echo "Acquire::http::Proxy \"$HTTP_PROXY\";" >> /etc/apt/apt.conf)) && \ + ([ -z "$HTTPS_PROXY" ] || (echo "Acquire::https::Proxy \"$HTTPS_PROXY\";" >> /etc/apt/apt.conf)) && \ + (echo "Acquire::Retries \"8\";" >> /etc/apt/apt.conf) && \ + echo '#!/bin/sh' > /install.sh && echo 'apt-get -y update && apt-get -y --fix-broken install autoconf automake libtool build-essential ca-certificates curl git nodejs npm golang-go libvips libvips-dev' >> /install.sh && chmod +x /install.sh && \ + /try.sh /install.sh && rm /install.sh && \ + /try.sh update-ca-certificates -f && c_rehash && \ + ([ -z "$HTTP_PROXY" ] || (git config --global http.proxy "$HTTP_PROXY" && npm config set proxy "$HTTP_PROXY")) && \ + ([ -z "$HTTPS_PROXY" ] || (git config --global https.proxy "$HTTPS_PROXY" && npm config set https-proxy "$HTTPS_PROXY")) && \ + export PATH=$PATH:"$(go env GOPATH)/bin" && \ + ([ -z "$CUSTOM_COMMAND" ] || (echo "Running custom command: $CUSTOM_COMMAND" && $CUSTOM_COMMAND)) && \ + echo '#!/bin/sh' > /install.sh && echo "(npm install -g n && n stable) || (npm cache clean -f && false)" >> /install.sh && chmod +x /install.sh && /try.sh /install.sh && rm /install.sh && \ + git version && \ + go version && \ + npm version + +# Build the base environment for application libraries +FROM base AS libbase +COPY . /tmp/.build/sshwifty +RUN set -ex && \ + cd / && \ + export PATH=$PATH:/ && \ + export DEBIAN_FRONTEND=noninteractive && \ + export CPPFLAGS='-DPNG_ARM_NEON_OPT=0' && \ + /try.sh apt-get install libpng-dev -y && \ + ls -l /tmp/.build/sshwifty && \ + /child.sh \ + "cd /tmp/.build/sshwifty && echo '#!/bin/sh' > /npm_install.sh && echo \"npm install || (npm cache clean -f && rm ~/.npm/_* -rf && false)\" >> /npm_install.sh && chmod +x /npm_install.sh && /try.sh /npm_install.sh && rm /npm_install.sh" \ + 'cd /tmp/.build/sshwifty && /try.sh go mod download' + +# Main building environment +FROM libbase AS builder +RUN set -ex && \ + cd / && \ + export PATH=$PATH:/ && \ + ([ -z "$HTTP_PROXY" ] || (git config --global http.proxy "$HTTP_PROXY" && npm config set proxy "$HTTP_PROXY")) && \ + ([ -z "$HTTPS_PROXY" ] || (git config --global https.proxy "$HTTPS_PROXY" && npm config set https-proxy "$HTTPS_PROXY")) && \ + (cd /tmp/.build/sshwifty && /try.sh npm run build && mv ./sshwifty /) + +# Build the final image for running +FROM alpine:latest +ENV SSHWIFTY_HOSTNAME= \ + SSHWIFTY_SHAREDKEY= \ + SSHWIFTY_DIALTIMEOUT=10 \ + SSHWIFTY_SOCKS5= \ + SSHWIFTY_SOCKS5_USER= \ + SSHWIFTY_SOCKS5_PASSWORD= \ + SSHWIFTY_LISTENINTERFACE=0.0.0.0 \ + SSHWIFTY_LISTENPORT=8182 \ + SSHWIFTY_INITIALTIMEOUT=0 \ + SSHWIFTY_READTIMEOUT=0 \ + SSHWIFTY_WRITETIMEOUT=0 \ + SSHWIFTY_HEARTBEATTIMEOUT=0 \ + SSHWIFTY_READDELAY=0 \ + SSHWIFTY_WRITEELAY=0 \ + SSHWIFTY_TLSCERTIFICATEFILE= \ + SSHWIFTY_TLSCERTIFICATEKEYFILE= \ + SSHWIFTY_DOCKER_TLSCERT= \ + SSHWIFTY_DOCKER_TLSCERTKEY= \ + SSHWIFTY_PRESETS= \ + SSHWIFTY_SERVERMESSAGE= \ + SSHWIFTY_ONLYALLOWPRESETREMOTES= +COPY --from=builder /sshwifty / +COPY . /sshwifty-src +RUN set -ex && \ + adduser -D sshwifty && \ + chmod +x /sshwifty && \ + echo '#!/bin/sh' > /sshwifty.sh && echo '([ -z "$SSHWIFTY_DOCKER_TLSCERT" ] || echo "$SSHWIFTY_DOCKER_TLSCERT" > /tmp/cert); ([ -z "$SSHWIFTY_DOCKER_TLSCERTKEY" ] || echo "$SSHWIFTY_DOCKER_TLSCERTKEY" > /tmp/certkey); if [ -f "/tmp/cert" ] && [ -f "/tmp/certkey" ]; then SSHWIFTY_TLSCERTIFICATEFILE=/tmp/cert SSHWIFTY_TLSCERTIFICATEKEYFILE=/tmp/certkey /sshwifty; else /sshwifty; fi;' >> /sshwifty.sh && chmod +x /sshwifty.sh +USER sshwifty +EXPOSE 8182 +ENTRYPOINT [ "/sshwifty.sh" ] +CMD [] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..cba6f6a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,660 @@ +### GNU AFFERO GENERAL PUBLIC LICENSE + +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +### Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains +free software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing +under this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +### TERMS AND CONDITIONS + +#### 0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public +License. + +"Copyright" also means copyright-like laws that apply to other kinds +of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of +an exact copy. The resulting work is called a "modified version" of +the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user +through a computer network, with no transfer of a copy, is not +conveying. + +An interactive user interface displays "Appropriate Legal Notices" to +the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +#### 1. Source Code. + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of +a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +#### 2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such +circumvention is effected by exercising rights under this License with +respect to the covered work, and you disclaim any intention to limit +operation or modification of the work as a means of enforcing, against +the work's users, your or third parties' legal rights to forbid +circumvention of technological measures. + +#### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +#### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these +conditions: + +- a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is + released under this License and any conditions added under + section 7. This requirement modifies the requirement in section 4 + to "keep intact all notices". +- c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +#### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +- d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, + provided you inform other peers where the object code and + Corresponding Source of the work are being offered to the general + public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, +family, or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of +coverage. For a particular product received by a particular user, +"normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected +to use, the product. A product is a consumer product regardless of +whether the product has substantial commercial, industrial or +non-consumer uses, unless such uses represent the only significant +mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to +install and execute modified versions of a covered work in that User +Product from a modified version of its Corresponding Source. The +information must suffice to ensure that the continued functioning of +the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or +updates for a work that has been modified or installed by the +recipient, or for the User Product in which it has been modified or +installed. Access to a network may be denied when the modification +itself materially and adversely affects the operation of the network +or violates the rules and protocols for communication across the +network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +#### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, + or requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors + or authors of the material; or +- e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the +above requirements apply either way. + +#### 8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +#### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +#### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +#### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you +are a party to an arrangement with a third party that is in the +business of distributing software, under which you make payment to the +third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties +who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by +you (or copies made from those copies), or (b) primarily for and in +connection with specific products or compilations that contain the +covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +#### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under +this License and any other pertinent obligations, then as a +consequence you may not convey it at all. For example, if you agree to +terms that obligate you to collect a royalty for further conveying +from those to whom you convey the Program, the only way you could +satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +#### 13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your +version supports such interaction) an opportunity to receive the +Corresponding Source of your version by providing access to the +Corresponding Source from a network server at no charge, through some +standard or customary means of facilitating copying of software. This +Corresponding Source shall include the Corresponding Source for any +work covered by version 3 of the GNU General Public License that is +incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +#### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU Affero General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever +published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +#### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +#### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR +LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM +TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +#### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +### How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + +To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively state +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper +mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for +the specific requirements. + +You should also get your employer (if you work as a programmer) or +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. For more information on this, and how to apply and follow +the GNU AGPL, see . diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f70251 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Sshwifty Web SSH & Telnet Client + +**Sshwifty is a SSH and Telnet connector made for the Web.** + +![Web Interface](Screenshot.png) + +## déployer + +```bash +docker compose up -d +``` + +## License + +Code of this project is licensed under AGPL, see [LICENSE.md] for detail. + +Third-party components used by this project are licensed under their respective +licenses. See [DEPENDENCIES.md] to learn more about dependencies used by this +project and read their copyright statements. + +[LICENSE.md]: LICENSE.md +[DEPENDENCIES.md]: DEPENDENCIES.md + +## Contribuer + +- [ ] traduire en français diff --git a/Screenshot.png b/Screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..db7993e7dcb052bccbcbb8a30e0b701dd38cd861 GIT binary patch literal 44438 zcmbq)WmFsA7cDNOxczV~6o=x0;O=d42(G1guwX@sTcL%vxVw8G&>{tjB|!0@1%d@A zp;&>(|HJ$6KEGMBX3ae__pE#N-sjBRb!QTd3^YlfFh0S;!XnkyQhSAkg^R+%!Z9bp zeYE`eCQtv^&@j?7R@d~=d$j!jZ?vpTk4_2;3+dTeXlQ8YSegIhU@a{zeKc~33-e09 z5K)j72FeJ^N%I3FMHB%-^3r^=5(08kqKa}nQeu2E;?E>S#gyc@B}85*%S);$iK_s` z#l_`ifks9~4-XF#stS^-iiU=U02w(>PR=HKA6q*MYHl_b5g|^I=S%`TLb5N|#b4b2 zJ3YI5;Fgk7c=Lr<)itDQ-s+{CZ=kP%El4HN{e`;Ta|uB?WtpwJ2RVIRet9`nM5dOu ztca4D3_wiX*i_os)hJ~^6DT3&-|pz^%c~y>va6R}7|{JnoBZxCMjLrB|VVt#&bWY8h8(p*;Hy`$pZ@!8n#OAaYZE@|Zt zKlZ(pguDLS%jn9jea|_BSB2$Q7v7%>GYrP(xFl?=&FmL~gAqbgbx&b**e0wEOD|u}|^d?_o0Ur>{G*Je{JY z@~f)4okLL~qTMe<=fp&p=LX>4$`WQ}MeL+U%R|HSrOrh}a$3F%=t%w(5iS4xuDd3K z{UrUr{}{k4HD%jPufv<|ADQL=zcP4@MNY&`LPdy;gU7(kCIpmz@my7lj$cAOPA4ud zF5Ev>yHX+HQGPXR+G@(i!OKT^>4wzqj3bvmIVOSlU|NyxmGLOn9#;NWN4>|GhVJBr zxD|uVB2GS^Pcs&%Jhi7Jxm^UxDav4fZpKUDC(FN|a=euovvF{U?mH zD?SD;D-;U123l5B+Ex7nzM{_o(M72LdzgEVx+3G$V!2B5 zW{IBbLdm9sDU>PVji{CoK6up{>z75{o8RIo@T#9zG|=x~D`S|I^E_|9m*`Eq%l35= zZ8>cpe=YC~ng7m2iC6t!h-YUxG;!~Wjw^xs{qzeYbni>HfTouc#Sn+nftL}oV9$O0 zlVg86djc1HzrhUobw5O2bC`0d8cJ^u>DWuvn2dbSohks9hjDhGYrsk;TV5uU~+(&8p^zjPs)ySiJqWn|$U$YhT%(LRS zpz344Jn?ap@8s-6uU;B&Ixk4hGD*zpt4TxFszMATbA%RbP>;BN+Wba28Mq5D0Se5X z=Xx>2ndX+s3SN&(eox1Zf5d`lguytNN3N=X?1EtXvyB#}ZBKB@d9r%@sNC{@P}Q7F z1Q_MV0VMCje}Tmj;$M5@iv$YBeaW%~k~&{nK8_YkuV2a{$Ol9cumld#lRiXuU#r*;116z^7u0nPtML&lhlZGK%@>@&$RKHJHP;e>sEDpl zo$k;W|8QINOOwQHhXT)32N)mK$xQIXI+hMXTRw_puJQ1P!b6-XWMr{f_R~S?Y}tc6m_1dZ;n3ce<(3S{LB-Z+|0T7 z4<(s_D(v|AAwp1mMkt{{Du@WvLD#3n+(l+fhF}rsF{iIZ?;XCxNJ-8JB{r}je(jP) z*|ed^tm8y-P)9Ynjx6-zIhz5s=wBibv(Qi$(@QDF{h!btUo8(FhngSI{gB06__V*?l zNH+R3B+v8rM2s~|DbSb+B(BZO*BqkXN={j0Fec#k?oNpl5O^<4qob}w4s1v7asA@v z`bH14OIP1>*jTngefQj!G0zU-tX8>XbN|HKT}K*S)AQ`|s}^RRMhMzLw_plDTviTR zeVM%bM2i$FjHlNJgnCY(_LdJN5i2juwx1+>nI{X>qi)YEzWVV%R}KUBy=-#3(E|Om zC7<{}5nVsLx)if9UjQY2Z#=D|{bDJ6UnmjhB2iV$byHpELi7 zL*Mkc(KJk0e;2(e2tDcwYH8iZRLiGN_AMD}9_rrQ!SY%1Vb0Jv^p#q_W{Ug+pB1L` zkpo){k4MP}I;QL4^6;-9^`G#DA8UmD>NfwPDOxg8h;dvShcySE9!JQxw{Pz0zC_x> zIAeSHEy3H*S#^H`G##Va&-`MO!D{qKm%?%eS|v;qyY>!jG@JIpCXNRx9gjjvaZ3G* zySo?#T|Tf{n@y~B*XA-ncIigv0*M=oJz_#NX?boIvkn&uomTyxL3tpvgi`7YLeNNu zt!vU9!Bz_+a0I88%_)N61*v;jA-77PF!bf5vcTfcFH<7rcn`1mzx0J(8_jatj^=>r!`e;8g=ZBcMoInU|_1`gBCa-CV`BO*g3{Fh+AOJe3;uMALe%<3eX zspUlSt(!@4vR!BnEpfD<=tjIVhRpu>%OG?J)1!C6@<(Pb!e#M~3q!j}2`l0W&iCUA zu%8m1Tj_#xOBx$~ob%k^*POkh$UIT6-p3tqF7#F-0*N+E^5M$CNJ_>%l_U*-EN-oL zHa|j};U620&knE;Luq=}>p>4o8K?qr5dCzp8$v;!`qrVcvUNM1V${`*ToFJ0>6!s| z&qf$R{B}bUxe(e>N{tQ-g8Ppy=^A6QSX}L?PVcw9G4Be4^5U{aM<;t=1r7N_!S30s z4~&T;_`~O2J{?kK$Yd(m_c)YeY+cM-TRfa|zr@JgvXT=O`&*8&NZnC!1!ismc}2AX zLkd6Fl;H}&$!jVc>PD)=t&+V`NO(Y?!4{? zlrEFVW;P!nwgYcFArpl*PY4XKoG;HH2lTST_48b-St|^1+gAOJ8=Ba!N(X{JDu|R3 z(U%XiP0(dhAuHv}e@DAUI{e5@%tT2qu`S7kmxMNCARD#)?A#N$J2;vbcyfyUrbBN> z^I1nx-=rIAf3G(bAi8{(B$@vG*eRLLTgLoN`#^z`$Iz%n`m){sdtB_OM(r+t8)Fo2Ni3yw|ij-O`^Ym{4Vb+@`;{+ZW3xuavspnTz9w>6J?rkKT5R znzN3-K-w;=6|O86wVYI_2h9@E|0{wJF&o~jQjYotJ^f7=j+;r&O0zGX=|g8nezkO& z(sH8p>g=wUo5;==3f29yX-Cg|)nYhP&~jP$Qao}s_cbPzTU5SRmFhkr79oOpOuM`B<3EL{G-)N;E?R-l=L$qo}B!Hm1s^Qg4dtKDKodIgZ}2ZzA<&U zIq%Jk6`5N6wxD3ayr&ZFTN$H!AefgYf%>&nVt6 zD{@Xr^Oq@Vgb3S9%v7Jn{#n|DJWf3|hv9T5_ma3qW&>iQ!^6&vHP-gzno5wh=U3>n ztCEG43p-|q!184fm!-?H<3tbhoHpN#ZrdvZi*PT>>3g9q`JuX5_V$&` z#D2>%wbi*_U;OKyjM&~Wb*WMm2~-+hg1Td3T25GNY$z7b*>_;1MY2$TS=GaQ76hCx z=|4+v*)>>^vC6s}OAV7y1qYHRMU6g*_6wSG37}7|zV4dS4u*o(0?JES= z_k|ByW$6=<7Gv^ ze>B+^dC`Ies`6fd8x`{grQ`puz|eC3ZckyX%NC#DPMjWP`_u=itK{yS|M#2xJNRu4 z2tmBkIG67nkV@f62&ycip9npe!nyXj+RC}?kULGyP81LyMi`3g{FY0cLdBZqZO#CPBR zVw*z~j5vwN2U9EWm_8jPvus!011^1wh+cEpnTE2Vl!u$M(B;2Bh5PPF)ZkU@JAIZT zoMc2}2HRH0TcJs%2gsXHB(f;-2Lm`fpTG4wFLrlwm!$Pi)G*3ZeSyQ`)-BN*OW5|( z2)wmi>yvoOs9fIjj78$^Uw6lz%~I)yE?tE8gPK8Xyb=tJo#jj3Cy=EnaJ*)Q&^`r~ z$#(m#HK4e6g{_{oW{wT`m?nATEG#$3DN}Mn2yTS5#^1jN`)?%CO;5>?t4BiU-*{wv z@D~6iR!Pp`v(T_0Ef)(VDPMxCp;#7Bwl}EFiVhq`A;nPFbi_llk(W9Qx5$iMYV7(a zC1&J(&VPJ!2sYr3f#(lDq?N6qKMU|F>e!^FpP2yg{W_ximpOY^L#;>ofH)^oN*@|B9V=9OfFB@BHl#N<|VBy-!Af>z~*s-ex!a3xJ*EZ zL?0G-Qv?evB+ucy`hmFb0Zt3xU~+vQp+61k!BR|mh~kAGZG93OQO)b@@&7p+BzgU{ z>Ua5Tf_bQJEmZKdvbdC}NNstF(LG4B>91d=WpOwBHL?+I+h=aIK2%#NmDr-F(jMx8 z?-;OI-d58mcHnA)72e?_C}iD3RJ6@|p!n4Bu;$DC%3ER~p(8;8oIAJ3sq}zM-_$vb z3o;VB)-pEACiupJ{^vMKKt9^gegE;waoi)b6F zm9uJuIAG`@%2g$*34J|1a_kzFO<7pIa5n^mac)$px7fRa?8fsQ)XY7cWi>EZIDpXw zD`X3gB1QlkHy_5r0`^DIhdW?Atyn-Vp8kyZz)Onz%0^p(Wf~Tt+6QhzV|p|NgPE&$ z?QP|nE>ZnJe?~j0aSU4z!cS!Ge^-5ELrkZvp|TjrVJ z9%>G2lM%v!$8%cF@j0XnN^D^NO*Xs62I_C?vQ6;eaC zB8ri)@U$_R*~k?1Q9x(FC{7w9b{KBKK2t2s{(ks!l>xUc!TvPI-i(3LG3BtU68k`$ZO!H zmrdt%73&G006tZ$JHh;PHoF4o)tAxJc?=&<>@K_#) zgLk-n_wsmVF&k?7BIPIg^nE>7gD+bf(M8vi{DjT^fOJ|E@N~^F3i7e)+%|np zTrCb+p#VLlFV#SiVf4bl7o>J&bN-*`Q#ts|`}4Si<=wtbrSdA`Hd)~a^w z69Du2?)E+EC9NvKIM}p>xnQ|2v`%?}?I>J=_X+rDm-&K$LH#&Zw|VS1)<4O5FO={A z*_hY=-xv|U^w1MOf1B?%E`#Xbn^SW{`T)P9MUKtE%Ct(27BvKZ468#@oq{QxulH}z z^?j%`%?fMig8zxk8QFDEZI>=w{rJ`ak~9C3d?1s1MNh|(^s@a-FGHuY0v|(7jLNIf z&}YvpozIJx@GG*alRLS|*RdHsOHz1WG|Rv2bOhVwh7n$P9Ti;eA|1IB zN-~_RHP^%jf{o`j4eIeFWIsK>F^*RFsZjrND&Vm=TP4+NB0!PXXGgcuno>0FRh6$2 zOAG~2^EiRP`69U)FV-cn>#4$AQ2LMenxEr~i9o^sb0HDKDom|dk#7mP4iLEU838h` zib?zHrKT-;c|7)~LqRy6A8#**PIBd=QxNCCg3t)&l}4_iEXLy`Dh6F0q|wm+6kR#-b>BTJ;aj( z{uCHI%osMZvn8DtQvw{>{gQls(w&UM;_;Pe&XKv8x8c?)3hiyU+f=kyxo9od?q-g4 zQ1a8O+qeWpNCs9+>~1PDoa{r`xb&yEXP;H(Lr!J9Br$kkCe-`S_qe39qURlK^WHs? zN4r=s(aG!+d{9QsxdsSuTGR@&l8I7qY}Mnz4Km1{D#JI=UY)V%{J!<{S*Vj(D^VmY z5byq|Go?L^OH*&_t*q=wMDRO4Wv{s=4YYFZtQbL)dpgNHGf!+_Lm8XC{+DoCc*Jyt z?%h{%lnu9YsP+?2s>m$jUuh<0TowkbI?n8`7fX>%hpcN7ssHTpLlrW5NNoRnkoqP% zzg2sUfER(K67vi58ksr zr3%G_Ts2aSZesz~hw?IlL2*B1`!=CrqO+%8+2?#`Xy_y1-Jz#v(-VpO-HERP`|txP z!~JEnyAAPB`Firx0U{2W{$~wM0wv)5%C_z2gtOd|n8hds4wwY91^t{3S9DNnkVq;W z~C|!skiJn1?6$c5Hxs&rp5*Y zna~^iZOhd;_)(oT^RfgnZBZ+yKgC!Dyg8=|ZE&qUS{gN9e;b>$u)=#wwm^K;&M!%m zbg-7SGlt4N_Z}6^Xtv*oivLDQF#ptr(P6skmXn7B`MHT#-}hxygx|GbdpBcf-a8=y-0(r%IJ;W?QizwNEhcFhHESRcx>dM(-)672H%FdmAdv0>Z!WhcjMUnluM_} zsif-Y^Rpa3#Ti0ZOJ8I7du9aOb#YrH4`&LS`;_gM-TjW73X`Vvvc-8_4W=f_2H)>| z`J%vk{vMRkbZZB~wOvpmA_ojLk=e&bmvChz$j`tD5`ZecI#b2#++msoP+B(EmDn=5 zsvh>2L91jeutw;=m+22fC~E^_as7Xl8tK`7hMbrxB|^5fLbk%|-EsEA z1=;j+$|$)YdK4XhD8uY(L4 zDhP-XF2oSu3<mLQHuET0YqiKz&DH?3%C6fUSV^SL%G*m z9}b?mshuYu%RRTSjwlC&@u)VSw+q5PMs(~^?{U$v`~pD_jZ(xYD<4^*5syUU|QW<($FDY^=A?a z41ttMtUkh)3+i9dpXnoT)fbe;s}L{TC8BC_8PGx2@3b;k; zu9uBy(^sHY4ihwA!jUi6l@RTqfxz^mh2$sDPujlt(KfO=O{>j9J?83_dz08bw1(B2 zSU}NDb$$qIJwINP0=tqK5dQ0e1_YGEI7|iCk&G`+g!6V0WiE?oshregJKtoELO_ zwWy~;j@3U)iV#@kPaW~9mK|n-^+x2Y;ctj4kR1N33M{Czyh90vG$L zGclI?Q(Xxqj)eUQMwqG=#qf752wI@!V1Y8C9qQOqtDfDqmDc^poDqEGN7n!eU_u!Y zGjF2stNHZnEs^s}jN7O_6(NL7zf}Dgk?2KhL4$<$SJ8ud{|CGwE+G5VCPL_fmllPvepRelCc4DUBt0U5oFsf3_U9GziUHJ6p%dCywBO$mmM#d9Ub8Siy{tC#9H{+3_ z#Qm!Nk>LAdQzo>tC~=fY?_P|Cna9;@iGGNXI^uARCaAf!1A!!s$o;kBO}!22^pE`B z_z>NBSUwIjg2xlscUvV7Ph1OrL`=Y@{@Rkkn6}j{`5>=$l2`u_sv_j>Qz+&CdJ|mL zb#lF1zGq@8a#Y#ONk;hjp3n7Pe=b95c}?A;Q?t?{!;LewJMZ`&klknA=z-y}O6ll_ ztJiWOU z)iJiSlz}nzZMf5Wgw11?<0IwSq>0r#52jqrVp`7o{r8YEg@9-=_;zn&0XG^EL;UA} zPEOS2x8Oy`QA_m4ZS0V?n5Vr{O-X`W8#NtA3clxCqd>^xvhmxOBI8(HG4_)JZ$veD z=CLa9*_?Lye%F4BZqR&su>t<{D+`NVy?y~X`?{pZ^CaqgD4e=?Yo)YyjnP)7_E*|@ z^_#U}*msSf2!+YH%m!ipzWiDCq(Hp}pUQLNN5{UUyPMrv1vq!67gW=gxzgKR|eP6+0Mi5lwD{sAKJjHT-SJ5UPO-vVlcXYCAN1%MKN_889Sh8PZL7vgz%gq%aFs z<6qor$OnunO{uPa;-SHkbMw05Euz63*QU*n-dvmt+q^TRd()k&v z)=|QDey6%8e_Aw^R_^yAOs6JsmHZ+c^`z+kJ{7+FUoBVn?&K^%x2%QTibb98uCQuH zkYv0b9d4F^zO9y+vRzwyUlug=H-S61w9qCt5`W4d^F1h|KNke}e@LwXr4CMRMd7NHw_JEKt^_Z_SfQ zT;AR#h!k#^YOzkz6koP595po?U6WF*_^E_&5F3;!LmNI-ilVYX{q>S5ie@d&#fJ=c zt=PX^FjO=f)!e6-l>eJwz-nLL{Sw`6n;D;Q_amIzS(ii3Bc+B-@kY2@4iZX0)S|` z40FYPyQc5Z%x_`RWPwdD#qbkAC4WiA?MyAiykSu{+l2qeb!0UZT|i~Opp*URHO8tq zsZXy$R$nIx7Ewrcv+i}mE}&`JO#DTIsI5V-rex5hU00?vBz*X3cQN*Gpkd1@Us^B~ zIJIP1H;-`4A)Rn>;U(ClH8olFNSU>Wm+{Q`X}BB>sN`GU7%LEBtoAB6{4D;<6UgnR zq#WD*;6><*0fNa`?%>Dv_0iR4Mn2aeU^KygKQux70}laI-i^DZ?l3{5qc6LpmSvqK zo)216EnD31N%->E1T3GUkx(M;=e}_^8020wcrL;t_pC4ETwp&m0t1gA`{%U8rvv1ixrC9;SNp7^P<|N{ImJa)|@hGg9T%*K$ruAc|iW=yf2;pR^BRF{kVWG1h`w!9J0MO<6XekO$E z=lBPu7j#E}niKKlga{xuJYoBO>yXEI*PYT0JY;#>6~aewSeC1^<*<~#hVLBf;_iMv zCKG1+h2fXxB+V{R$*MuQOV-bW4W~*_Uj!DF$N$6aDA`jF2+5biS7C z&AhK!EmkB^Pmf#J8=yg&1)Tp~=fTAU{r11P^0@>47PO5o9np%=mt!@nB$hKBDB&sZ z|HB8NIxgU4Z!K{x0=a)E7kh#WE^bNFNhypeX>{-etvyjWk>SuCl@ho?*r*ZAb3|^57%=RD$h^bCFnq2D5XGLM0&QepM-TcIYn) zAER1HiprAEX+m^njAiG6tw)P`MZe|VeN`)Mej1ccaIO9BkjcW6sg+Sd-!U3Z3#~3M zdg$kaD$cK$$`z$+RC1%>z4u}anB8L*i{+c!Y4l!_DC#&qqK;4hd^Pt*j?yVU*@RKB ztB><{D7r9BB^dQh$t-JpscuJ1kb#B;kGK8EG-mjZosKzret?OdF}`F3Vq|hRoQxjr zN8@_hrWfEYcE;wR|i~9dFIGpI7`tDd72L6hYIpBC3Ze|9Xkp%2u(KY0A|cDIzC5OV|)W9w)i z@?cr1Y}gfJcQ7+IJ$%A-+bn9jL>h2ebnITH$QyiAr*@-K|4ERoIXxkF(ktToEc`!V zb3*Etc-_iUTho?^DP25>x=zYK*;4C>k*pfz8jRd6j#e4ON*aAGa*Mvrv2$9Q8p;)1WKSkYG!qW*`YZnWDJ~SGY&WL@~mMR^D4VIdW z3`~^I>tQl;Y$rqhOTEj-S`R%8&|ZGoz|_4kKlo<)+L)TCi5gsePdW165#kQ~jw_T- z>Xc-UtwLsT!|PwPFp#9#^Zf2i$(=1+>TCAg5j^t0dNYnoP4OGk@vr1V4fhIw!0|@u zp{IQpfmvGqFxXHZEci0IIo;r0-^27*4W|Zwr~nr52~IS)$1rA%_z!ca9CpaNU!}^0 zpsg4(EjW-CUV1Cqbnd{beEGh}P=R?&DV=Oy4}Y(wtjjMN{ae}4&Ww;niVq%X^G;#K zk4u?QpBa34alC?k#!vCCphUQW1&x2L(#nbr?Wf06%c=TJw>qhu5!99LU-%pGvZz$e zw2n`l7=WTGx_yVk@~SAN$7fYP=aBCx09eWv>*@P;>V!2dthD&*&3hZKQeG|s7ITF+!aI(Ri5)+6XO3?2N#N@LE=q`zv=3okh0G|K~iqA>ixNL`g1ZX)&o zr+OhNzy-gkBj)+jv8^nC1|i{QbDa~$4Z|Ml^$9-if(d(T;0Mun057DaqV0&!$`BlQ z>-Vm-Vt~*{7LD%&6imR~o6q(b>t{>(@9G#xN5|xyF|58Zq_5!g2A$faE0I|Zh(jxm zvktA4v|}i7ij5+9WeZE^?)P6$0vBJ-FbiKkBCJz7ExlM@o|?ux^SWqof58&F^gR0z z7}cC%!qG@)yB;o1Rj_IZeN~!1K)u9zYf4fl znp?4~Dgr|YarzoygZmJBp}JR=9N3^AjT9e4x(xwU4L4L!sE z=zog&7!Ze`EkRe&b2>4Gf6(I+g$7CAYY)h}k`S?qBmkAsPnpR91YDP+ztf2Y)crNYkvZHiI@og z#Mi=i`0EMmNTaaThW)^Rkp)KLC6X9+wTPPc)0l4dI;Ip4ibF&Vp#l|0r8elaTV9mn z79(cGWJshjCFnjbDaaq#BtGo^h5QF0MC+GZ{^{;twX~4y)B@ddi-=U#*2qxpg77a9 z;T>%V@x?F2otz%vI#Yb}H>rKWvO`7NmA1b%_8JlNW#_4ViY%j$UfAk6>xQ1kGfbr& z#;HqJ9qC(I>F(bOlD=HwU5G_c!{wY_=tWh@Ef;%GTeKK42AqhNqCX8nRRKGF}|A@$keT#olN^*&C z^}l^r^&cq-x>P)+KoP|w3GI)VT-F>ZR4!`$Dj$_&&u&#rzMlMdQ9sJYxm!`Jw9-Df z)@=^xicweix8&_z#8EmjDmB_B`xBLs#SlM@1Fg%6F?C^{Mup7L`XLi1WC{=#nwAJQ zcHBGX>Ckto^^cI(X#~xF2oDr5b?oulpF`F>8=QFoxk*Xp*IT;}q82eNehSUH_ng{F z4&F70+Y|#0Vg?%YZbFr@5=lsV*O#MP(dtG$)+;J{CM{1RdcX_2#5nX6m#OVDnBa<;SJv~|Cm~E zm!lXF`60rW_!5VoqVQG9{GuC}I^mIe_N0raFJFbte8c`W71mMMY*1(uGq&Ge7lmv0 zPJvz7Oc5sKvEc&~mIBy(u23)QpXaT2dive|Mtzzsm9^-F{$qiuN95ZBRt;{tRig{O z6&N7nSKOHM=Oj{C%ctL3p>NmWG3Fpf=Th=?_=yZ4L`tQwGkw_xUSy>{p;QPAsn!g3JT`@5Jqz}l-{Jwo9e||oH-%Nk?&&->p8+forPbtIc0uk1ZGs9q9s zqc)dxE8@TdJkospyZJvl!aGugMoynV`F|DttmWewdEzOo>n7H>AF4Ynco|hy%aszjA(5+32+`3nv?%N)m%kw*Nof4_(v|n! z8iB`8$^E4G-I|PB>J+FW7XeG7j2ZWVf!QY!))as1(H8)x4nHcM>6fm~Sa!pQRhCVb z9+++f{jX;X8ka4F&bl7>rPI8p1RlBvk`dLhHp1{*N^Fr7W-^o)UZOytn$u7ZOgJh4 ztjE2i^oI^A#jo9@?v&EaRQGGCgqNFs`b z!$#eP-TI$&E53xMlE#A?O2|2ov+6w=f}+L}%V7amBVK>XEG2I!?Hg6lk^FG|h4}bd zcY@|<8wwZu>7B_h&UXlivhm3Ly;G4@1yg@rnZmy}%YC1Z37Rv74K>V;7!NOWQ0e5* z9-OH{6>*|2#`(#=(M`bZw%daJ3#jd5vJ%6QCNeBmjaFXRU58lfzW(zIzbI`k*~*8r zW|$QY>Z#=~Au4mp9ge#Sha`5FreZOj0&e42H-bGE*A;-V$ZB`N#m%KkeU=}yWs4jC z+16hM;gkiM4WFD10*+Vh(5FeITK%)utBZBjL3V^nU9fC6ed*)@0XSL7Jgx7}TOz#I z!90in3n@XvLx;s$AZF$rYkE&-q}k5D>*G`i^O}S4XUfzFc#(1t1qvQ@Hyfu6cRjpV zC4+#W#T|`-^i#9HOUP%VAcdw+z`3~4AVtm4^W-LEh#xV9Ng$a$^neG(}#`U&bip%^$N=+8#Povn@ygWx%`m zNplaQr1{KoPav@59M_vUZlycxa8r?;8ccU&6mUS+d&`yo_i$Om^-H)1G5qx$L!H@Z z_7jZjN&mdd$SY?QbUmKUqb9HCPMihqs3nArS~HLvkC(;+{0s-ZZK8(d1&d$aoh4h- zULU$=G39ajnn^)H{Y*g&`|Sj&1|P`cbfc!orc6{s=uzmy73ua6Hv{>vrH!~kr2^iK zRxqdCDSRZ71Uh5QCDp~c%^7%`Z{{gfO7hx?xIl;!HUE!r3_E*AIq1|x5oJVZ z{zW|K2yY@Jn0q~7Rtj*iLh!NGDM$3iVRkYWq22iS(%8a)WQo`p?rYBElPAAizS^#+ zKnOis%#!TF!d&^UOcwfE(SbcGmE~8mU1nu&w}tQTjNQ{gv#80K)1*QQZzf4d;NmYD zl>h&9^CxPpA6VVJxRHrw?YmW8N2y5@&9g#|%*0SrZ_#~P@s;)q*x@gO-w;InyL~3x zGQ}ddvhHSndbPlWxx$8ECqe@yumL@=0kUFrg-;_QsSj5drjYh%Ed}~ld-(k1F zy89I#Xb=@4ALLz|7a`_?xzHYeTb^{VXvv@)g!GnXhV#uK2SpTVq;8IaWHHga+^{{GW}jm z5>W5I7~S_C49f9Ag;F5&H}jzW+fU*_jmO77Q`QuLHhJOSXKkg+imYD3cMENU%!}k; z8y`W11Vt|7Loy?U6?N#&A5J`Zk*xOvYdUOk_AxIY3S=k5N|*ocss}^x!QUR1C2MWG2rL$tKG8N zwBZdmb_dN%#JzX^(h_pzJ#2epck@fn7xN=gHW|B!TZP@p(3jQ16B^_rlak}m4*0WA zsM>LX0Ht*Eqbbwfo{Y^tppPHus_z9t-$(53SBQF9s9(0sy%1j8UPVZyhx;q!! zb_@?_Y;@FF_f-rF`%loMPqQF%n+o_l?(ydOVn%4_8#i=NEUs zhy7==#n-`^n?c=+Tz~)k=}`$!OsmM@B2XY>KEs~4P^vr#L=T~$&r!q8eb)7-3j~Cz z?CPsNGXH*!1O@*-mx1?;=W;P|8OcE(iqx#cYEm1s_D2k! zi`cwifYuddjhv8XqWp68uUeBA|9xS1kBr+&BMjwZp#%UBQf#!`TfDeY1L zCkfwnlsUpl%Tm2Md}~e!5~FQ|c3u(Q79BcM3C8l_0_hTk25AnYtJ>zQVVu zZ3a}kmf=)-$`-!OD`((vp(@|_0V}J+4(N`dV`gC|=3Ov7rmVkgVI|CWv373Ddx~jo z;@SFP#7}G5_S_D5`6pCCD3Y)~F)==f1NB4)6WET>UF+xsk8nnh^A4mMbX5-0Yw0m8>n*+aKEgF2BX0fJJ64 zjJtx*2v}MAO=h3WADZ;@Y@HAmXoq~xp0I2u2*VALkn1IZG!3oi@&DPwkKD~%->-j$ zQ01#=ABU+cX}XAT4EeU}M1Q`~rr=mKQQOa&zGC!gRMtDP8!?Gl;-n)|{lT*E!gUb* z?5`&{rCF8GSLVNYj&_uFqt>Pjdb4>^B)t*As*xX+pdX#sv-C}Q(OT@ZpDp#v z_Kux_&ktHgUEwc!zqiThaAkg^qZkoW2Be7 z&RA8u$^Z-YAHA0UfkX>E(@%!{8R5$gT-auv1e=ti{L@4BVfL@zfeJu*HAx~N_zF2# zbWVN1>zi?ewu(s%MK1e2?p!zm2Zpb#xcRD9LcQQA*1UZg^cht7+Zsqj$24ZSr3g1M zaABM`TwR*t4pe6yUyU@!4LJ78c?yoDpydB$FA==2O~`-CnRY=#aeP*ESR_NN9OS6| zL93X|2(Hh4pi$3kte;8tdRia5JA(NU&vN|y%rqzZUJw~iHD{TbrqLAB6`#8uH z{=tow6))gBe=qjdSKXwAZK2JG;0-QmD@T55l#08weHt>>^3aYz4)3sp<;^=|?z&M~_spTcCsfE5da2>*0C+5Hz_ z`dyOOx`sr0pd5-_bYJ);C+@GsJ2q0-CPhzK zgdRLC7XLf@pu`aQv`N&W+PhJ@dcY&6XSv9Ko%s!~h`f^n zdNw^=r~2F$wr=c@y;#6r+c+#= zS(E>F&9@Rqce}cP(qGp8H_MhDQNsTwQb$h`q(2f@T>J3%(xK`J0N;w45@2b=PXbjb z73I~mdOP4NwBWNo+3U#wb+1NTW~k&xP1D(>lA`3acry+)I{uG8u%09+YIZl^Lk^?K zC^$WI3P1la+WnJEYuih?Yg*d@w_?O7)#Ci#g+Fkfl&~Tmv3T z>M3fgJ$R{Bxe9!#UN_c&55*~35kBmgd(z8KzsT`1xX6+s8b9<^Of?qDCd)d;OsNLf zL+K9Cc0t*< zCdw zhT4sq$(8^el6TQza|DES*)O$%2zIMs*tN^Or9fwXcw2f$JcJG}oOO_Z_z#v_QlFu> z80cK8+X|@gME=M*Mar~V4Vdg&ptm0AU~YI7^34g3q){eH)~bJ}RlD3<6Lh%CFM^NQ zL9V{E=K*_Nsjgk_Eetx4wXQ}n2ve*dUhx`FTA;Ty=uo~nqC^39>C?y>=Xc_!udTH@ z=uo1asBq|JD#7}B)PvAGwh8D!;WW~xr4g;f!8V-zi8G67Cp1Inc$XlA zrr1Zv?Yh=FBKZ<+*)X-mQg+>a15D;i>or~lc4Qn)Kh1ge`oteIxmaN7-;+&-rT*g6 z+kSWW>aX?8bq4!nskCm=k6HRj=>{&26)^pfY*@~-Z^#XUO_fRo990MMrR)U#b-a?#e{$;I z;qSJe{9coMuApzfNK-&BZ~r0P6zJ*l-*Pn$-0b~>eTMe#!NI|91=*E@S@vPYUSk9M zgJ^L7fVDNcapjOsBC~S1hRl`Ssp=QmVc+sCJAl^t;%hqaa*&YWyO~V?RvhW(HO~DPfdb8pBW8l@L|}~yL-Esfxb62yLUK?D!Ub%vQ0rhT$x%stWW_yRXJRt zA|9d2{x0La!d?v#&>vjO(^kcKa{JqC=~@38nb^5CI=R!IixcRp16$!wohs-P40I9) z`mGCN&#tdj2dbc(>9In6(C=^ezZqM4GPQU$vt|0AV;}VU54T7NbUfZ?H)gNTXMV}w z%X}3P*)#oX5u7KtUkuie=nt+%W+>>$gSCA3pJo*F`OMu2=>Pf0H!kQu_P@#RT%QfC ze4hC=`}oU`E6vgXd&~83_kojA@nU17U_wuPMZ+a zH9WA@)s^*jIn z^PCez(CI&K6c{&S13jU-MwA1L%SEg~MHlr`1h=S%?eu^!z`_=9UL4#3q+P7XmC|ZO z^`W9Wr{oFFWdU7e=*sP483vp_gS)tJ(+$xOc4cTN^;AopiZd%J?jpk9mFLfy+S zDD^HU&xlwhGCV9LCRc_UY7p=U2cQc)gMd4wX?fJfxs)pvte|sZAcvs$XG$tqfeq8V zbD6R#I))tn$&Ze~RkGo=KB2xLFJ0;jpI1L(F7ViM`KF*R#X|FPXjRFJ{9;W$bz4Cf zr+$@hBwT0YzgS+~UVjx0FGRy|WM?J5Yyo|Gg?^?(mFL%#=p187!}HB6shj3|p!=`d z0D32sZrecTbghwq4l z9sXS6og-KDFUWTe$B0t*IM!*` z7iP%ynG(wb=r{5oNit_d03zsKFM+2m7xo?~x+|O96|9OLq@V*^@8f@vgvS6K-hCPV zj08QfDvxv046%Ck8DJX9iyFE6Em>lvpDS=9mCCqtO#70If4*Uz*qX`c57LLqN*rn? zoDyx#G6-{2fscKT8b6tASh4P}m9lowojuTh%oG9it(XzgOY`upzkI1bkt5{T6lmWX zx%;^q^H@NSX5eeDJAClH`le+J&Ywa5=Ah5Y;e}t1=VP9MLdiP_T{>fc{+68j2Goyy zHkpuoJ3#Mt`JYFLcRIrORpvxeoRPF`(ZsTiY6;TB-r z1lewd5%f;C+s&&M(9>Nmbd|?Y9eB5c9{-#nm)2po)NdIu6@s8{lh5+5Mdy#?G!qD_ z0?WK5>!JwO{dLIg+mHPNKtJ?u7Ghxo^nvZD#4IZ7tY8Cuhs?P_K}9wBm0wkg-oP5! zWer8o{>#rqlm_g^!h=&}&lqz<84kXA^TwIdPS#4o14SQ>2X!GowmHwZ^G`KyC*JH=|r%|mNKa8fPP znqg9O^T9AEIs`C_%2%})+50V2kANL`QqlWzu{m~!$Oh=#3^~t`crP@EXJgbda8@V{ zd53I@PWFagZpT8P=8fMq>hhmK|K^}ik#Mb2f1B_}$O$8E7B>8%4!QtOA{{WBC&vsw zsWdjX>%OD9Z!I3$k24|@G16~fo=0b68(7iWlR>&0*<%%>qpr&;$-9-wY}T5YLk z-9ybz(*#|`03C(D*;dTxnj$Z=7@vPu`(7r!^q`M-3P7`-d zLFk{B^E@Ww#A&j74|;G!IcC{xp`qT-k~KhwfK(M+xsr;>IEWE8R0DJf9-P5s34-0a z;98_Ly&<+NfWE2_GMds3=jCt@be9ggwBNwrpL;^l*EzuE!~`Ao{vxB6;rDET5E=R! zbK)F(QFfz)pInKK)Boqt51=Et+?ZCP!8^1jrvElnXJ3^j%)Q1#a_ zr_H>$I8Y~_RP_E;jgaWRspx$(L|#s1;z;Y@W+95>Y=c+;`Id8 z9wBuL=-Jzqxbhfumk1_0_|p;veY0?ES9EahcF+w)e+0T%B^#_Y`yTx1_4tvTH>c9* zpgWyYN%HC34;wn@py*!(>h1MAGeCclDf{oU?gr>C`Z|63yt!F&{^m{L;BQYTdjHt# zzlU-OtD-ME9P{J^iGD@hcs2uXSn@%{UM2A_LpIRg>7Y9@&tr_<0-iz9*>*FegU;p= zaUJwfr`2q>x&ngEbn_2D$IoFKqZc-lMTnU5f`A6v1f2m_PYziFbap>>F7240FB_oG zkW+fk#>Fg6#(^!FB}J_H(o^(^MbV{s0{7=Rp=##Dd_I$a?Mcm6V}DelvUvT zX+?LwwShjOk-GF?q>a5uc+6j`kS@xeURF;1P$Naj0G-o8|KYwh*?Z7~ncgTH_W*Ri zF&Q5Ew_Zs9So5JaliDUu2;?~XJ341-ls65^AzZCXN_mjzYplC zK!p0N(E@iyxY?%1gCU$s=8hXsNzb*L|A zRn-8!*>xG9qs!+#-8_QMw^|QDUz5{xE#mSAV16)D0+~-kysk7Xpo5riZbKx_&Qs9Y z2(IoycW+_Opo1PEdpMsSgw+Jy?@A@ho)LIO{MCxyIQlTMs)HUZ9Qa*J$M*k0^Wk#lh;O@c$H@1=&Z?bLC+&XPs0FSEjUvYH{;Nv5ypV7sTt+7`;DKbZAZe zXX$j%`*JjY$JrD;Sg2bD?IfJ%RyY_!od3(-xi>YDWdZ!Nbf?qiwz-*%L)HCfYKx>_ zFma1E+XdOk2!x#gE`&4$2ti&E+~K95u(Rq?3Ti~K$_R?$;CGvIyF=1}489TvBn_4B zboWjAkzb$3J@*`^t4N|#(BWr5x41aV>wb%2TBtqF@*=)sT+Q;X|JDbVYK5mB4BKu?Y+_mSQmfsd1$oz4b@THU5Zr&pzfM4ye;u#*HpuUA%j zOyCbNZ`h|U5PO_FJi<)Z-1&XcB~Z{E$&rpl0T)0=Gf}tztYw@p3djZMk0CyJu?X}A zWkoL@=9aV3_2Gya9pj??=zw<1Dh~%pd)K&Flx{(@ksp1N@**HBH`mtMdd2<9 zzJ$eocpOlBJ^RuG9os<16G}8rU2Z&erbWQeZyF}saWWR--IK(~1S6LUu{1HVof!nS z1p&{JZDUmJffEa62$kr;dkQffcl4CT1fiE6wGyy_E*k@63ps_R8EC7d0rY!OSYzu) z{}sM@dZq;ERGLTck?BhmbU4oY2q1*UyRhZwX>5kW*Tad#h^HNJzu}UaWnl)XVCR^jZaAnd#&?@U48w>Q-0nkgN#TZs zm|h~s<)3Vz-Z_wVe--qv54v_a9$@R@2`b6Qjfb5q#uzv48T%oOv~b4Q2jT1< z+A}sJq~6|Q<{xW)J0j9rVT)E21e=7`0cbN}JZ#Q)qhp-P(!e?KWlGEf=u$R|QZPqi zwjcFpv)QL0$pLga0q9Rn$6S_w3JWj~6^5|&j2r)_J?^~PlUb!}d=r_>1_Mi4>z=ug zAJVGz?!l4Juz3B(xRL2ZLhoLtvp$|e6FbShj2S}*tumQaSTN5S7hLu+1KLvU!0GEV z9VeIox4UTeE z0)2ljZS6J`H|PYQ3$dLLNc8$Ri0XgG6E1chuJz^nVt6}^T;kqNK~v}9Hmnwh%ksn2 zWanIP(KDtqj^igX#>OSC$=JvZf6?igncllF+|5SLA=0~9!}ZMWI$^ghp5yT5bm-6k zn5Mm574&qy?|lvE?-NFx{-qsoS2F=Cwq||@1Uq89a49?e0J?cTHqa$My)%ATXwAG| z{AKhiGrr?v;A)0iw{O-mo{SkVbNjxpO7d3)9V3h}?eB&ekKG#O9J4UdxyrKCW`_-k z#mL$du_})c?4TYQrHW~R2RiOdEcoKgCJ3vrx5}C&8%2?~0_SK?70Ly`PzZ8V(>;YF zDXIiM*SB^sLJq!3w003ip(rtp)3oEQNLj>loB+a)?kecIF2K$QV_fnJF5AEt7boQQ z5Obb*J>SbNn#XCX;3CD0zU6f8a7t(Zp03lab28V4<%2MJ!Fk@vSt>c_fDdtiF$^;? z7w!r#I{gaspz~gL-Tuy;>Vu}OFCaeeiwCp%9PsTZ3{}f*G3{!KzHY1jMpgEC(s^Xe z+qrY*D`%Ez;m%t=cKVL9k774}h?-_vg8CcyFHAz!5`1)QzPUfo zOHobz`mKUaS;wdn(}~YORgRp|?pQ)7b6y&CG7>7PzW&LyJBCL+pP6s23gI^lx`{fj z4C^Popxc*{&bi8-MwWA-FU&aS5{1u9E?E`m-!|w6T0A}_a{a8;eW_U0mqV~uGsT?= z(D5l!;mcl;`4rTBseFF;YU45q?cbWH|iD28l6uXTNUVM33QAqqr#tx z^zfCa(|Q^Tx>-f6@gt4PQ1bHDtCtX3ZY&l@C<@p85O=lV!gZ;dxh$` zop970T(#QTnFJjpjL~VKT_}5<){`#RD?oY`z%vQD81Ax+^^&-IJtwftQyC|%R;Y}3 z?ikp4{?lPEAMn*a^wSHv^!K(du{6ev=R=@t!?K8T@$yGlPsa8?Qrs^u7ZriR!t(Mp zYXsaoy2+j;KHS!&*}?UM+b%ZJJto>@TC$POzk~NvZ|Jk*_0-bR`m#+Pw-T9**>lImV1*n2 zs^^HTC}UCqYkiI=Bw>3>^mfAv+E{2&U}@!;DsA=3169xmMoT>ma4c6CMjw=KR;o-Z zsPK1RO|3T`qSlGlo_$`2TX{(5tNfi((6!4Xec_+MbSa7#My`848uSuja~}yhzPofy zSbJsLb?uFVdotAw@_gwH_wGq!q4Y~n@PT83qziKU7rFEM-e4reCr9q+Km72WP4?H3 z#QDwk=KKn6IMVoiA8m{WBiAmBCytTm$AP}keLYtjs;N-)tpY-Q`T3J0pu5!10{sAX zS5=^&RnVomjzx?GH|z6LznZTWUTLjMpw8s?cW#PXR=$h5WY%H1h z`f6{Cn4mMv@u0ico`Ldi7wPYy*}L`U7~h?OwJPEd40b#(_ai{so0ss~1(nzSD>w4V z?nhFQG0x0Q*MMYB7qkIA1X5jjAkj3nJnu$-?wu=0bl#_GKE__E(YFx1?)OpaT*$qu zV5>8=@L5i{I@Y*zx10ZB(0TRlA<&(wYJpA{DD~!`byfXps8=oYQwut?DW`y37H_r# z@HF!WZrD@g_l>M>RbOcjVYYXMME=F9BP+x0&9~bFB|zu<6nQ@c3;%$|TOz_ZJSeYP zpbyKbV?kHfpCj~oDp0KX^XqT0XYCcXMa>~zj71&1_G|m1y@Q+wt)vlT{Vj>LTk|wy zGp`4C)uJ9YngAVbc3k~{b1o115J|*H#Fk{X!76>y;i3iL+7|^OIg*}@HqS-_g0pE- zkvGhd>oJllgOCT%?Xfh@*K1456m-Y(+WNC=PQ12W1Dr2C`&y3ps>}WKg6?i1(SDX? zQ43kYxSsqb^#t2jC%U&JzH5Svc3tTLW=Z7Aj2!rbz4PyB8%g8%YsoYoJU$QQbZAG? zNvpC>;?nfACWj~o6e!#U+5(Yi!_PwL)(~3Iwt=)X0D`0#k&^xELeZgparew5X&iu* zEe;FRrozE?3>o#~@AG5kdGv+oR%mq{?D1ou8$)dEIqT7Ag!SBg&GvjCiT(#8On^Qj ziprkO`F=V`=%JY{t2RmY)BRL84?w!8tu)y^Kxv;+@u(m(6n5) zz;=h=w@Q-ninn1q@5U9~HgirojK{MnGb2lunN6BzleBLpfc|v9bU{fsH-~<8K}Tp2 z^xL4wGcpCQ{VlYcJ@wW42&Jeo@Y1KM)U(i6ua1TG=6I);*++N6t;awg4NZ9(hFL#C z|1fHGFblYhs|6pu?QUgHor6vkGh0@&hFbA@Zoa3|A}UubHap%UW3ar-KnLyKr1)Ow zzp+>BT?y+h_5na2nJP2rjbRo9RsX!ic)_{T=hE0fpYqi5Di5VtoTx|1y>v-{u1_vE z%+DH2J#xFh9~tj^J3!0Z?zT$KD7q~?XPGUM(C?Zij5*{t;#o;fn)MfzV|U@Z%Agxy zm`}GNbtp`YJAiE@F#ChSqwRVl9;bF0q78$LR;_C>dYE-5v+ zj9v)FN%*gccvg{9a3qz%7qs%lo#lR|K}Se^&(>7*m`yY+Y$8e{1FUz0N8IwD)5rN= zxLm61eoWEd`zI(J3@rob;H(b-=%KYD=wD5x7eOyG-BY=G5s*U!=MKI+0t{YJ-VmmH z$sO+sd@g3x=JTcB3!oo2-RTmK!ViW%`mpi3=h$p`e3GII&^K-2_b;Fq6}<@hNPeJ# zg{3cwqlzv`HSp|{1nAPUl$kRD^!;>)l*?Y!#5tF}D-JrU9qI9*xyhG}fnWKL`oEUg zln10-fQ}!tHxFU^u@dO)SFqm$o4%h1rv7cr+d%*Ngm}b*arm$-7)SFm4n8{253*G} zuDuA$ga_kFYhTE{f!X>Z$MC@U;-iaTTuo@t2D)~iy@9v!9t^;ISUG9dT)u5v1w|*P zD*8wdAE(YHr{>5WQFLzsba()mi5rJp(P=z;csvDHCE8sMv+U zyo=sd20ap(Zj!qKYqbcqu~$<**zI{Kw}B}ra$hU1p}cE}qWq1V2>Ose*zBcqlRY=J zC4UR=%!DJM-Y0Zi^d#V*lO=vHQTq90(B)+OCe2Tn(c>9R2s(tFXuh-`PvHG{wsh@a zXp=_^5g{s%{Qlqww^={VXX3^t`&xyA-rE8HVHMmMcq2E4 zK313!uYO|vH`>`Rcr-BemwO}OzuUs@V5l^CZyr~4`8M0WDc`)oFM0u{#d%9r$dbsT zmlWMf2fax%BORs>WUs|dcO{Wa9pLk?eK`y4ZVvrwo9^9=MOj}5LTGG~y#qn-4)|B2 z>|GrQ{UY;6qpkGb0*glNEtzbI^e(t$S_Sz0QJ$Y`gT22UKpz}n(b-o*(MQ+{s<^Bb zr{X!YCGW1sz1%@Jb9`$e)B<1B7Ii1;Z59nIYa%YkUit{R&)8}-8vW>-;eSD2hdq_H z07t|W*e=F`d}bW;^J|}C(hhg&sjsy3B@%#PS=N-%K-hm|wyZV`!x8o#Q*@c~6M;>t zC_KGJ>~(3rfp!cd0OJ7WdXeUc?6gBW8b^7pzr+cGrCT7c{W1}q=;|*FOu=7!>Mq;}i zjq7hI9gM|VXLc#2TK8_feN`(xVUb!g`z-ITClhd5OJ>Ce%=;4AggqLs(YDxzXc42f z#lYC7-MndNY<)vV;&YUBW%NCW^@~L<%FG7kskL?9@pC38Iu>Owd>A$Z7?Rdm`$ zIjiWb)#T3E3Icyy392eAV3!2we@4YQ`J~k{xVh}@N+Jb)ZCzxkNSU!^^IRetki&{pqL5oTFmv#Y>Gkb{h7de5FRM zm#${Cu2-wz+>t=M>!Dv8(9gU%$~wI&eP0|dEm@sv71eXsUF^O?(2=ty{FPxHE7;*~ z5SP8llebuJ&K30U5Oilbuc8g_eB&3z@%r$Wzw?VA-KFlk1ifg&J7&g{YK7)5eI40q z^mvjTrrn+8{(Xa4#4hu=q~70g8gh84+YadGZ61|Y)-09H;D6s?uOj2 z1L%W0?|ex8?e|(IVE@L=m$0(W`1^fHD{F^cOqAlI|4=GR(=fBOD_TH} zJ>S~k6{k_;xAh<*^zlwVA#FQPQQOYWR_yJWh}Va9w%}`n4E}FtYklL^MJ3qnx{mdv zm0yp(Z)NN4=n^>4qKMKIR`#a2HgV*V;+vD!fd&79vaDpSL|I{Ieu-ESqbpWcJP!Dk zR>BqZ>j-)y>wAPoL(@J-bp4xXCzhFsn=ObQ#^V_?*IHWplFV7TdP1#^6FApyv#B3C zOJ}5cwi=jiC1h~Kzsj}&@NO24vVKJGvu#3OuoZNht*!=G{~1UpW3N}EaP=-34Ta&A zt&rpeMB)o|CX%L^FFdiI&g61UC56^4IAA8QbeIw&2UsW5nRv#w{6*$(4sy^lsSI2k zl$2CFlg`@YU9wEm>+Ze_X%0_PmsCEzE<^9?^|?#by_!G zM>*(EQ6#YJJCj{bhWt-3!K0xy@_+Wu{;6qni{pPwpEhZeO#qcK>ve|Pm!w(fb}ZPb zh~?@67Ai$|RV%gNAO@}g$}L8gF2gcKdS_97x)wW^x%brW8g3jm4Dm<3za@Ad6n=5F&Y##7;=gK@2-7K@aTF4gp zT83wHSYUY!3P@nazuujt2}qTg$gEx=Cvpy!zTeSW`8iHC;KG|IeS!+R*`geM|F zml+WaWeeMK<$zEKiG>}u)N2PEZj0Re{@$fT{N5_}D+;K4W+1|)CspowYJ>W^i=Dw~ z(*O9EcJ;?O19Wv^_*PwGZWMGjfS|L_Q!ho>)8;s~ckV>p8if+aoOrXq}YwL^wPTk@<`y1-dw;=$Q|ja zOdbZe2pPK_lm^xVaPGV${oYH&$kYQYY4Wp5;L^l>w2!{iGZ;To)+y*H<29gyZ%!Kg4YKz)^dSsXL4P(F=frEuJubbba=tgiy`$(j743Q7(m%8C==$RY1N89} zHJm%XZ_WUnDmpuxMgPg`(&OW&$et+dhG$$p*NygZ@qH`TG}BM62%r~BfGuctt>K&r zI(-8G=qw`^OD=nbCj)e7R6vib9Mc>_Z&1*u2F3=LAxB@7*1~gxy|Y>6pFN2mde($d zEYKpoKr^|!(z_ql9f%IODXm(HZh+piMIsqx{px=Q0uUL$OWu~YWN}nl^(_y$MF^=U z!^=_#g!{UrgpT4|DCP`13y4##3a)|t_Fhgeo1PT(qcfQJepz~@ig(Uul#cLF74&OT z3?1KJaOuY~V6VhGrJn^Z`e04&AAa(5<$acdesx(1)z=MgSM<){hQM7RvPH*pVsxo>nGi!miu9=}LN`MJCovn#dC+ z1kpFzJ!6h;ABpReCL@ayfW89nfn`aF%L~fLowPIwpij*#{F*}0VSCqGpI3#Y=B5GAI%Hn_cdW3HqbVPpbg>sPqDI>g?0m>{DOnsW3ko4X()qaOwrnmuO#|n3?ekbG8op zs-iC||BD+rxWONbHnW;kIwEw1arG^$ZBQ(b*0ux_y6(T+|D>ol5a$ zE;5G&9QvgZvop@NMejoLun3@!FH6!|7i0-Qx$+xT^oNQPr=Y)3!`b0~)iuIyDLOps z#5z|QN~?c_{bUrK=hb4ao8J$*)1taiYG)2pbk+*pizNV?=Zc$~Wvb}bT$+7~?s2L3 z-Y7Q(y)i)_xvRth^nT;E8y4tSf}r0Zq1^!bv@y&MwzU)ANgY4n{LC`tY=XG4e!mSa% zA9Hk$^ZC3qM<2eGoq3)za&)+OdjDK_?t_XxdD-Xq)af)Qq3A4x?mWM{xlgXkCeE5p zS*D8KeOQi;J#eN=g$g-(%_%jCUTHGT481Wy9~~4a=r7KlJ9oi(K+$cWtJ$Sd>J)nb zbRtHldXyOC=v!RNGXUL}k%H1R0nk^^eS7xonGa!(KJGLzXLmPuYDU=kQhQ-UC|I*- zd~*X-(GLS1=g?%ZOC7e^>`P+i^sfkjzOqyS9rx6=_Qjr~|2bY2rlPy_+ypXz(KaF8 zLt}LsxR+TIbjaG<56RKlz0DnKqD|#8YoxxLu_^jN(3@TEjRbm&mjLM3gG1^a=vVyvwYBGz;{5o@nuL5X|N+z+}J!uOsvnIjI4gVzh;BJi~W?{s-o@^yQ!ySc~V_`I@iC_2BZg;+>wRKsN!r^&KKQ{VYQ$&}_eqZX(IJje01ijHNK zDdkq$u5y*=-iQGzIrVVt)V zb0Hq{LAH>?DVeOrdAG0>iy{&{UIPIWcmnJ{!GB>9SSarI1(BalyO~;$)n>)ZU z06OAoZEXc%&&A-o6N@Rg8Y#Lnze(rKv|_^z898qNAJumzCJM9z&t<+Q+?eRQU5v}^ zE^IqnU3;54oT9~=g5I#8qoR}XIk>A5uYw-Qzz$lZsRFwAJcTvTtu1m5&X`Vb5p^E_ zNs~CdCP|5Y0KF4Cv6QzWQSmOA922aUa_A7cmV-?z%f%eFv}*J|LDTobc1yJc6#m8p z>i>Sw@i$A;D#tD2FNRdTtqTX*dpSGkcxa@a#V@m2`jFppRnY4kUOQqg&BwFOYMPeQ zw6e;(;oCNP%X4}B5hR{&+iKoulre;--{@#M&Y@$?Bb=q|cy7*!0}2{y{9fhaqS4Te zhR!rI^hN}IJlTuoQ#%CwOoqFO8V^$FG(HP>BKz>SL` zF5lJZkeWK78x4k;pdQh#%Ec4rV`tb2{IS{P-jJYIqHw|b#;Mpa>bF=WTa2x(Gx)k~ zzwrbXcEQ>#STqHHqI=oYTLD9XRhiz^G?G@iL)W<(zmL$OE5gC!wL7dE1W4_oR+c}c zBr~NN53njVg68U41PiCq!Yec@u8yfOJpYGRuzw8c)eN6iS+h|&#M+j$(rK%V%TrV5 zg*U1|*!Hk&%`xj@^G7=CmTz{sH#X>XtyvlSK3yZ|AK2K`8ZdiT3Dv!*mHTw7ihAMw zldUCBdX*b{d{w7UbE;sPg8o&5F5JYq@O8cv7Nx|G4{&_#0~S;#YD^>kRoo>{dhrW$ zO1?DBF88k#bdf}C3)ha@B3O+yCZERo^`mTQPRTa<6ilPpFWha8M)b`t_pdf||G3rZ z=>ArRL;RP{I?H|Wp3$dbLej*;AnXv{*k$YsnJ_y(#ET&`+NBFTe5& zdm5c{{Fhs(wJGTTXYXv7nn=<({#mAFxea!j0aMk>))hNLhQ!EO2A~jD5H75N1q6;j z!~m!f5LnOz1x3rEf^q?tt0amN!};B|dxnGw1UAGO$t2xHkq8+wWz{eL=lx-z>!j@Y z&U*gqx|X{jSje4$ZWPvaG2u=5&bmgRGtL<32JzQT23f$K49#g6p!?yQgLBM7XP_I) zVb>+!mhYuB-yWP}m=z-EN{K3jho!1V-!2SwYl~G&VK*B>*UPj|x-mg#pr=50{<`pC zO9V^vuV4R%JUSO&Hv`x{>Qkd9B0a$l#OP@OKnlmAEf9b}BpUH4oVG+`VUVxVUYg&q z|B!2Rj)mL{5%gi)HH4@5r3UwZhp@Yij#DL|)9J5AedpdD9XYpn;!$#1KuathjU9LZ zi1{REM4}BWzIY@WkJ*c90lSuKT5~kvS;)QcK!=02seg)nez4Rq$%$9-V=)8h4%>*H zitdY>p(Fudjaea##3Xa94WI}e{@Owyx3~B}-UjDN^@+o4ynOw2$M zl0O34E?`Y-KG^UY=iJVv>`c%L5cH+~nO+&P(?P$gyx}_u%r4uotB1UF3wPbdU3IhL z?*=e_Koe!_IOH#N3ne`8}kurJqcwa&W`ap!M0xNh72oE^tg)ysH=g5IAq=u$WmIfyBZ z?iOF%N0nW%52S+_(!7+$q*J3?-pA&$Rdp6}FGSEC_%VSBLaOL=VzVbhFwS`gkI;PE zJ=+Qd4%^eEhG)RLGS>focD%b6p#CZ8Xhuy=K6)(RiAU%Hm~cGqQT@Vt5QTaq*o~y> z-#-k`5Gzp71v!JhM5j3tyF63W{f<1`k^In8Q}avz%cX{uB~Io`WMv=c93p&MmEGgT&#>G%uWEA@`yJop9}k&?zaP zljFRKdmx<I$)R_sPTGV28~Ql>ogObaEk;T{-WTo9Rvi?9_C(Qo;5esOC?4XlM>d z<`caydEeBkj}5Qwz&VyfFQ$T-vHq^D>jPbZB;p?I?rRI5ly&WMZU%|Nmz9<2 ztLseA3l((e@Y0nkQWA8?wbN}Y&1p~#kRPoJV3saqaamNLyS(IDLC&MAz4~*3ZVyKe z;q zB^BjXx?s0yi&e4Ct;r`xG?O2l)zNAI8(w?b z%d`&8rNZzm-60dw@ zKprJ$&YXJq*59k^k1iGDqt53;E}7m(9EY@)bzOpUd7$eohhEsA-?gm^5P7Gh994a6 z9Ao?{oy!h&GkZ3iukCHWke`t!&GN(U=`X&9F@BS3WxaDa^+wg(+BxL@s(kbekGe;Y z_ETidhM6tNLhcN7Z9oUZ*ng?fk`DUZ=ktu5L5WasH#EHq0o5>JU>CX6zJNar;pDw&X~yeSN^^nh8(GMmfqvF; zF1yVTBB(}ktU+1elx@5&v1vgOI?a4zDN8sR~Yv6eDl6zGhr;sM=srScL%7NVxADyxifMI|}(>E+6bbin^n zc?CcSo67IIE9KvTRLafS*DYgbYPDut%|@;F;3Wb&G2qvNy_>s~^-$VenI>{0^r5O9 z`ztHcJ_4Y_rt%%^)W3J&$7puE+X6tkw{On-a(LY$@ytVKpevwvjAD#GTA_VFo-hNU zxLi9#9dJpyX2Z4bxg*TQTjV8E0CGF>txWOi_?);SJIu~iy4xjtH$7MaN`w0n3{gcF z)^^v@+;n<#ZXW=P@*CEA(&yi}oyY!M2LZh!&2iU?D#qd?c?tym1Ay!;!bTF zdF_3-HQlI3i!Q6|o}-#`gya$%yX2a(P;g_Pkojr*MQCE8MD3XSshU59bmIKT3HkOP z&%n_M)Qflo{rcp|TS@=!b)z`!r;eavR22p2h=PtDVOO1!PUmh6_jh>+=)DAV5A49N z1VPw&p31e0UKgRgoof6^(8ZmX2%NXkjSKCOi%h|NlN^4h5%M=_mkXu^;W}@i)BE~bq_vv*r8D?tq$+RiCD-y+B@dO!Sohk)LP zpUuv@IPxeTHRw}^^WQUo?x+oBvzxog>9718W!Yg{ z&jh;tj~OxjCwChrIRHBGONkfeXN2$8*J?5ur%aX!da;4t@LWK=caqxW;@|?V<2$jx zhMOaxOY@^dLrdS3!S+6-C<(;}zX zl2$idJwwr(`kxEO$d@Jlp6xjY%zp>EmKE^^ni-we=*B$sA^@E(L`7o_pLsd4)`4FV zfZHHXL0_E_ki4v^f5v$%dERA#+)h^@1L(l(Pd=NFTUxVdbTb9_XzC2D1zQx*H+Nw= z=zVlG^R3K2`b}Fhx_?>D=Q$tY9!MU=Ib4?o=sHZRFp8`vU3SJ)5rEF!!XIzX|*cVJib0DdN&1f6$u1rB9@6~7m< z5q7f>+G|csOiaK})BDy$XwQ<^=Rsm`Hq?_^++g>1y&Z0#sbS~ugo zg+Dj8wzgWqpv}GYpkXGP1{-s+y6e=2XKic606OBo*syy=K-{nw6Ca(2V+$C+X|MGV zCC5r;of{s-_(Pp4;C9uEYrj&t7w^|v(=GRRhps#U?wOR;w2HM2`D{yYEwnMct?Y<_ zo55{W!Ht95)CZ5UyZIRCgZL2i4x5Ws>NF_-Kg|Gk4$<4rehL-PG!C!RAX)8U|BJn| z>uH-w!|-pJdD-q}HJSh?nYo_jtl`p`oGIz_6c$dnRG)ii}d z1;NJBC@HOJn*M}M%fPdyAa{HY$2ws>_Rf3b!(ZvSwj)jU&SE(Z;lcWDW(6TQ^x^6PZrL-zH_tKI9 ztSs5s8N2914?E~&f)9RA&CwfxPVcviMW}AWhxbp{1yErJT}IJA{_!`762A*>0@fu7 zH~c&|U%WYyp@$yys1@AKRmYEG*VbOCw|)CKH~JF8V>=mKSR?7&zx||N8HnzAh|x0eao6hqQ;i2^NUI^P@0cSndM!-wiu%4q?X)j=IRu1?c{P zPDAU%`yRyK>L^(u=mPY{u;Ift96qoIy48kVkR(9=Io89e>;BJkyPdH#LeK^1zo6>W zf6^P@oAZCv+%2p(CoFdXI&G{zH{2Y$-V9H95`E~Q1zo1Tmq13uhpvA;s6Mxd5cF_@ z4kZBAgK_6VX3hnYtvgY?$k0OwI;8=i9@^C^n&FYm5n;K9Qqh6+Ku`~4IDAuhb3sgY z^Roy+4=L!u1n|MIjxh<{{B@r*b|L6t20fq{KA_%p(hT$58g){uu-wB9dSJR9+O;Kp zp3~}=pbN`gfZjwgJZX*?zVQ~O5rQs2mjg5B0$P=(;rrYq|3mm+MO`o?Ko2~I7mG{! zV$Dgyau=Zc1A0*I90_m6?(cATNeH?C-6znS$cS(J1?ME6AHe>zs0%?Cpx^*|Pq#Z8j1|HgT4!g3d&-+sDIL)qo#_jsBc1@S`A1?WE# z_CRDk0NNXWu?h+Qu17qH3|)X;kE#bE>+WHyCQ3v;vNsLwrNnH!Z(`YVokO8P>cy| zyiJ38aipbbx?LgmIqa?e4YOqF-MbU1F1#&GkuJ|7xjNWkhhkUYg6Lw8?nT+M53w}o zi$GO{59#e-w=~^BTGgWV7LLY3&_fS88e)6bagBO=kay1zJ5cO&{Yn_0Wo-LNIel3J<&;%eI0jU?z~x&CY&!{R;1humO61Y29XDF$N>jD z_h^G{bF^o7;kIBQ=mK=MavkXF^Q#`Ur-}`F?Jl?EiSixJqdAu5Ekx4j@&xUdpgXYd z=Gk05l1{tt@e)6B?JF54%WRqupzi2VVa=)zo7+u|4GxZB{4!I9N}5)2o;3b5I5^m1 zZLy=V4YqRmLS!bMI(g2@2;)BXev-Y#IGZupFhAV@J6g&wA~(J>@ij|Eb#P9HOw$29 z$^Kk2PwC}4veq->%TmVZm$Go>NpsDTB3p0)JS@Z0W%F&7V&c46ip-AP8yj1KkK3I| zjqS4O>LGXn4gt_5{KPPA(DjOO@`R;@8jts081E*+nZ^A7geN5@d6 zfYHe>kfY1J$K$=lN9`Q*+=o~dbvYZ~Q#$t4t+XRR4G=(F*a3Sbm1 zp6O)$`57T{wrHQfCh6Uox79|0CRvQoeL%m@xHw#1I!@T2FY3gWY4i0t%B1&0j}3at ziMIiE94)L>gZexCRnF0jxre0=I3@gVgZ{vLuVXa7P>uf|6hAY#QBxRAPFwKZAmE4F zaw1E3GMy|YdW#7+=?>`7hEk}mkg5+)B)HhAE?J=66KqAAiNBKI%5$z996O-%G6cbk zwQo#G5;S*iLvF|cUD9pP+dEh+f=eSiLeK^14(Mo^<@4;7EbAq9m}l>m?*HbQad`N# z{i>p)yDV>xBxJz%;|DW!2-&&5ISQa#9c+^2A$z1$EwNKqFuy+kN~?^H_pMo|gI@?5 zf!Pepr%V^<@&ucMpDo3O1)V~iZP8Bi(`S}CoU6sTH34W>|LbGG_W;`_&xRF*Y*)@I zfYK5|Hx?c~?2-xYWGfHFF}VU>hF>RI($O6Uv~jIu|bF1(jSoq*qjh_0lEXaIvK-|C?@1ywx^S9 zmn;+b-62BdtBSq_01W}oDo6z?t*NpL^wFJY#Q=#%Npsp2yz85*FEhr@ei<5zRianc z#!(3bbHWWe+8-UCU};JU3ku<&^8goB)=*7BhZn|OmixZZPh|MfLnu2_NS*PZ9W9Ua zbFnXQFAlQzA=|&>KAP2Gq>0Tt6esMKvRta&C0c>_02smW#seO1+n`71*Hi*2d=>QQ z9e5o>UaV{m8?nE%NT_1Oq&|q5D9jKykCjf*a;lYDLoAjx%bGVw z*Eh?`19L^@pxcVRK3_(bClhy-8PlE4+o0S06!ON|E`%n0r((aBOslya{?xD!MKAQg^|4S9t0H-J|HB{G|~a^l4$a z3(y_VsX9COInTC{ewPiOBoOa4XSH2-wXg_(23T5wU>t#9@hHqAVZBN@u7RNu!cvqF~Rbp9Jl}0PAipRuGLQ-vNRa_P*|2QR)GefASzl!E@~yJ3Q_eJF=0-@gOsgW^D~1v*^r zS(0{+!zZTo_AYmDsQbZ32ivz^Rk_DL?5rMv?<5+87b39MHl~%kDw0&Li^@2j8#wChgPs7U)jghGS z$L@g$%X82b#R0mbdo47c+uC`sUwH|N&UjJak5+Yn&QJdJ7C5zmQq=}MsXyM`>zobG z`t*Tz&_Un>0j~ai@RJzQhg;oiJa-1%UvU|7Xn6v%AYB$x{wxHZu0H4BTp2eSyuyUJ zA;HNURa?=yR}P%EJM%3?zio35#Fna|OBG#Os=SqCit-=2L$qlC{eDa(V3*G+ z%DVZA5!Q}+l)lv%A(MJRU8>|BsfTZS>3>4-XASJLrKjSQerDzUbp7TwA$wz6`JO z=$=N@dbT&Q5K~#yc*`C4sbw$+r09W4)IOdWH$=CIqO`CJOqexz9vNi3syWd_)gXg|U!kvtpP(qr%ycxF!8zH^$e zt!ytK98Qf1&gBU>nMdJ&EzS$j?=U55e&LLlm+dONm4PZU?=z64;FPhUmd*W85Cy&K zK|f8UQgQW(nT>COkIok9>D>jRrYVYcNKZ#TwRNwNrB!Y!!=GHKHUyMwe+&y209PD@f4c(@)kXs%$9i6T|XGP z^INdPd~-5e&Gxpt?Bq2)ndPd^wsDr z>AoCD((1!_D%H$i36v}JU?dffholH?Oww&BFc^=QvhjF-cvU~yqK*LlIYs9k@abhE zQ@99vcqE>p^CR(IQ1kg|D@SgeQXUOlr28ZAA91g|%I}dNS)eNkZW6aA!5TG{l`_D7c-o|& zc{0tkk&TsFphxK&0G*Z6N_mS`HxFMWltId`UsXFyE~!>>z0OQIxSR;jg{O_Q*pL@9 zB6lWPr=R(bTMI>c)GrMEH4({^2YIe|P-2JH?{uTdZqnM3R;9p#sqY1NI=d*(r2EHk zj&lIaxUJ|F3v?-%Eb$wKS8LjXnq~>N1-i(#r~2Qa;V99MB6L@-RNAhg^cca1W^IHpfdUQ`1?NQ?+yq8UD2tX)mRecLeK25XS{6}!Q z9CT(410#fKJu2abIN>9sEUsHyXPORsh;;zo%9RZ;-4}&puW2H554&a76M~w~?loD~ zH0uXg)|&5%)>3_9ZS!LcHW~NRw|lhE3+`TDoLf@U&UdWCnf#rP*AfXc5)(V*1Ru@D zKg;$8*aM=53=gdlYv3C%Et!_B9h$YNGzYrzDol)bYmeIz`%HUy8|>ku%HUNECg7je z3fr;fWuZ#QN{M55qn-&_?H?UR9PYoMEyVD><_;4judx8}*s0dBVf zVt^Y8dbfklv6Wj_zc7OBYr|Hjz-knxS`^%uFKx6`x59zZrCI`qvS5cUi}fJA&>8=yr7O#w)uwad;nO!#k{a6m$gr zI-tAV$L>`JdpC}JaTxFj41#`5&|UY~Jz0R=n~Zp0B+j9rBj~Px?z#={Q7%oF3cG+E zL+%K=JD`)BpCK!Iln(EQ?sen^L3ass4`OqQCsFur48xS`c(^KzBC^@0AtbH6z`S76jcB(1{mWvOXHYe#3(9=%FL% zo`LSV!>-7_>0n1e$I@M(yBml1C~M9e)Nhcmqo5<`-h%GByY68RyC>dy00kXOmw@g; zpPR3yst6$GSh@vt_j}zG->{B?j-|UmcRdX6r%|oQIhHO`beC1#S7tndj-{(W_rqKj z9y#j)47p?JBGA29_yrwy6m$gr#(_@U?qZkyvl_$?o`ebo9YO!8pu3(n=c6^A0x0NM zx(9T>MCN?G$P;3Mp#OZ(l^#`bopxD4))90B-5b!E+T7H%tDjcdxJ=qL)dgYy{olPV zlI}+%9zApf-A&M?g*|3(hGuJ<&V%=V``cyb)wera@$!1Uk+^mko($30O<&^4V_8Q^ z5cKZ>U46QHwAo?*Ql<9D-kdP#oa5MQ11wG~~M@o?>Pw4Uv;-p_jb zerE4F3Obe_0(#k;2~8G{7-7lB#S_3flHFV@9u1SI0hV_nqR%}sUjm5_UTL7llf}+VV%sR~eak`^A9%$X%i4-l@U_C$2 zk)3V8S32eRMjund(C1p5hmMQ7VSG`QiIrmGHphFgls(|cv#0z>)dF4k>-@O}+F4a5 zj*Fj%0dRiufUNX2){0}1-7)3L?z-#;+K~|iy#wgRUMc(GNM*D1-3pygun1krexS8~ zhV%1W&Gp_$25@DgK<=msb@T$OH$l%7-lCM;89< zhg}+QohhhJ(08*RjBKv}{gnVcUHV+7`@B)uTQC}_%_isTl~;GS^wb!Mn%PfAZc`y^ zw0aC*TM5@|cNghN{0wwODd)3)mNJDFs4QY+-qmxPa@5RxGP0X;Uoo5CeVord`svF- zAm>;-2i^D|duPJaMv}$x&r(sOXm`Dxn%QVdT8);fN_QJzqwRP{gyHnsiw(vG13p1G zF%brXAr?^%g+W0gvLXXFc$B;aM?a?$Y&mLC66%2H%__{=f^;1YN8^`%CeCy{YG2W~j<#pWdjfb%(1% z@W+_F31Mk`9_U_bE9eD#23!ZkI@uh64z~CJ+W_1BwfZ|*0A<}8jmj=_-}b3npfAOU z$b@(UbWvf0gk;T>KaHRl5p<$!l#twNFFcvC*M!h)93}dmZLi513STXua z^Ut|}ZleYIjL}ECSV~W@T~4tbqwF=MQ&2g`#;6~#Zi1c-Jmr+;3%m*X4EO+eft5Az zKYZOGKDs1FFA91a4|(!=9=ey>1^Pa(kDITi(`>^AeF#7ok9O&`BpkXCkVROi^k;Ml4yb6 z$u0QzPGdL(e*6M^Rva}wraOVLNz8)!;;i8rR$(ZEo^gV~{L)@I}qpto;y z?y^(tK93L&^z%Rm`F$UoaDq;X5fntyUC`r;WN4`g`UyoZI~0Az*f}^j*cqc`s3`ZM z*4gg>&VALb=vnr*spzDk=%n8Sk3Xe5rq1@l%khGRtTsU3P@r>t#aJ$fs?-7c5j5rC z6K`yNX@IU>5$M{DJmSd{bPx3NK-aTiq?4cv^bun=1IBu>Kr&lX=_NGXxvl6Cm+4+; zfj+=`)lO6bo;4JC`mJKY|wgKJKrh6`aT>C^C z^XORA1k>HJ*W=*7V7jvn9oNJsOf=ogq058bE?vI_?;ao?=;tc>D44!-(*?R*duPIr zN{DpDBZKd`o-yXQ{52->;h3y2HR@iK1MHb|Fm6|JoJhyn@=kmRA;pkudaZLy^3r@X z=1}y4?8rk8;Z$@5dTH&cbj_lZO4yz@*Y}KBF=2k7rFcllA~d<5^$5Etyg`afGzogR zkeF4<7jQr?YeuVFj1#Cl2bFiVs{q}0*R`86Hs|r-fqp*drMOXxPrE=D6^1-cgkwZX z0u*({*>sYLj2dHaD!LKL zuw9FsR`ep9uI+ppY|Wzgo1t3mV2smj+?Zxyse{)ZNkV0sh(~I*oozv>FJXqKL-%MR zV@%fqDN4x*Lr-3VS2LzFtVgR?z8cVP)(Pw$A0FtR0X_NNW-d`X9Lpx>NH7W7I`n2v z=`)U*mF8d7sidXH$)g^ixRN~nd0;`5R$l~@$I1`aHw(5#Pt^{8!y!06liar_(JDtV z*9+th%)=hnJo@fTy%D8VoT?*}1wlmduunXROef!%fIY1*iO-d%lgTt&By{~_@)M5I ziLU8GH0J&JL;U2OT0>Ktes57KXOha!r+o5*+ABj6ACt#booZOw(zN0cpIDF+Z)UG% zf$R-%**!cw&_BD@y*^;3>jc+@QjJj$U@S&;oP{Iyy4l8Hx;w#R zEY}d6MNf8{;twvS$)SJ&+Rgx1S-q356v)VIqZ)!hYn+%JI-oDu@0qT8N!FSRtS`6QwLzyoi?B0;O2l~Y~wfr|2 zx6N_A+n7!oJy-V%8P~QpH5hkhnqYIBh)p#@uR>LbwF0LiYrZ(OkzK8;X)K|=-r$ng zXvBL%?jGnD23?{}%{VPj6WZ zvZL|gc(i)`XE$D7Xh~1dJ<$Jvq6^+M%moov`_w{wwhz%~(vh9Px<#*w_9xSa+h;|g z!i_42;F)XJA2Zt5CfdKa`^6J<5A>TA9P(?E#>I(VfOC5@?HQyC!O2aD5AVB{n9&{_ z9_W8g(9cPf-uU2>ZQ15_j;woZc%XlepkFQQZ3!dWrZbt(hTZefJ}_dx%qpy$T>E>(YW>^jHcb025F2KMYu-x;}B*_|WE<#-pe zgP+VIXD^%c!0X;P zBKKhY{{rvj)UX{-p8q=hFXs|^yScq~*+wI66JF$odldE1{64stvfr$y)e{hzux|

A2l{!Sm)7uMQ4GS47sjj@zz`6l7#L#2gq+0- zHt%wv2i5-7E^G@P=F*>G_T$7PDX*}t$? zEzq-NxP>C*Nt$7V8VoQ8kHWtZ>=-kk)UPp0VC`s-wc9giR1bLFka3(ni%@hzKlj|&gBYM3(}c5;3GepJLmD=0li;VJ)4Y=RBmugd8pQRbbn^)$7nEz z?~}wPvkzzi+xiHdXj(}t!@mW(zi38MuG)!@EW8b(oP)|^gapG+%y%QE^SDKjPhNMn zL9#LEg|Hu&2<)FS9PFKSU;|hr$a-DlfdD$8*cH&f1ol;E$(P}nOQ-9gQ(b|MQFSO8 zFmNDFcZQdSlB*aI4}A4bqo+R7W{a{W*FqN9>^<;VzA&Opca07xqu!zDMOGNpO7SHN zus7f3((3;%a$loUtE}*lf*<}k&9;7L@9cWoIMz7+S;oDJv>nX_I&%}1*=2W)fhZd% zN;t$0#y&9=?2thG2E`;q#+U#^K_ti+B@$7EsC<)3NR^WBcAjTuZLh(hIgP1$+F2!l z?45nHfAhS|%=5n$1Rb9#ykO_$z6TxB*+f1)vkahLdnR`=->9#`j$zz<&C^Gk4GsG5 zfWA3x1}u7f8p|t%qsQlLgk7tAH_X@m1re=NtvNejFx_O;O)WwQoxVHi?%(j*pj$QP zTtl3)g{1g#+~=|U(sRT;`@Ue z-PL9#=!#{UJCtTRbJi`q-U6K{(C>3#{GkfhC)KYO=(UfJ>nDO?je1L3XFA7ad3d(P zcL(204^Rp%e9-Bnd(dUkd2-cFhoJE-il@5vV1chZ82izn6K-;>mWt6ZJg2GrN{41| zPCZkDJ(%L<2V=2L4-j#kYMz%3h$23%a_CYf#x|X?G1z5}m#2c|x%8$%zXOvK2#`^3zj3G&=olJy^Uqb%gAiA5-(V+&* z;=Ucu!zE`4D`^_Wy>WpRFfmV%rUZ5o|F+ESXvNfYXwWasy32ZREsLI-Egk!y7dUr1 zd9H}NM$Fowl4Qc_>9VFA)^$hSRC7aS#TA*f248V=xT|qX<|WOr@e=sk5obIS1RW)n zT3PkzVJC?Xk#u^dSW68Avjqa&`VWK*^JdO@+5>blndAI02jRM`F3m2p=m5S$9hAB>28>84@cHZsn$2_PNVJpiG{M(^@y%&*5p?&= zR-m)_JWqFlZa;FJ==3X1hX(!97+r2{i#}d(e}ou4Q?ZB{1fAUHPcoHP3Uo8hTcA%1 zfbb)@<{e)owYz`BoaNl zJ9w%=*Pwq5=;1c#*2W=(y&Leq3v@gGxq4y}Vs56T%-SOArAVa~v$Rq@nI#>IZqzy{ zdsQQ*;GQYzrxLpE(6|@qd6k3r=J*Gb1VArh)_u~SJEN>M@(b>L9OLt>%+R+>Qu&~p zvNyu63}BjEHtq#FO1;SdJ?Z(7A?QqLw>*;7 zU<-0UF6)l6a7jwO9y5}V^&`R9$&%CzL5CX|zRTv_J&P#W7#!=W%3= zg_rll9e*S)9*pNne1*_#CW$j$_DFFVC7cUUImxczzQg!@ABWjlE9>4Z!CZ)z4ZDVl zZ|qHZ`M{PAe`UZ3QM2TX8pbtB?|m9FjQiMUw5HCT!aH%kZ(IN!@AvySFB{+^P&y)A z_vg+MSaj$-J(iHUN$8oEG$R`HFM@8b=j*OhuP;i_i!NI8VeaIVf{JhQYp}!P&nB^7 z^SYag5mL-Jb-rSvE(^Sa!(Fyn;&u40iRbVNm@Q1VtEn##jMj7Df{PK=xCiK-w9kY= z1X_pf)^;9Rr0XjdAO*FVw%9=~1(`yX-BQB^^fyxa@8`QeGE1J7j?RUvpf$7fKj^|a!t z$Q{qD1X_L2jZ#GfExP*zp5Q)}mRu=3g7t=b8=~1|I59-+5jcLGpLG_$gtAe5rXGNO z4)jnV%5#-k*&ujlhiK7_0_>c7i*hyf44AWiInldbzW$Pc^3GIu|m( zT8$B<-7HtcnDWeON_?R?io8SM99+M)-CTSImn{KbPWlVlo~<^1FetU)X^axe)}O(R zbj^_8#%GGjxngt8?wT_<4CEx880 zAM4R^jHoSm+Pz7aTZ$-u<7S_xVqbWx&lOa9SE?|dJEOn9upOru^e8TAKIfyoSFzu` zkq^#|HTqaxGH^$_#*7BNFNG%x(PX0;P4*bX+0LgLjeP3cRh!IWZS7AqOc!cU6>Cqw zrhrAF-b)b%8~4N&dVu5%yP7Ig8VC(~zd=Va)t`^7Uhjy7Cr?hU_v7~d^hH?Ruv%YL zh=%mu&HPerNo&yhmhx1WaIUSm2EG5Kq->Nh?M(~)e{~&MpR35yUIpR}%26Vue1 z0BHkprqgO}Ak)zU&pq(gylvS(SpFfUu*|;iC4A{K-!35vbO*2O*SG7K-1%&P&K=#& zbNqG8#m)x+kNp+`fY-?u<#qdRe3@6Dajdk*~`=u=B z2K@=>&OM#$2G7ue-VXh)0XlbdcTVxw?h8BP_8I!!JD{`C9lNl%T4&Zk?*)CELq7w3 z%tQP%CwM09DfI17=qI2rW3Sg*XULvHKMmgKr-%J!6G!5Wncu&Div9t8w5>kwZ(6J0 zxiMtFpa($`#hcCkp#!@0#!L~qd^OJe{|$c`?1)SKBcIJNVBet2pQBfxSDaUVzP7dB zfi?dl(;o)w>LI@P&(iSoRB!%(l+0)A4cKkVtL}}zQ&pGr%Y{FmMcmg}=>8>Pz7!w2 z-v~Wmnjb2Cp=LpSfn z$(7C^CzXD|ISuPoEcEQXxSr6>)3|J=2&oMWqB za8A9U%Y|M#xpzS~-=t?Nok1>Wtur?tq1y{R`xdJ)6GK1U(OlaryDLU@U5yp>SSa==IQ7 zRp@G^tJC`-ijpu4g5XhhKA)e6X!6b?VKBuQhDj71;-k2r=L@|EeYI*oyIY5MC0$V$ z){S-|nxR0IGEbs|_+o9dHwSmQ(A!S#<+HnaX(rq0n!2zq3c`ShCQs561QW5%53zKv z_IdG!-VQzgD&4GfDcw*{tqbi*2A+r}P4Y{Tr^+?+g7eas=vC;oD_u!f$9HL6U{~CU zXr3g(I2M<4b!^X^cQ5o3bW3`^)TMQyU3({@$%|!2sobn{={!5Q_ko_TbS>R1bzxm$ zH{gkAmK4IhP%hWGdJZR@7Z-XR^osO#CS6k3)-CNsG<|z%Tr0QMx%I@o&AAQT+Ue@} zo>CXq1@^B7JQ2;1M);tVr_8JSybpBinr@^U>I%CBo`@#S7?dD2bIZBC(CeX>5ARaC zp)Rd!?FxK`o`|ODlmXYsrE+s@FFRkY+M#FC%|%^X*Vu{3g+6^U%+2+@*FG;@)AOZn zTL(Om8^A5)_B!ug=*5+uNzbVlxf9WhX_1O~)p;BA8tKKYPDC!1Z}TC}8>I{CYq%4+ zM0{;+^I^`*(lvEyJ?BnDv!_vhv6=UFo=Z2>jrE*65jjvU^S;Eqhw~hIjdZ{hIRw1P zc^~LD^~`#Wb|RX+Ttyh6RQ)``fCcJE+b zm5z5Jr-k3>fpDM`0iB3|PDD$BPDDgRL_|bHL_|bHL`1Ux XNT^7b-J#rq00000NkvXXu0mjfhVn_< literal 0 HcmV?d00001 diff --git a/application/application.go b/application/application.go new file mode 100644 index 0000000..f33d170 --- /dev/null +++ b/application/application.go @@ -0,0 +1,192 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package application + +import ( + "fmt" + "io" + goLog "log" + "os" + "os/signal" + "sync" + "syscall" + + "github.com/nirui/sshwifty/application/command" + "github.com/nirui/sshwifty/application/configuration" + "github.com/nirui/sshwifty/application/log" + "github.com/nirui/sshwifty/application/server" +) + +// ProccessSignaller send signal to the running application +type ProccessSignaller chan os.Signal + +// ProccessSignallerBuilder builds a ProccessSignaler +type ProccessSignallerBuilder func() chan os.Signal + +// DefaultProccessSignallerBuilder the default ProccessSignallerBuilder +func DefaultProccessSignallerBuilder() chan os.Signal { + return make(chan os.Signal, 1) +} + +var ( + screenLineWipper = []byte("\r") +) + +// Application contains data required for the application, and yes I don't like +// to write comments +type Application struct { + screen io.Writer + logger log.Logger +} + +// New creates a new Application +func New(screen io.Writer, logger log.Logger) Application { + return Application{ + screen: screen, + logger: logger, + } +} + +// Run execute the application. It will return when the application is finished +// running +func (a Application) run( + cLoader configuration.Loader, + closeSigBuilder ProccessSignallerBuilder, + commands command.Commands, + handlerBuilder server.HandlerBuilderBuilder, +) (bool, error) { + var err error + + loaderName, c, cErr := cLoader(a.logger.Context("Configuration")) + + if cErr != nil { + a.logger.Error("\"%s\" loader cannot load configuration: %s", + loaderName, cErr) + + return false, cErr + } + + // Allowing command to alter presets + c.Presets, err = commands.Reconfigure(c.Presets) + + if err != nil { + a.logger.Error("Unable to reconfigure presets: %s", err) + + return false, err + } + + // Verify all configuration + err = c.Verify() + + if err != nil { + a.logger.Error("Configuration was invalid: %s", err) + + return false, err + } + + closeNotify := closeSigBuilder() + closeNotifyDisableLock := sync.Mutex{} + signal.Notify(closeNotify, os.Kill, os.Interrupt, syscall.SIGHUP) + defer func() { + closeNotifyDisableLock.Lock() + defer closeNotifyDisableLock.Unlock() + if closeNotify == nil { + return + } + signal.Stop(closeNotify) + close(closeNotify) + closeNotify = nil + }() + + servers := make([]*server.Serving, 0, len(c.Servers)) + s := server.New(a.logger) + + defer func() { + for i := len(servers); i > 0; i-- { + servers[i-1].Close() + } + s.Wait() + }() + + for _, ss := range c.Servers { + newServer := s.Serve(c.Common(), ss, func(e error) { + closeNotifyDisableLock.Lock() + defer closeNotifyDisableLock.Unlock() + if closeNotify == nil { + return + } + err = e + signal.Stop(closeNotify) + close(closeNotify) + closeNotify = nil + }, handlerBuilder(commands)) + servers = append(servers, newServer) + } + + switch <-closeNotify { + case syscall.SIGHUP: + return true, nil + case syscall.SIGTERM: + fallthrough + case os.Kill: + fallthrough + case os.Interrupt: + a.screen.Write(screenLineWipper) + return false, nil + default: + closeNotifyDisableLock.Lock() + defer closeNotifyDisableLock.Unlock() + return false, err + } +} + +// Run execute the application. It will return when the application is finished +// running +func (a Application) Run( + cLoader configuration.Loader, + closeSigBuilder ProccessSignallerBuilder, + commands command.Commands, + handlerBuilder server.HandlerBuilderBuilder, +) error { + fmt.Fprintf(a.screen, banner, FullName, version, Author, URL) + + goLog.SetOutput(a.logger) + defer goLog.SetOutput(os.Stderr) + + a.logger.Info("Initializing") + defer a.logger.Info("Closed") + + for { + restart, runErr := a.run( + cLoader, closeSigBuilder, commands, handlerBuilder) + + if runErr != nil { + a.logger.Error("Unable to start due to error: %s", runErr) + + return runErr + } + + if restart { + a.logger.Info("Restarting") + + continue + } + + return nil + } +} diff --git a/application/command/commander.go b/application/command/commander.go new file mode 100644 index 0000000..1c4b819 --- /dev/null +++ b/application/command/commander.go @@ -0,0 +1,68 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package command + +import ( + "io" + "sync" + "time" + + "github.com/nirui/sshwifty/application/log" + "github.com/nirui/sshwifty/application/network" + "github.com/nirui/sshwifty/application/rw" +) + +// Configuration contains configuration data needed to run command +type Configuration struct { + Dial network.Dial + DialTimeout time.Duration +} + +// Commander command control +type Commander struct { + commands Commands +} + +// New creates a new Commander +func New(cs Commands) Commander { + return Commander{ + commands: cs, + } +} + +// New Adds a new client +func (c Commander) New( + cfg Configuration, + receiver rw.FetchReader, + sender io.Writer, + senderLock *sync.Mutex, + receiveDelay time.Duration, + sendDelay time.Duration, + l log.Logger, +) (Handler, error) { + return newHandler( + cfg, + &c.commands, + receiver, + sender, + senderLock, + receiveDelay, + sendDelay, + l, + ), nil +} diff --git a/application/command/commands.go b/application/command/commands.go new file mode 100644 index 0000000..cf87251 --- /dev/null +++ b/application/command/commands.go @@ -0,0 +1,128 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package command + +import ( + "errors" + "fmt" + + "github.com/nirui/sshwifty/application/configuration" + "github.com/nirui/sshwifty/application/log" +) + +// Consts +const ( + MaxCommandID = 0x0f +) + +// Errors +var ( + ErrCommandRunUndefinedCommand = errors.New( + "undefined Command") +) + +// Command represents a command handler machine builder +type Command func( + l log.Logger, + w StreamResponder, + cfg Configuration, +) FSMMachine + +// Builder builds a command +type Builder struct { + name string + command Command + configurator configuration.PresetReloader +} + +// Register builds a Builder for registration +func Register(name string, c Command, p configuration.PresetReloader) Builder { + return Builder{ + name: name, + command: c, + configurator: p, + } +} + +// Commands contains data of all commands +type Commands [MaxCommandID + 1]Builder + +// Register registers a new command +func (c *Commands) Register( + id byte, + name string, + cb Command, + ps configuration.PresetReloader, +) { + if id > MaxCommandID { + panic("Command ID must be not greater than MaxCommandID") + } + + if (*c)[id].command != nil { + panic(fmt.Sprintf("Command %d already been registered", id)) + } + + (*c)[id] = Register(name, cb, ps) +} + +// Run creates command executer +func (c Commands) Run( + id byte, + l log.Logger, + w StreamResponder, + cfg Configuration, +) (FSM, error) { + if id > MaxCommandID { + return FSM{}, ErrCommandRunUndefinedCommand + } + + cc := c[id] + + if cc.command == nil { + return FSM{}, ErrCommandRunUndefinedCommand + } + + return newFSM(cc.command(l, w, cfg)), nil +} + +// Reconfigure lets commands reset configuration +func (c Commands) Reconfigure( + p []configuration.Preset, +) ([]configuration.Preset, error) { + newP := make([]configuration.Preset, 0, len(p)) + + for i := range c { + for pp := range p { + if c[i].name != p[pp].Type { + continue + } + + newPP, pErr := c[i].configurator(p[pp]) + + if pErr == nil { + newP = append(newP, newPP) + + continue + } + + return nil, pErr + } + } + + return newP, nil +} diff --git a/application/command/fsm.go b/application/command/fsm.go new file mode 100644 index 0000000..c18bdb1 --- /dev/null +++ b/application/command/fsm.go @@ -0,0 +1,176 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package command + +import ( + "errors" + + "github.com/nirui/sshwifty/application/rw" +) + +// Errors +var ( + ErrFSMMachineClosed = errors.New( + "FSM Machine is already closed, it cannot do anything but be released") +) + +// FSMError Represents an error from FSM +type FSMError struct { + code StreamError + message string + succeed bool +} + +// ToFSMError converts error to FSMError +func ToFSMError(e error, c StreamError) FSMError { + return FSMError{ + code: c, + message: e.Error(), + succeed: false, + } +} + +// NoFSMError return a FSMError that represents a success operation +func NoFSMError() FSMError { + return FSMError{ + code: 0, + message: "No error", + succeed: true, + } +} + +// Error return the error message +func (e FSMError) Error() string { + return e.message +} + +// Code return the error code +func (e FSMError) Code() StreamError { + return e.code +} + +// Succeed returns whether or not current error represents a succeed operation +func (e FSMError) Succeed() bool { + return e.succeed +} + +// FSMState represents a state of a machine +type FSMState func(f *FSM, r *rw.LimitedReader, h StreamHeader, b []byte) error + +// FSMMachine State machine +type FSMMachine interface { + // Bootup boots up the machine + Bootup(r *rw.LimitedReader, b []byte) (FSMState, FSMError) + + // Close stops the machine and get it ready for release. + // + // NOTE: Close function is responsible in making sure the HeaderClose signal + // is sent before it returns. + // (It may not need to send the header by itself, but it have to + // make sure the header is sent) + Close() error + + // Release shuts the machine down completely and release it's resources + Release() error +} + +// FSM state machine control +type FSM struct { + m FSMMachine + s FSMState + closed bool +} + +// newFSM creates a new FSM +func newFSM(m FSMMachine) FSM { + return FSM{ + m: m, + s: nil, + closed: false, + } +} + +// emptyFSM creates a empty FSM +func emptyFSM() FSM { + return FSM{ + m: nil, + s: nil, + } +} + +// bootup initialize the machine +func (f *FSM) bootup(r *rw.LimitedReader, b []byte) FSMError { + s, err := f.m.Bootup(r, b) + + if s == nil { + panic("FSMState must not be nil") + } + + if !err.Succeed() { + return err + } + + f.s = s + + return err +} + +// running returns whether or not current FSM is running +func (f *FSM) running() bool { + return f.s != nil +} + +// tick ticks current machine +func (f *FSM) tick(r *rw.LimitedReader, h StreamHeader, b []byte) error { + if f.closed { + return ErrFSMMachineClosed + } + + return f.s(f, r, h, b) +} + +// Release shuts down current machine and release it's resource +func (f *FSM) release() error { + f.s = nil + + if !f.closed { + f.close() + } + + rErr := f.m.Release() + + f.m = nil + + if rErr != nil { + return rErr + } + + return nil +} + +// Close stops the machine and get it ready to release +func (f *FSM) close() error { + f.closed = true + + return f.m.Close() +} + +// Switch switch to specificied State for the next tick +func (f *FSM) Switch(s FSMState) { + f.s = s +} diff --git a/application/command/handler.go b/application/command/handler.go new file mode 100644 index 0000000..d867d55 --- /dev/null +++ b/application/command/handler.go @@ -0,0 +1,369 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package command + +import ( + "errors" + "fmt" + "io" + "sync" + "time" + + "github.com/nirui/sshwifty/application/log" + "github.com/nirui/sshwifty/application/rw" +) + +// Errors +var ( + ErrHandlerUnknownHeaderType = errors.New( + "unknown command header type") + + ErrHandlerControlMessageTooLong = errors.New( + "control message was too long") + + ErrHandlerInvalidControlMessage = errors.New( + "invalid control message") +) + +// HandlerCancelSignal signals the cancel of the entire handling proccess +type HandlerCancelSignal chan struct{} + +const ( + handlerReadBufLen = HeaderMaxData + 3 // (3 = 1 Header, 2 Etc) +) + +type handlerBuf [handlerReadBufLen]byte + +// handlerSender writes handler signal +type handlerSender struct { + writer io.Writer + lock *sync.Mutex + needWait bool + sign *sync.Cond +} + +// pause pauses sending +func (h *handlerSender) pause() { + h.lock.Lock() + defer h.lock.Unlock() + + h.needWait = true +} + +// resume resumes sending +func (h *handlerSender) resume() { + h.lock.Lock() + defer h.lock.Unlock() + + h.needWait = false + h.sign.Broadcast() +} + +// signal sends handler signal +func (h *handlerSender) signal(hd Header, d []byte, buf []byte) error { + bufLen := len(buf) + dLen := len(d) + + if bufLen < dLen+1 { + panic(fmt.Sprintf("Sending signal %s:%d requires %d bytes of buffer, "+ + "but only %d bytes is available", hd, d, dLen+1, bufLen)) + } + + buf[0] = byte(hd) + + wLen := copy(buf[1:], d) + 1 + + _, wErr := h.Write(buf[:wLen]) + + return wErr +} + +// Write sends data +func (h *handlerSender) Write(b []byte) (int, error) { + h.lock.Lock() + defer h.lock.Unlock() + + for h.needWait { + h.sign.Wait() + } + + return h.writer.Write(b) +} + +// streamHandlerSender includes all receiver as handlerSender, but it been +// designed to be use in streams +type streamHandlerSender struct { + *handlerSender + + sendDelay time.Duration +} + +// Write sends data +func (h streamHandlerSender) Write(b []byte) (int, error) { + defer time.Sleep(h.sendDelay) + + return h.handlerSender.Write(b) +} + +// Handler client stream control +type Handler struct { + cfg Configuration + commands *Commands + receiver rw.FetchReader + sender handlerSender + senderPaused bool + receiveDelay time.Duration + sendDelay time.Duration + log log.Logger + rBuf handlerBuf + streams streams +} + +func newHandler( + cfg Configuration, + commands *Commands, + receiver rw.FetchReader, + sender io.Writer, + senderLock *sync.Mutex, + receiveDelay time.Duration, + sendDelay time.Duration, + l log.Logger, +) Handler { + return Handler{ + cfg: cfg, + commands: commands, + receiver: receiver, + sender: handlerSender{ + writer: sender, + lock: senderLock, + needWait: false, + sign: sync.NewCond(senderLock), + }, + senderPaused: false, + receiveDelay: receiveDelay, + sendDelay: sendDelay, + log: l, + rBuf: handlerBuf{}, + streams: newStreams(), + } +} + +// handleControl handles Control request +// +// Params: +// - d: length of the control message +// +// Returns: +// - error +func (e *Handler) handleControl(d byte, l log.Logger) error { + buf := e.rBuf[1:] + + if len(buf) < int(d) { + return ErrHandlerControlMessageTooLong + } + + rLen, rErr := io.ReadFull(&e.receiver, buf[:d]) + + if rErr != nil { + return rErr + } + + if rLen <= 0 { + return ErrHandlerInvalidControlMessage + } + + switch buf[0] { + case HeaderControlEcho: + l.Debug("Echo %d bytes", d) + + hd := HeaderControl + hd.Set(d) + + e.rBuf[0] = byte(hd) + e.rBuf[1] = HeaderControlEcho + + var wErr error + + if !e.senderPaused { + _, wErr = e.sender.Write(e.rBuf[:rLen+1]) + } else { + e.sender.lock.Lock() + defer e.sender.lock.Unlock() + + _, wErr = e.sender.writer.Write(e.rBuf[:rLen+1]) + } + + return wErr + + case HeaderControlPauseStream: + if !e.senderPaused { + e.sender.pause() + e.senderPaused = true + + l.Debug("Pause Stream") + } else { + l.Debug("Repeated Pause Stream command, ignore") + } + + case HeaderControlResumeStream: + if e.senderPaused { + e.sender.resume() + e.senderPaused = false + + l.Debug("Resume Stream") + } else { + l.Debug("Repeated Resume Stream command, ignore") + } + } + + return nil +} + +// handleStream handles streams +// +// Params: +// - d: Stream ID +// +// Returns: +// - error +func (e *Handler) handleStream(h Header, d byte, l log.Logger) error { + st, stErr := e.streams.get(d) + + if stErr != nil { + return stErr + } + + // WARNING: stream.Tick and it's underlaying commands MUST NOT write to + // client. This is because the client data writer maybe locked + // and only current routine (the same routine will be used to + // tick the stream) can unlock it. + // Calling write may dead lock the routine, with there is no way + // of recover. + if st.running() { + l.Debug("Ticking stream") + + return st.tick(h, &e.receiver, e.rBuf[:]) + } + + l.Debug("Start stream %d", h.Data()) + + if e.senderPaused { + e.sender.resume() + defer e.sender.pause() + } + + return st.reinit(h, &e.receiver, streamHandlerSender{ + handlerSender: &e.sender, + sendDelay: e.sendDelay, + }, l, e.commands, e.cfg, e.rBuf[:]) +} + +func (e *Handler) handleClose(h Header, d byte, l log.Logger) error { + st, stErr := e.streams.get(d) + + if stErr != nil { + return stErr + } + + if e.senderPaused { + e.sender.resume() + defer e.sender.pause() + } + + cErr := st.close() + + if cErr != nil { + return cErr + } + + hhd := HeaderCompleted + hhd.Set(h.Data()) + + return e.sender.signal(hhd, nil, e.rBuf[:]) +} + +func (e *Handler) handleCompleted(d byte, l log.Logger) error { + st, stErr := e.streams.get(d) + + if stErr != nil { + return stErr + } + + if e.senderPaused { + e.sender.resume() + defer e.sender.pause() + } + + return st.release() +} + +// Handle starts handling +func (e *Handler) Handle() error { + defer func() { + if e.senderPaused { + e.sender.resume() + e.senderPaused = false + } + + e.streams.shutdown() + }() + + requests := 0 + + for { + time.Sleep(e.receiveDelay) + + requests++ + + d, dErr := rw.FetchOneByte(e.receiver.Fetch) + + if dErr != nil { + return dErr + } + + h := Header(d[0]) + l := e.log.Context("Request (%d)", requests).Context(h.String()) + + l.Debug("Received") + + switch h.Type() { + case HeaderControl: + dErr = e.handleControl(h.Data(), l) + + case HeaderStream: + dErr = e.handleStream(h, h.Data(), l) + + case HeaderClose: + dErr = e.handleClose(h, h.Data(), l) + + case HeaderCompleted: + dErr = e.handleCompleted(h.Data(), l) + + default: + return ErrHandlerUnknownHeaderType + } + + if dErr != nil { + l.Debug("Request failed: %s", dErr) + + return dErr + } + + l.Debug("Request successful") + } +} diff --git a/application/command/handler_echo_test.go b/application/command/handler_echo_test.go new file mode 100644 index 0000000..c78f4ff --- /dev/null +++ b/application/command/handler_echo_test.go @@ -0,0 +1,104 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package command + +import ( + "bytes" + "io" + "sync" + "testing" + + "github.com/nirui/sshwifty/application/log" + "github.com/nirui/sshwifty/application/rw" +) + +func testDummyFetchGen(data []byte) rw.FetchReaderFetcher { + current := 0 + + return func() ([]byte, error) { + if current >= len(data) { + return nil, io.EOF + } + + oldCurrent := current + current++ + + return data[oldCurrent:current], nil + } +} + +type dummyWriter struct { + written []byte +} + +func (d *dummyWriter) Write(b []byte) (int, error) { + d.written = append(d.written, b...) + + return len(b), nil +} + +func TestHandlerHandleEcho(t *testing.T) { + w := dummyWriter{ + written: make([]byte, 0, 64), + } + s := []byte{ + byte(HeaderControl | 13), + HeaderControlEcho, + 'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D', '1', + byte(HeaderControl | 13), + HeaderControlEcho, + 'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D', '2', + byte(HeaderControl | HeaderMaxData), + HeaderControlEcho, + '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', + '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', + '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', + '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', + '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', + '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', + '2', '2', + byte(HeaderControl | 13), + HeaderControlEcho, + 'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D', '3', + } + lock := sync.Mutex{} + handler := newHandler( + Configuration{}, + nil, + rw.NewFetchReader(testDummyFetchGen(s)), + &w, + &lock, + 0, + 0, + log.NewDitch(), + ) + + hErr := handler.Handle() + + if hErr != nil && hErr != io.EOF { + t.Error("Failed to write due to error:", hErr) + + return + } + + if !bytes.Equal(w.written, s) { + t.Errorf("Expecting the data to be %d, got %d instead", s, w.written) + + return + } +} diff --git a/application/command/handler_stream_test.go b/application/command/handler_stream_test.go new file mode 100644 index 0000000..5ff8c5c --- /dev/null +++ b/application/command/handler_stream_test.go @@ -0,0 +1,262 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package command + +import ( + "bytes" + "fmt" + "io" + "sync" + "testing" + + "github.com/nirui/sshwifty/application/log" + "github.com/nirui/sshwifty/application/rw" +) + +func testDummyFetchChainGen(dd <-chan []byte) rw.FetchReaderFetcher { + var data []byte + var ok bool + + current := 0 + + return func() ([]byte, error) { + for { + if current >= len(data) { + data, ok = <-dd + + if !ok { + return nil, io.EOF + } + + current = 0 + + continue + } + + oldCurrent := current + current++ + + return data[oldCurrent:current], nil + } + } +} + +type dummyStreamCommand struct { + lock sync.Mutex + l log.Logger + w StreamResponder + downWait sync.WaitGroup + echoData []byte + echoTrans chan []byte +} + +func newDummyStreamCommand( + l log.Logger, + w StreamResponder, + cfg Configuration, +) FSMMachine { + return &dummyStreamCommand{ + lock: sync.Mutex{}, + l: l, + w: w, + downWait: sync.WaitGroup{}, + echoData: []byte{}, + echoTrans: make(chan []byte), + } +} + +func (d *dummyStreamCommand) Bootup( + r *rw.LimitedReader, + b []byte, +) (FSMState, FSMError) { + d.downWait.Add(1) + + echoTrans := d.echoTrans + + go func() { + defer func() { + d.w.Signal(HeaderClose) + + d.downWait.Done() + }() + + buf := make([]byte, 1024) + + for { + dt, dtOK := <-echoTrans + + if !dtOK { + return + } + + wErr := d.w.Send(0, []byte{dt[0], dt[1], dt[2], dt[3]}, buf) + + if wErr != nil { + return + } + } + }() + + commandDataBuf := [5]byte{} + + _, rErr := io.ReadFull(r, commandDataBuf[:]) + + if rErr != nil { + return nil, ToFSMError(rErr, 11) + } + + if !bytes.Equal(commandDataBuf[:], []byte("HELLO")) { + panic(fmt.Sprintf("Expecting handsake data to be %s, got %s instead", + []byte("HELLO"), commandDataBuf[:])) + } + + if !r.Completed() { + panic("R must be Completed") + } + + return d.run, NoFSMError() +} + +func (d *dummyStreamCommand) run( + f *FSM, r *rw.LimitedReader, h StreamHeader, b []byte) error { + rLen, rErr := rw.ReadUntilCompleted(r, b[:]) + + if rErr != nil { + return rErr + } + + d.echoData = make([]byte, rLen) + copy(d.echoData, b) + + d.lock.Lock() + defer d.lock.Unlock() + + if d.echoTrans != nil { + d.echoTrans <- d.echoData + } + + return nil +} + +func (d *dummyStreamCommand) Close() error { + close(d.echoTrans) + + d.lock.Lock() + d.echoTrans = nil + d.lock.Unlock() + + d.downWait.Wait() + + return nil +} + +func (d *dummyStreamCommand) Release() error { + return nil +} + +func TestHandlerHandleStream(t *testing.T) { + cmds := Commands{} + cmds.Register(0, "name", newDummyStreamCommand, nil) + + readerDataInput := make(chan []byte) + + readerSource := testDummyFetchChainGen(readerDataInput) + wBuffer := bytes.NewBuffer(make([]byte, 0, 1024)) + + lock := sync.Mutex{} + hhd := newHandler( + Configuration{}, + &cmds, + rw.NewFetchReader(readerSource), + wBuffer, + &lock, + 0, + 0, + log.NewDitch()) + + go func() { + stInitialHeader := streamInitialHeader{} + + stInitialHeader.set(0, 5, true) + + readerDataInput <- []byte{ + byte(HeaderStream | 63), stInitialHeader[0], stInitialHeader[1], + 'H', 'E', 'L', 'L', 'O', + } + + stHeader := StreamHeader{} + stHeader.Set(0, 5) + + readerDataInput <- []byte{ + byte(HeaderStream | 63), stHeader[0], stHeader[1], + 'W', 'O', 'R', 'L', 'D', + } + + readerDataInput <- []byte{ + byte(HeaderStream | 63), stHeader[0], stHeader[1], + '0', '1', '2', '3', '4', + } + + readerDataInput <- []byte{ + byte(HeaderClose | 63), + } + + close(readerDataInput) + }() + + hErr := hhd.Handle() + + if hErr != nil && hErr != io.EOF { + t.Error("Failed to handle due to error:", hErr) + + return + } + + // Build the expected header: + + // HeaderStream(63): Success + stInitialHeader := streamInitialHeader{} + stInitialHeader.set(0, 0, true) + + stHeaders := StreamHeader{} + stHeaders.Set(0, 4) + + expected := []byte{ + // HeaderStream(63): Success + byte(HeaderStream | 63), stInitialHeader[0], stInitialHeader[1], + + // HeaderStream(63): Echo 'W', 'O', 'R', 'L' (First 4 bytes of data) + byte(HeaderStream | 63), stHeaders[0], stHeaders[1], 'W', 'O', 'R', 'L', + + // HeaderStream(63): Echo '0', '1', '2', '3', + byte(HeaderStream | 63), stHeaders[0], stHeaders[1], '0', '1', '2', '3', + + // HeaderClose(63) + byte(HeaderClose | 63), + + // HeaderCompleted(63) + byte(HeaderCompleted | 63), + } + + if !bytes.Equal(wBuffer.Bytes(), expected) { + t.Errorf("Expecting received data to be %d, got %d instead", + expected, wBuffer.Bytes()) + + return + } +} diff --git a/application/command/handler_test.go b/application/command/handler_test.go new file mode 100644 index 0000000..c9ed7d8 --- /dev/null +++ b/application/command/handler_test.go @@ -0,0 +1,18 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package command diff --git a/application/command/header.go b/application/command/header.go new file mode 100644 index 0000000..3f63f95 --- /dev/null +++ b/application/command/header.go @@ -0,0 +1,143 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package command + +import "fmt" + +// Header Packet Type +type Header byte + +// Packet Types +const ( + // 00------: Control signals + // Remaing bits: Data length + // + // Format: + // 0011111 [63 bytes long data] - 63 bytes of control data + // + HeaderControl Header = 0x00 + + // 01------: Bidirectional stream data + // Remaining bits: Stream ID + // Followed by: Parameter or data + // + // Format: + // 0111111 [Command parameters / data] - Open/use stream 63 to execute + // command or transmit data + HeaderStream Header = 0x40 + + // 10------: Close stream + // Remaining bits: Stream ID + // + // Format: + // 1011111 - Close stream 63 + // + // WARNING: The requester MUST NOT send any data to this stream once this + // header is sent. + // + // WARNING: The receiver MUST reply with a Completed header to indicate + // the success of the Close action. Until a Completed header is + // replied, all data from the sender must be proccessed as normal. + HeaderClose Header = 0x80 + + // 11------: Stream has been closed/completed in respond to client request + // Remaining bits: Stream ID + // + // Format: + // 1111111 - Stream 63 is completed + // + // WARNING: This header can ONLY be send in respond to a Close header + // + // WARNING: The sender of this header MUST NOT send any data to the stream + // once this header is sent until this stream been re-opened by a + // Data header + HeaderCompleted Header = 0xc0 +) + +// Control signal types +const ( + HeaderControlEcho = 0x00 + HeaderControlPauseStream = 0x01 + HeaderControlResumeStream = 0x02 +) + +// Consts +const ( + HeaderMaxData = 0x3f +) + +// Cutters +const ( + headerHeaderCutter = 0xc0 + headerDataCutter = 0x3f +) + +// Type get packet type +func (p Header) Type() Header { + return (p & headerHeaderCutter) +} + +// Data returns the data of current Packet header +func (p Header) Data() byte { + return byte(p & headerDataCutter) +} + +// Set set a new value of the Header +func (p *Header) Set(data byte) { + if data > headerDataCutter { + panic("data must not be greater than 0x3f") + } + + *p |= (headerDataCutter & Header(data)) +} + +// Set set a new value of the Header +func (p Header) String() string { + switch p.Type() { + case HeaderControl: + return fmt.Sprintf("Control (%d bytes)", p.Data()) + + case HeaderStream: + return fmt.Sprintf("Stream (%d)", p.Data()) + + case HeaderClose: + return fmt.Sprintf("Close (Stream %d)", p.Data()) + + case HeaderCompleted: + return fmt.Sprintf("Completed (Stream %d)", p.Data()) + + default: + return "Unknown" + } +} + +// IsStreamControl returns true when the header is for stream control, false +// when otherwise +func (p Header) IsStreamControl() bool { + switch p { + case HeaderStream: + fallthrough + case HeaderClose: + fallthrough + case HeaderCompleted: + return true + + default: + return false + } +} diff --git a/application/command/streams.go b/application/command/streams.go new file mode 100644 index 0000000..d8858af --- /dev/null +++ b/application/command/streams.go @@ -0,0 +1,447 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package command + +import ( + "errors" + "io" + + "github.com/nirui/sshwifty/application/log" + "github.com/nirui/sshwifty/application/rw" +) + +// Errors +var ( + ErrStreamsInvalidStreamID = errors.New( + "stream ID is invalid") + + ErrStreamsStreamOperateInactiveStream = errors.New( + "specified stream was inactive for operation") + + ErrStreamsStreamClosingInactiveStream = errors.New( + "closing an inactive stream is not allowed") + + ErrStreamsStreamReleasingInactiveStream = errors.New( + "releasing an inactive stream is not allowed") +) + +// StreamError Stream Error signal +type StreamError uint16 + +// Error signals +const ( + StreamErrorCommandUndefined StreamError = 0x01 + StreamErrorCommandFailedToBootup StreamError = 0x02 +) + +// StreamHeader contains data of the stream header +type StreamHeader [2]byte + +// Stream header consts +const ( + StreamHeaderMaxLength = 0x1fff + StreamHeaderMaxMarker = 0x07 + + streamHeaderLengthFirstByteCutter = 0x1f +) + +// Marker returns the header marker data +func (s StreamHeader) Marker() byte { + return s[0] >> 5 +} + +// Length returns the data length of the stream +func (s StreamHeader) Length() uint16 { + r := uint16(0) + + r |= uint16(s[0] & streamHeaderLengthFirstByteCutter) + r <<= 8 + r |= uint16(s[1]) + + return r +} + +// Set sets the stream header +func (s *StreamHeader) Set(marker byte, n uint16) { + if marker > StreamHeaderMaxMarker { + panic("marker must not be greater than 0x07") + } + + if n > StreamHeaderMaxLength { + panic("n must not be greater than 0x1fff") + } + + s[0] = (marker << 5) | byte((n>>8)&streamHeaderLengthFirstByteCutter) + s[1] = byte(n) +} + +// streamInitialHeader contains header data of the first stream after stream +// reset. +// Unlike StreamHeader, streamInitialHeader carries no extra data +type streamInitialHeader StreamHeader + +// command returns command ID of the stream +func (s streamInitialHeader) command() byte { + return s[0] >> 4 +} + +// length returns the data of the stream header +func (s streamInitialHeader) data() uint16 { + r := uint16(0) + + r |= uint16(s[0] & 0x07) // 0000 0111 + r <<= 8 + r |= uint16(s[1]) + + return r +} + +// success returns whether or not the command is representing a success +func (s streamInitialHeader) success() bool { + return (s[0] & 0x08) != 0 +} + +// set sets header values +func (s *streamInitialHeader) set(commandID byte, data uint16, success bool) { + if commandID > 0x0f { + panic("Command ID must not greater than 0x0f") + } + + if data > 0x07ff { + panic("Data must not greater than 0x07ff") + } + + dd := data & 0x07ff + + if success { + dd |= 0x0800 + } + + (*s)[0] = 0 + (*s)[0] |= commandID << 4 + (*s)[0] |= byte(dd >> 8) + (*s)[1] = 0 + (*s)[1] |= byte(dd) +} + +// send sends current stream header as signal +func (s *streamInitialHeader) signal( + w *handlerSender, + hd Header, + buf []byte, +) error { + return w.signal(hd, (*s)[:], buf) +} + +// StreamInitialSignalSender sends stream initial signal +type StreamInitialSignalSender struct { + w *handlerSender + hd Header + cmdID byte + buf []byte +} + +// Signal send signal +func (s *StreamInitialSignalSender) Signal( + errno StreamError, success bool) error { + shd := streamInitialHeader{} + shd.set(s.cmdID, uint16(errno), success) + + return shd.signal(s.w, s.hd, s.buf) +} + +// StreamResponder sends data through stream +type StreamResponder struct { + w streamHandlerSender + h Header +} + +// newStreamResponder creates a new StreamResponder +func newStreamResponder(w streamHandlerSender, h Header) StreamResponder { + return StreamResponder{ + w: w, + h: h, + } +} + +func (w StreamResponder) write(mk byte, b []byte, buf []byte) (int, error) { + bufLen := len(buf) + bLen := len(b) + + if bLen > bufLen { + bLen = bufLen + } + + if bLen > StreamHeaderMaxLength { + bLen = StreamHeaderMaxLength + } + + sHeaderStream := StreamHeader{} + sHeaderStream.Set(mk, uint16(bLen)) + + toWrite := copy(buf[3:], b) + buf[0] = byte(w.h) + buf[1] = sHeaderStream[0] + buf[2] = sHeaderStream[1] + + _, wErr := w.w.Write(buf[:toWrite+3]) + + if wErr != nil { + return 0, wErr + } + + return len(b), wErr +} + +// HeaderSize returns the size of header +func (w StreamResponder) HeaderSize() int { + return 3 +} + +// Send sends data. Data will be automatically segmentated if it's too long to +// fit into one data package or buffer space +func (w StreamResponder) Send(marker byte, data []byte, buf []byte) error { + if len(buf) <= w.HeaderSize() { + panic("The length of data buffer must be greater than 3") + } + + dataLen := len(data) + start := 0 + + for { + wLen, wErr := w.write(marker, data[start:], buf) + + start += wLen + + if wErr != nil { + return wErr + } + + if start < dataLen { + continue + } + + return nil + } +} + +// SendManual sends the data without automatical segmentation. It will construct +// the data package directly using the given `data` buffer, that is, the first +// n bytes of the given `data` will be used to setup headers. It is the caller's +// +// responsibility to leave n bytes of space so no meaningful data will be over +// +// written. The number n can be acquired by calling .HeaderSize() method. +func (w StreamResponder) SendManual(marker byte, data []byte) error { + dataLen := len(data) + + if dataLen < w.HeaderSize() { + panic("The length of data buffer must be greater than the " + + "w.HeaderSize()") + } + + if dataLen > StreamHeaderMaxLength { + panic("Data length must not greater than StreamHeaderMaxLength") + } + + sHeaderStream := StreamHeader{} + sHeaderStream.Set(marker, uint16(dataLen-w.HeaderSize())) + + data[0] = byte(w.h) + data[1] = sHeaderStream[0] + data[2] = sHeaderStream[1] + + _, wErr := w.w.Write(data) + + return wErr +} + +// Signal sends a signal +func (w StreamResponder) Signal(signal Header) error { + if !signal.IsStreamControl() { + panic("Only stream control signal is allowed") + } + + sHeader := signal + sHeader.Set(w.h.Data()) + + _, wErr := w.w.Write([]byte{byte(sHeader)}) + + return wErr +} + +type stream struct { + f FSM + closed bool +} + +type streams [HeaderMaxData + 1]stream + +func newStream() stream { + return stream{ + f: emptyFSM(), + closed: false, + } +} + +func newStreams() streams { + s := streams{} + + for i := range s { + s[i] = newStream() + } + + return s +} + +func (c *streams) get(id byte) (*stream, error) { + if id > HeaderMaxData { + return nil, ErrStreamsInvalidStreamID + } + + return &(*c)[id], nil +} + +func (c *streams) shutdown() { + cc := *c + + for i := range cc { + if !cc[i].running() { + continue + } + + if !cc[i].closed { + cc[i].close() + } + + cc[i].release() + } +} + +func (c *stream) running() bool { + return c.f.running() +} + +func (c *stream) reinit( + h Header, + r *rw.FetchReader, + w streamHandlerSender, + l log.Logger, + cc *Commands, + cfg Configuration, + b []byte, +) error { + hd := streamInitialHeader{} + + _, rErr := io.ReadFull(r, hd[:]) + + if rErr != nil { + return rErr + } + + l = l.Context("Command (%d)", hd.command()) + + ccc, cccErr := cc.Run( + hd.command(), l, newStreamResponder(w, h), cfg) + + if cccErr != nil { + hd.set(0, uint16(StreamErrorCommandUndefined), false) + hd.signal(w.handlerSender, h, b) + + l.Warning("Trying to execute an unknown command %d", hd.command()) + + return nil + } + + signaller := StreamInitialSignalSender{ + w: w.handlerSender, + hd: h, + cmdID: hd.command(), + buf: b, + } + + rr := rw.NewLimitedReader(r, int(hd.data())) + defer rr.Ditch(b) + + bootErr := ccc.bootup(&rr, b) + + if !bootErr.Succeed() { + l.Warning("Unable to start command %d due to error: %s", + hd.command(), bootErr.Error()) + + signaller.Signal(bootErr.code, false) + + return nil + } + + c.f = ccc + c.closed = false + + sErr := signaller.Signal(bootErr.code, true) + + if sErr != nil { + return sErr + } + + l.Debug("Started") + + return nil +} + +func (c *stream) tick( + h Header, + r *rw.FetchReader, + b []byte, +) error { + if !c.f.running() { + return ErrStreamsStreamOperateInactiveStream + } + + hd := StreamHeader{} + + _, rErr := io.ReadFull(r, hd[:]) + + if rErr != nil { + return rErr + } + + rr := rw.NewLimitedReader(r, int(hd.Length())) + defer rr.Ditch(b) + + return c.f.tick(&rr, hd, b) +} + +func (c *stream) close() error { + if !c.f.running() { + return ErrStreamsStreamClosingInactiveStream + } + + // Set a marker so streams.shutdown won't call it. Stream can call it + // however they want, though that may cause error that disconnects. + c.closed = true + + return c.f.close() +} + +func (c *stream) release() error { + if !c.f.running() { + return ErrStreamsStreamReleasingInactiveStream + } + + return c.f.release() +} diff --git a/application/command/streams_test.go b/application/command/streams_test.go new file mode 100644 index 0000000..de14f6a --- /dev/null +++ b/application/command/streams_test.go @@ -0,0 +1,97 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package command + +import ( + "testing" +) + +func TestStreamInitialHeader(t *testing.T) { + hd := streamInitialHeader{} + + hd.set(15, 128, true) + + if hd.command() != 15 { + t.Errorf("Expecting command to be %d, got %d instead", + 15, hd.command()) + + return + } + + if hd.data() != 128 { + t.Errorf("Expecting data to be %d, got %d instead", 128, hd.data()) + + return + } + + if hd.success() != true { + t.Errorf("Expecting success to be %v, got %v instead", + true, hd.success()) + + return + } + + hd.set(0, 2047, false) + + if hd.command() != 0 { + t.Errorf("Expecting command to be %d, got %d instead", + 0, hd.command()) + + return + } + + if hd.data() != 2047 { + t.Errorf("Expecting data to be %d, got %d instead", 2047, hd.data()) + + return + } + + if hd.success() != false { + t.Errorf("Expecting success to be %v, got %v instead", + false, hd.success()) + + return + } +} + +func TestStreamHeader(t *testing.T) { + s := StreamHeader{} + + s.Set(StreamHeaderMaxMarker, StreamHeaderMaxLength) + + if s.Marker() != StreamHeaderMaxMarker { + t.Errorf("Expecting the marker to be %d, got %d instead", + StreamHeaderMaxMarker, s.Marker()) + + return + } + + if s.Length() != StreamHeaderMaxLength { + t.Errorf("Expecting the length to be %d, got %d instead", + StreamHeaderMaxLength, s.Length()) + + return + } + + if s[0] != s[1] || s[0] != 0xff { + t.Errorf("Expecting the header to be 255, 255, got %d, %d instead", + s[0], s[1]) + + return + } +} diff --git a/application/commands/address.go b/application/commands/address.go new file mode 100644 index 0000000..d15f703 --- /dev/null +++ b/application/commands/address.go @@ -0,0 +1,275 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package commands + +import ( + "errors" + "net" + "strconv" + + "github.com/nirui/sshwifty/application/rw" +) + +// Errors +var ( + ErrAddressParseBufferTooSmallForHeader = errors.New( + "buffer space was too small to parse the address header") + + ErrAddressParseBufferTooSmallForIPv4 = errors.New( + "buffer space was too small to parse the IPv4 address") + + ErrAddressParseBufferTooSmallForIPv6 = errors.New( + "buffer space was too small to parse the IPv6 address") + + ErrAddressParseBufferTooSmallForHostName = errors.New( + "buffer space was too small to parse the hostname address") + + ErrAddressMarshalBufferTooSmall = errors.New( + "buffer space was too small to marshal the address") + + ErrAddressInvalidAddressType = errors.New( + "invalid address type") +) + +// AddressType Type of the address +type AddressType byte + +// Address types +const ( + LoopbackAddr AddressType = 0x00 + IPv4Addr AddressType = 0x01 + IPv6Addr AddressType = 0x02 + HostNameAddr AddressType = 0x03 +) + +// Address data +type Address struct { + port uint16 + kind AddressType + data []byte +} + +// ParseAddress parses the reader and return an Address +// +// Address data format: +// +-------------+--------------+---------------+ +// | 2 bytes | 1 byte | n bytes | +// +-------------+--------------+---------------+ +// | Port number | Address type | Address data | +// +-------------+--------------+---------------+ +// +// Address types: +// - LoopbackAddr: 00 Localhost, don't carry Address data +// - IPv4Addr: 01 IPv4 Address, carries 4 bytes of Address data +// - IPv6Addr: 10 IPv6 Address, carries 16 bytes Address data +// - HostnameAddr: 11 Host name string, length of Address data is indicated +// by the remainer of the byte (11-- ----). maxlen = 63 +func ParseAddress(reader rw.ReaderFunc, buf []byte) (Address, error) { + if len(buf) < 3 { + return Address{}, ErrAddressParseBufferTooSmallForHeader + } + + _, rErr := rw.ReadFull(reader, buf[:3]) + + if rErr != nil { + return Address{}, rErr + } + + portNum := uint16(0) + portNum |= uint16(buf[0]) + portNum <<= 8 + portNum |= uint16(buf[1]) + + addrType := AddressType(buf[2] >> 6) + + var addrData []byte + + switch addrType { + case LoopbackAddr: + // Do nothing + + case IPv4Addr: + if len(buf) < 4 { + return Address{}, ErrAddressParseBufferTooSmallForIPv4 + } + + _, rErr := rw.ReadFull(reader, buf[:4]) + + if rErr != nil { + return Address{}, rErr + } + + addrData = buf[:4] + + case IPv6Addr: + if len(buf) < 16 { + return Address{}, ErrAddressParseBufferTooSmallForIPv6 + } + + _, rErr := rw.ReadFull(reader, buf[:16]) + + if rErr != nil { + return Address{}, rErr + } + + addrData = buf[:16] + + case HostNameAddr: + addrDataLen := int(0x3f & buf[2]) + + if len(buf) < addrDataLen { + return Address{}, ErrAddressParseBufferTooSmallForHostName + } + + _, rErr := rw.ReadFull(reader, buf[:addrDataLen]) + + if rErr != nil { + return Address{}, rErr + } + + addrData = buf[:addrDataLen] + + default: + return Address{}, ErrAddressInvalidAddressType + } + + return Address{ + port: portNum, + kind: addrType, + data: addrData, + }, nil +} + +// NewAddress creates a new Address +func NewAddress(addrType AddressType, data []byte, port uint16) Address { + return Address{ + port: port, + kind: addrType, + data: data, + } +} + +// Type returns the type of the address +func (a Address) Type() AddressType { + return a.kind +} + +// Data returns the address data +func (a Address) Data() []byte { + return a.data +} + +// Port returns port number +func (a Address) Port() uint16 { + return a.port +} + +// Marshal writes address data to the given b +func (a Address) Marshal(b []byte) (int, error) { + bLen := len(b) + + switch a.Type() { + case LoopbackAddr: + if bLen < 3 { + return 0, ErrAddressMarshalBufferTooSmall + } + + b[0] = byte(a.port >> 8) + b[1] = byte(a.port) + b[2] = byte(LoopbackAddr << 6) + + return 3, nil + + case IPv4Addr: + if bLen < 7 { + return 0, ErrAddressMarshalBufferTooSmall + } + + b[0] = byte(a.port >> 8) + b[1] = byte(a.port) + b[2] = byte(IPv4Addr << 6) + + copy(b[3:], a.data) + + return 7, nil + + case IPv6Addr: + if bLen < 19 { + return 0, ErrAddressMarshalBufferTooSmall + } + + b[0] = byte(a.port >> 8) + b[1] = byte(a.port) + b[2] = byte(IPv6Addr << 6) + + copy(b[3:], a.data) + + return 19, nil + + case HostNameAddr: + hLen := len(a.data) + + if hLen > 0x3f { + panic("Host name cannot longer than 0x3f") + } + + if bLen < hLen+3 { + return 0, ErrAddressMarshalBufferTooSmall + } + + b[0] = byte(a.port >> 8) + b[1] = byte(a.port) + b[2] = byte(HostNameAddr << 6) + b[2] |= byte(hLen) + + copy(b[3:], a.data) + + return hLen + 3, nil + + default: + return 0, ErrAddressInvalidAddressType + } +} + +// String return the Address as string +func (a Address) String() string { + switch a.Type() { + case LoopbackAddr: + return net.JoinHostPort( + "localhost", + strconv.FormatUint(uint64(a.Port()), 10)) + + case IPv4Addr: + return net.JoinHostPort( + net.IPv4(a.data[0], a.data[1], a.data[2], a.data[3]).String(), + strconv.FormatUint(uint64(a.Port()), 10)) + + case IPv6Addr: + return net.JoinHostPort( + net.IP(a.data[:net.IPv6len]).String(), + strconv.FormatUint(uint64(a.Port()), 10)) + + case HostNameAddr: + return net.JoinHostPort( + string(a.data), + strconv.FormatUint(uint64(a.Port()), 10)) + + default: + panic("Unknown Address type") + } +} diff --git a/application/commands/address_test.go b/application/commands/address_test.go new file mode 100644 index 0000000..ba95a5b --- /dev/null +++ b/application/commands/address_test.go @@ -0,0 +1,138 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package commands + +import ( + "bytes" + "strings" + "testing" +) + +func testParseAddress( + t *testing.T, + input []byte, + buf []byte, + expectedType AddressType, + expectedData []byte, + expectedPort uint16, + expectedHostPortString string, +) { + source := bytes.NewBuffer(input) + addr, addrErr := ParseAddress(source.Read, buf) + + if addrErr != nil { + t.Error("Failed to parse due to error:", addrErr) + + return + } + + if addr.Type() != expectedType { + t.Errorf("Expecting the Type to be %d, got %d instead", + expectedType, addr.Type()) + + return + } + + if !bytes.Equal(addr.Data(), expectedData) { + t.Errorf("Expecting the Data to be %d, got %d instead", + expectedData, addr.Data()) + + return + } + + if addr.Port() != expectedPort { + t.Errorf("Expecting the Port to be %d, got %d instead", + expectedPort, addr.Port()) + + return + } + + if addr.String() != expectedHostPortString { + t.Errorf("Expecting the Host Port string to be \"%s\", "+ + "got \"%s\" instead", + expectedHostPortString, addr.String()) + + return + } + + output := make([]byte, len(input)) + mLen, mErr := addr.Marshal(output) + + if mErr != nil { + t.Error("Failed to marshal due to error:", mErr) + + return + } + + if !bytes.Equal(output[:mLen], input) { + t.Errorf("Expecting marshaled result to be %d, got %d instead", + input, output[:mLen]) + + return + } +} + +func TestParseAddress(t *testing.T) { + testParseAddress( + t, []byte{0x04, 0x1e, 0x00}, make([]byte, 3), LoopbackAddr, nil, 1054, + "localhost:1054") + + testParseAddress( + t, + []byte{ + 0x04, 0x1e, 0x40, + 0x7f, 0x00, 0x00, 0x01, + }, + make([]byte, 4), IPv4Addr, []byte{0x7f, 0x00, 0x00, 0x01}, 1054, + "127.0.0.1:1054") + + testParseAddress( + t, + []byte{ + 0x04, 0x1e, 0x80, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x7f, 0x00, 0x00, 0x01, + }, + make([]byte, 16), IPv6Addr, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x7f, 0x00, 0x00, 0x01}, 1054, + "[::7f00:1]:1054") + + testParseAddress( + t, + []byte{ + 0x04, 0x1e, 0xff, + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + '1', '2', '3', + }, + make([]byte, 63), HostNameAddr, []byte{ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + '1', '2', '3', + }, 1054, + strings.Repeat("ABCDEFGHIJ", 6)+"123:1054") +} diff --git a/application/commands/commands.go b/application/commands/commands.go new file mode 100644 index 0000000..c08746d --- /dev/null +++ b/application/commands/commands.go @@ -0,0 +1,30 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package commands + +import ( + "github.com/nirui/sshwifty/application/command" +) + +// New creates a new commands group +func New() command.Commands { + return command.Commands{ + command.Register("Telnet", newTelnet, parseTelnetConfig), + command.Register("SSH", newSSH, parseSSHConfig), + } +} diff --git a/application/commands/integer.go b/application/commands/integer.go new file mode 100644 index 0000000..cf26bb6 --- /dev/null +++ b/application/commands/integer.go @@ -0,0 +1,120 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package commands + +import ( + "errors" + + "github.com/nirui/sshwifty/application/rw" +) + +// Errors +var ( + ErrIntegerMarshalNotEnoughBuffer = errors.New( + "not enough buffer to marshal the integer") + + ErrIntegerMarshalTooLarge = errors.New( + "integer cannot be marshalled, because the vaule was too large") +) + +// Integer is a 16bit unsigned integer data +// +// Format: +// +-------------------------------------+--------------+ +// | 1 bit | 7 bits | +// +-------------------------------------+--------------+ +// | 1 when current byte is the end byte | Integer data | +// +-------------------------------------+--------------+ +// +// Example: +// - 00000000 00000000: 0 +// - 01111111: 127 +// - 11111111 01000000: 255 +type Integer uint16 + +const ( + integerHasNextBit = 0x80 + integerValueCutter = 0x7f +) + +// Consts +const ( + MaxInteger = 0x3fff + MaxIntegerBytes = 2 +) + +// ByteSize returns how many bytes current integer will be encoded into +func (i *Integer) ByteSize() int { + if *i > integerValueCutter { + return 2 + } + + return 1 +} + +// Int returns a int of current Integer +func (i *Integer) Int() int { + return int(*i) +} + +// Marshal build serialized data of the integer +func (i *Integer) Marshal(b []byte) (int, error) { + bLen := len(b) + + if *i > MaxInteger { + return 0, ErrIntegerMarshalTooLarge + } + + if bLen < i.ByteSize() { + return 0, ErrIntegerMarshalNotEnoughBuffer + } + + if *i <= integerValueCutter { + b[0] = byte(*i & integerValueCutter) + + return 1, nil + } + + b[0] = byte((*i >> 7) | integerHasNextBit) + b[1] = byte(*i & integerValueCutter) + + return 2, nil +} + +// Unmarshal read data and parse the integer +func (i *Integer) Unmarshal(reader rw.ReaderFunc) error { + buf := [1]byte{} + + for j := 0; j < MaxIntegerBytes; j++ { + _, rErr := rw.ReadFull(reader, buf[:]) + + if rErr != nil { + return rErr + } + + *i |= Integer(buf[0] & integerValueCutter) + + if integerHasNextBit&buf[0] == 0 { + return nil + } + + *i <<= 7 + } + + return nil +} diff --git a/application/commands/integer_test.go b/application/commands/integer_test.go new file mode 100644 index 0000000..b051547 --- /dev/null +++ b/application/commands/integer_test.go @@ -0,0 +1,124 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package commands + +import ( + "bytes" + "testing" +) + +func TestInteger(t *testing.T) { + ii := Integer(0x3fff) + result := Integer(0) + buf := make([]byte, 2) + + mLen, mErr := ii.Marshal(buf) + + if mErr != nil { + t.Error("Failed to marshal:", mErr) + + return + } + + mData := bytes.NewBuffer(buf[:mLen]) + mErr = result.Unmarshal(mData.Read) + + if mErr != nil { + t.Error("Failed to unmarshal:", mErr) + + return + } + + if result != ii { + t.Errorf("Expecting result to be %d, got %d instead", ii, result) + + return + } +} + +func TestIntegerSingleByte1(t *testing.T) { + ii := Integer(102) + result := Integer(0) + buf := make([]byte, 2) + + mLen, mErr := ii.Marshal(buf) + + if mErr != nil { + t.Error("Failed to marshal:", mErr) + + return + } + + if mLen != 1 { + t.Errorf("Expecting the Integer to be marshalled into %d bytes, got "+ + "%d instead", 1, mLen) + + return + } + + mData := bytes.NewBuffer(buf[:mLen]) + mErr = result.Unmarshal(mData.Read) + + if mErr != nil { + t.Error("Failed to unmarshal:", mErr) + + return + } + + if result != ii { + t.Errorf("Expecting result to be %d, got %d instead", ii, result) + + return + } +} + +func TestIntegerSingleByte2(t *testing.T) { + ii := Integer(127) + result := Integer(0) + buf := make([]byte, 2) + + mLen, mErr := ii.Marshal(buf) + + if mErr != nil { + t.Error("Failed to marshal:", mErr) + + return + } + + if mLen != 1 { + t.Errorf("Expecting the Integer to be marshalled into %d bytes, got "+ + "%d instead", 1, mLen) + + return + } + + mData := bytes.NewBuffer(buf[:mLen]) + mErr = result.Unmarshal(mData.Read) + + if mErr != nil { + t.Error("Failed to unmarshal:", mErr) + + return + } + + if result != ii { + t.Errorf("Expecting result to be %d, got %d instead", ii, result) + + return + } +} diff --git a/application/commands/ssh.go b/application/commands/ssh.go new file mode 100644 index 0000000..9e76063 --- /dev/null +++ b/application/commands/ssh.go @@ -0,0 +1,783 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package commands + +import ( + "errors" + "io" + "net" + "sync" + "time" + + "golang.org/x/crypto/ssh" + + "github.com/nirui/sshwifty/application/command" + "github.com/nirui/sshwifty/application/configuration" + "github.com/nirui/sshwifty/application/log" + "github.com/nirui/sshwifty/application/network" + "github.com/nirui/sshwifty/application/rw" +) + +// Server -> client signal Consts +const ( + SSHServerRemoteStdOut = 0x00 + SSHServerRemoteStdErr = 0x01 + SSHServerConnectFailed = 0x02 + SSHServerConnectSucceed = 0x03 + SSHServerConnectVerifyFingerprint = 0x04 + SSHServerConnectRequestCredential = 0x05 +) + +// Client -> server signal consts +const ( + SSHClientStdIn = 0x00 + SSHClientResize = 0x01 + SSHClientRespondFingerprint = 0x02 + SSHClientRespondCredential = 0x03 +) + +const ( + sshCredentialMaxSize = 4096 +) + +// Error codes +const ( + SSHRequestErrorBadUserName = command.StreamError(0x01) + SSHRequestErrorBadRemoteAddress = command.StreamError(0x02) + SSHRequestErrorBadAuthMethod = command.StreamError(0x03) +) + +// Auth methods +const ( + SSHAuthMethodNone byte = 0x00 + SSHAuthMethodPassphrase byte = 0x01 + SSHAuthMethodPrivateKey byte = 0x02 +) + +type sshAuthMethodBuilder func(b []byte) []ssh.AuthMethod + +// Errors +var ( + ErrSSHAuthCancelled = errors.New( + "authentication has been cancelled") + + ErrSSHInvalidAuthMethod = errors.New( + "invalid auth method") + + ErrSSHInvalidAddress = errors.New( + "invalid address") + + ErrSSHRemoteFingerprintVerificationCancelled = errors.New( + "server Fingerprint verification process has been cancelled") + + ErrSSHRemoteFingerprintRefused = errors.New( + "server Fingerprint has been refused") + + ErrSSHRemoteConnUnavailable = errors.New( + "remote SSH connection is unavailable") + + ErrSSHUnexpectedFingerprintVerificationRespond = errors.New( + "unexpected fingerprint verification respond") + + ErrSSHUnexpectedCredentialDataRespond = errors.New( + "unexpected credential data respond") + + ErrSSHCredentialDataTooLarge = errors.New( + "credential was too large") + + ErrSSHUnknownClientSignal = errors.New( + "unknown client signal") +) + +var ( + sshEmptyTime = time.Time{} +) + +const ( + sshDefaultPortString = "22" +) + +type sshRemoteConnWrapper struct { + net.Conn + + writerConn network.WriteTimeoutConn + requestTimeoutRetry func(s *sshRemoteConnWrapper) bool +} + +func (s *sshRemoteConnWrapper) Read(b []byte) (int, error) { + for { + rLen, rErr := s.Conn.Read(b) + + if rErr == nil { + return rLen, nil + } + + netErr, isNetErr := rErr.(net.Error) + + if !isNetErr || !netErr.Timeout() || !s.requestTimeoutRetry(s) { + return rLen, rErr + } + } +} + +func (s *sshRemoteConnWrapper) Write(b []byte) (int, error) { + return s.writerConn.Write(b) +} + +type sshRemoteConn struct { + writer io.Writer + closer func() error + session *ssh.Session +} + +func (s sshRemoteConn) isValid() bool { + return s.writer != nil && s.closer != nil && s.session != nil +} + +type sshClient struct { + w command.StreamResponder + l log.Logger + cfg command.Configuration + remoteCloseWait sync.WaitGroup + remoteReadTimeoutRetry bool + remoteReadForceRetryNextTimeout bool + remoteReadTimeoutRetryLock sync.Mutex + credentialReceive chan []byte + credentialProcessed bool + credentialReceiveClosed bool + fingerprintVerifyResultReceive chan bool + fingerprintProcessed bool + fingerprintVerifyResultReceiveClosed bool + remoteConnReceive chan sshRemoteConn + remoteConn sshRemoteConn +} + +func newSSH( + l log.Logger, + w command.StreamResponder, + cfg command.Configuration, +) command.FSMMachine { + return &sshClient{ + w: w, + l: l, + cfg: cfg, + remoteCloseWait: sync.WaitGroup{}, + remoteReadTimeoutRetry: false, + remoteReadForceRetryNextTimeout: false, + remoteReadTimeoutRetryLock: sync.Mutex{}, + credentialReceive: make(chan []byte, 1), + credentialProcessed: false, + credentialReceiveClosed: false, + fingerprintVerifyResultReceive: make(chan bool, 1), + fingerprintProcessed: false, + fingerprintVerifyResultReceiveClosed: false, + remoteConnReceive: make(chan sshRemoteConn, 1), + remoteConn: sshRemoteConn{}, + } +} + +func parseSSHConfig(p configuration.Preset) (configuration.Preset, error) { + oldHost := p.Host + + _, _, sErr := net.SplitHostPort(p.Host) + + if sErr != nil { + p.Host = net.JoinHostPort(p.Host, sshDefaultPortString) + } + + if len(p.Host) <= 0 { + p.Host = oldHost + } + + return p, nil +} + +func (d *sshClient) Bootup( + r *rw.LimitedReader, + b []byte, +) (command.FSMState, command.FSMError) { + // User name + userName, userNameErr := ParseString(r.Read, b) + + if userNameErr != nil { + return nil, command.ToFSMError( + userNameErr, SSHRequestErrorBadUserName) + } + + userNameStr := string(userName.Data()) + + // Address + addr, addrErr := ParseAddress(r.Read, b) + + if addrErr != nil { + return nil, command.ToFSMError( + addrErr, SSHRequestErrorBadRemoteAddress) + } + + addrStr := addr.String() + + if len(addrStr) <= 0 { + return nil, command.ToFSMError( + ErrSSHInvalidAddress, SSHRequestErrorBadRemoteAddress) + } + + // Auth method + rData, rErr := rw.FetchOneByte(r.Fetch) + + if rErr != nil { + return nil, command.ToFSMError( + rErr, SSHRequestErrorBadAuthMethod) + } + + authMethodBuilder, authMethodBuilderErr := d.buildAuthMethod(rData[0]) + + if authMethodBuilderErr != nil { + return nil, command.ToFSMError( + authMethodBuilderErr, SSHRequestErrorBadAuthMethod) + } + + d.remoteCloseWait.Add(1) + go d.remote(userNameStr, addrStr, authMethodBuilder) + + return d.local, command.NoFSMError() +} + +func (d *sshClient) buildAuthMethod( + methodType byte) (sshAuthMethodBuilder, error) { + switch methodType { + case SSHAuthMethodNone: + return func(b []byte) []ssh.AuthMethod { + return nil + }, nil + + case SSHAuthMethodPassphrase: + return func(b []byte) []ssh.AuthMethod { + return []ssh.AuthMethod{ + ssh.PasswordCallback(func() (string, error) { + d.enableRemoteReadTimeoutRetry() + defer d.disableRemoteReadTimeoutRetry() + + wErr := d.w.SendManual( + SSHServerConnectRequestCredential, + b[d.w.HeaderSize():], + ) + + if wErr != nil { + return "", wErr + } + + passphraseBytes, passphraseReceived := <-d.credentialReceive + + if !passphraseReceived { + return "", ErrSSHAuthCancelled + } + + return string(passphraseBytes), nil + }), + } + }, nil + + case SSHAuthMethodPrivateKey: + return func(b []byte) []ssh.AuthMethod { + return []ssh.AuthMethod{ + ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { + d.enableRemoteReadTimeoutRetry() + defer d.disableRemoteReadTimeoutRetry() + + wErr := d.w.SendManual( + SSHServerConnectRequestCredential, + b[d.w.HeaderSize():], + ) + + if wErr != nil { + return nil, wErr + } + + privateKeyBytes, privateKeyReceived := <-d.credentialReceive + + if !privateKeyReceived { + return nil, ErrSSHAuthCancelled + } + + signer, signerErr := ssh.ParsePrivateKey(privateKeyBytes) + + if signerErr != nil { + return nil, signerErr + } + + return []ssh.Signer{signer}, signerErr + }), + } + }, nil + } + + return nil, ErrSSHInvalidAuthMethod +} + +func (d *sshClient) confirmRemoteFingerprint( + hostname string, + remote net.Addr, + key ssh.PublicKey, + buf []byte, +) error { + d.enableRemoteReadTimeoutRetry() + defer d.disableRemoteReadTimeoutRetry() + + fgp := ssh.FingerprintSHA256(key) + fgpLen := copy(buf[d.w.HeaderSize():], fgp) + + wErr := d.w.SendManual( + SSHServerConnectVerifyFingerprint, + buf[:d.w.HeaderSize()+fgpLen], + ) + + if wErr != nil { + return wErr + } + + confirmed, confirmOK := <-d.fingerprintVerifyResultReceive + + if !confirmOK { + return ErrSSHRemoteFingerprintVerificationCancelled + } + + if !confirmed { + return ErrSSHRemoteFingerprintRefused + } + + return nil +} + +func (d *sshClient) enableRemoteReadTimeoutRetry() { + d.remoteReadTimeoutRetryLock.Lock() + defer d.remoteReadTimeoutRetryLock.Unlock() + + d.remoteReadTimeoutRetry = true +} + +func (d *sshClient) disableRemoteReadTimeoutRetry() { + d.remoteReadTimeoutRetryLock.Lock() + defer d.remoteReadTimeoutRetryLock.Unlock() + + d.remoteReadTimeoutRetry = false + d.remoteReadForceRetryNextTimeout = true +} + +func (d *sshClient) dialRemote( + networkName, + addr string, + config *ssh.ClientConfig) (*ssh.Client, func(), error) { + conn, err := d.cfg.Dial(networkName, addr, config.Timeout) + + if err != nil { + return nil, nil, err + } + + sshConn := &sshRemoteConnWrapper{ + Conn: conn, + writerConn: network.NewWriteTimeoutConn(conn, d.cfg.DialTimeout), + requestTimeoutRetry: func(s *sshRemoteConnWrapper) bool { + d.remoteReadTimeoutRetryLock.Lock() + defer d.remoteReadTimeoutRetryLock.Unlock() + + if !d.remoteReadTimeoutRetry { + if !d.remoteReadForceRetryNextTimeout { + return false + } + + d.remoteReadForceRetryNextTimeout = false + } + + s.SetReadDeadline(time.Now().Add(config.Timeout)) + + return true + }, + } + + // Set timeout for writer, otherwise the Timeout writer will never + // be triggered + sshConn.SetWriteDeadline(time.Now().Add(d.cfg.DialTimeout)) + sshConn.SetReadDeadline(time.Now().Add(config.Timeout)) + + c, chans, reqs, err := ssh.NewClientConn(sshConn, addr, config) + + if err != nil { + sshConn.Close() + + return nil, nil, err + } + + return ssh.NewClient(c, chans, reqs), func() { + d.remoteReadTimeoutRetryLock.Lock() + defer d.remoteReadTimeoutRetryLock.Unlock() + + d.remoteReadTimeoutRetry = false + d.remoteReadForceRetryNextTimeout = true + + sshConn.SetReadDeadline(sshEmptyTime) + }, nil +} + +func (d *sshClient) remote( + user string, address string, authMethodBuilder sshAuthMethodBuilder) { + defer func() { + d.w.Signal(command.HeaderClose) + + close(d.remoteConnReceive) + d.remoteCloseWait.Done() + }() + + buf := [4096]byte{} + + conn, clearConnInitialDeadline, dErr := + d.dialRemote("tcp", address, &ssh.ClientConfig{ + User: user, + Auth: authMethodBuilder(buf[:]), + HostKeyCallback: func(h string, r net.Addr, k ssh.PublicKey) error { + return d.confirmRemoteFingerprint(h, r, k, buf[:]) + }, + Timeout: d.cfg.DialTimeout, + }) + + if dErr != nil { + errLen := copy(buf[d.w.HeaderSize():], dErr.Error()) + d.w.HeaderSize() + d.w.SendManual(SSHServerConnectFailed, buf[:errLen]) + + d.l.Debug("Unable to connect to remote machine: %s", dErr) + + return + } + + defer conn.Close() + + session, sErr := conn.NewSession() + + if sErr != nil { + errLen := copy(buf[d.w.HeaderSize():], sErr.Error()) + d.w.HeaderSize() + d.w.SendManual(SSHServerConnectFailed, buf[:errLen]) + + d.l.Debug("Unable open new session on remote machine: %s", sErr) + + return + } + + defer session.Close() + + in, inErr := session.StdinPipe() + + if inErr != nil { + errLen := copy(buf[d.w.HeaderSize():], inErr.Error()) + d.w.HeaderSize() + d.w.SendManual(SSHServerConnectFailed, buf[:errLen]) + + d.l.Debug("Unable export Stdin pipe: %s", inErr) + + return + } + + out, outErr := session.StdoutPipe() + + if outErr != nil { + errLen := copy(buf[d.w.HeaderSize():], outErr.Error()) + + d.w.HeaderSize() + d.w.SendManual(SSHServerConnectFailed, buf[:errLen]) + + d.l.Debug("Unable export Stdout pipe: %s", outErr) + + return + } + + errOut, outErrErr := session.StderrPipe() + + if outErrErr != nil { + errLen := copy(buf[d.w.HeaderSize():], outErrErr.Error()) + + d.w.HeaderSize() + d.w.SendManual(SSHServerConnectFailed, buf[:errLen]) + + d.l.Debug("Unable export Stderr pipe: %s", outErrErr) + + return + } + + sErr = session.RequestPty("xterm", 80, 40, ssh.TerminalModes{ + ssh.ECHO: 1, + ssh.TTY_OP_ISPEED: 14400, + ssh.TTY_OP_OSPEED: 14400, + }) + + if sErr != nil { + errLen := copy(buf[d.w.HeaderSize():], sErr.Error()) + d.w.HeaderSize() + d.w.SendManual(SSHServerConnectFailed, buf[:errLen]) + + d.l.Debug("Unable request PTY: %s", sErr) + + return + } + + sErr = session.Shell() + + if sErr != nil { + errLen := copy(buf[d.w.HeaderSize():], sErr.Error()) + d.w.HeaderSize() + d.w.SendManual(SSHServerConnectFailed, buf[:errLen]) + + d.l.Debug("Unable to start Shell: %s", sErr) + + return + } + + defer session.Wait() + + clearConnInitialDeadline() + + d.remoteConnReceive <- sshRemoteConn{ + writer: in, + closer: func() error { + session.Close() + + return conn.Close() + }, + session: session, + } + + wErr := d.w.SendManual( + SSHServerConnectSucceed, buf[:d.w.HeaderSize()]) + + if wErr != nil { + return + } + + d.l.Debug("Serving") + + d.remoteCloseWait.Add(1) + + go func() { + defer d.remoteCloseWait.Done() + + errOutBuf := [4096]byte{} + + for { + rLen, rErr := errOut.Read(errOutBuf[d.w.HeaderSize():]) + + if rErr != nil { + return + } + + rErr = d.w.SendManual( + SSHServerRemoteStdErr, errOutBuf[:d.w.HeaderSize()+rLen]) + + if rErr != nil { + return + } + } + }() + + for { + rLen, rErr := out.Read(buf[d.w.HeaderSize():]) + + if rErr != nil { + return + } + + rErr = d.w.SendManual( + SSHServerRemoteStdOut, buf[:d.w.HeaderSize()+rLen]) + + if rErr != nil { + return + } + } +} + +func (d *sshClient) getRemote() (sshRemoteConn, error) { + if d.remoteConn.isValid() { + return d.remoteConn, nil + } + + remoteConn, remoteConnFetched := <-d.remoteConnReceive + + if !remoteConnFetched { + return sshRemoteConn{}, ErrSSHRemoteConnUnavailable + } + + d.remoteConn = remoteConn + + return d.remoteConn, nil +} + +func (d *sshClient) local( + f *command.FSM, + r *rw.LimitedReader, + h command.StreamHeader, + b []byte, +) error { + switch h.Marker() { + case SSHClientStdIn: + remote, remoteErr := d.getRemote() + + if remoteErr != nil { + return remoteErr + } + + for !r.Completed() { + rData, rErr := r.Buffered() + + if rErr != nil { + return rErr + } + + _, wErr := remote.writer.Write(rData) + + if wErr != nil { + remote.closer() + + d.l.Debug("Failed to write data to remote: %s", wErr) + } + } + + return nil + + case SSHClientResize: + remote, remoteErr := d.getRemote() + + if remoteErr != nil { + return remoteErr + } + + _, rErr := io.ReadFull(r, b[:4]) + + if rErr != nil { + return rErr + } + + rows := int(b[0]) + rows <<= 8 + rows |= int(b[1]) + + cols := int(b[2]) + cols <<= 8 + cols |= int(b[3]) + + // It's ok for it to fail + wcErr := remote.session.WindowChange(rows, cols) + + if wcErr != nil { + d.l.Debug("Failed to resize to %d, %d: %s", rows, cols, wcErr) + } + + return nil + + case SSHClientRespondFingerprint: + if d.fingerprintProcessed { + return ErrSSHUnexpectedFingerprintVerificationRespond + } + + d.fingerprintProcessed = true + + rData, rErr := rw.FetchOneByte(r.Fetch) + + if rErr != nil { + return rErr + } + + comfirmed := rData[0] == 0 + + if !comfirmed { + d.fingerprintVerifyResultReceive <- false + + remote, remoteErr := d.getRemote() + + if remoteErr == nil { + remote.closer() + } + } else { + d.fingerprintVerifyResultReceive <- true + } + + return nil + + case SSHClientRespondCredential: + if d.credentialProcessed { + return ErrSSHUnexpectedCredentialDataRespond + } + + d.credentialProcessed = true + + sshCredentialBufSize := 0 + + if r.Remains() > sshCredentialMaxSize { + sshCredentialBufSize = sshCredentialMaxSize + } else { + sshCredentialBufSize = r.Remains() + } + + credentialDataBuf := make([]byte, 0, sshCredentialBufSize) + totalCredentialRead := 0 + + for !r.Completed() { + rData, rErr := r.Buffered() + + if rErr != nil { + return rErr + } + + totalCredentialRead += len(rData) + + if totalCredentialRead > sshCredentialBufSize { + return ErrSSHCredentialDataTooLarge + } + + credentialDataBuf = append(credentialDataBuf, rData...) + } + + d.credentialReceive <- credentialDataBuf + + return nil + + default: + return ErrSSHUnknownClientSignal + } +} + +func (d *sshClient) Close() error { + d.credentialProcessed = true + d.fingerprintProcessed = true + + if !d.credentialReceiveClosed { + close(d.credentialReceive) + + d.credentialReceiveClosed = true + } + + if !d.fingerprintVerifyResultReceiveClosed { + close(d.fingerprintVerifyResultReceive) + + d.fingerprintVerifyResultReceiveClosed = true + } + + remote, remoteErr := d.getRemote() + + if remoteErr == nil { + remote.closer() + } + + d.remoteCloseWait.Wait() + + return nil +} + +func (d *sshClient) Release() error { + return nil +} diff --git a/application/commands/string.go b/application/commands/string.go new file mode 100644 index 0000000..27bee10 --- /dev/null +++ b/application/commands/string.go @@ -0,0 +1,103 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package commands + +import ( + "errors" + + "github.com/nirui/sshwifty/application/rw" +) + +// Errors +var ( + ErrStringParseBufferTooSmall = errors.New( + "not enough buffer space to parse given string") + + ErrStringMarshalBufferTooSmall = errors.New( + "not enough buffer space to marshal given string") +) + +// String data +type String struct { + len Integer + data []byte +} + +// ParseString build the String according to readed data +func ParseString(reader rw.ReaderFunc, b []byte) (String, error) { + lenData := Integer(0) + + mErr := lenData.Unmarshal(reader) + + if mErr != nil { + return String{}, mErr + } + + bLen := len(b) + + if bLen < lenData.Int() { + return String{}, ErrStringParseBufferTooSmall + } + + _, rErr := rw.ReadFull(reader, b[:lenData]) + + if rErr != nil { + return String{}, rErr + } + + return String{ + len: lenData, + data: b[:lenData], + }, nil +} + +// NewString create a new String +func NewString(d []byte) String { + dLen := len(d) + + if dLen > MaxInteger { + panic("Data was too long for a String") + } + + return String{ + len: Integer(dLen), + data: d, + } +} + +// Data returns the data of the string +func (s String) Data() []byte { + return s.data +} + +// Marshal the string to give buffer +func (s String) Marshal(b []byte) (int, error) { + bLen := len(b) + + if bLen < s.len.ByteSize()+len(s.data) { + return 0, ErrStringMarshalBufferTooSmall + } + + mLen, mErr := s.len.Marshal(b) + + if mErr != nil { + return 0, mErr + } + + return copy(b[mLen:], s.data) + mLen, nil +} diff --git a/application/commands/string_test.go b/application/commands/string_test.go new file mode 100644 index 0000000..b35e398 --- /dev/null +++ b/application/commands/string_test.go @@ -0,0 +1,83 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package commands + +import ( + "bytes" + "testing" +) + +func testString(t *testing.T, str []byte) { + ss := NewString(str) + mm := make([]byte, len(str)+2) + + mLen, mErr := ss.Marshal(mm) + + if mErr != nil { + t.Error("Failed to marshal:", mErr) + + return + } + + buf := make([]byte, mLen) + source := bytes.NewBuffer(mm[:mLen]) + result, rErr := ParseString(source.Read, buf) + + if rErr != nil { + t.Error("Failed to parse:", rErr) + + return + } + + if !bytes.Equal(result.Data(), ss.Data()) { + t.Errorf("Expecting the data to be %d, got %d instead", + ss.Data(), result.Data()) + + return + } +} + +func TestString(t *testing.T) { + testString(t, []byte{ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + }) + + testString(t, []byte{ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', + }) +} diff --git a/application/commands/telnet.go b/application/commands/telnet.go new file mode 100644 index 0000000..2c875f3 --- /dev/null +++ b/application/commands/telnet.go @@ -0,0 +1,229 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package commands + +import ( + "errors" + "net" + "sync" + "time" + + "github.com/nirui/sshwifty/application/command" + "github.com/nirui/sshwifty/application/configuration" + "github.com/nirui/sshwifty/application/log" + "github.com/nirui/sshwifty/application/network" + "github.com/nirui/sshwifty/application/rw" +) + +// Errors +var ( + ErrTelnetUnableToReceiveRemoteConn = errors.New( + "unable to acquire remote connection handle") +) + +// Error codes +const ( + TelnetRequestErrorBadRemoteAddress = command.StreamError(0x01) +) + +const ( + telnetDefaultPortString = "23" +) + +// Server signal codes +const ( + TelnetServerRemoteBand = 0x00 + TelnetServerDialFailed = 0x01 + TelnetServerDialConnected = 0x02 +) + +type telnetClient struct { + l log.Logger + w command.StreamResponder + cfg command.Configuration + remoteChan chan net.Conn + remoteConn net.Conn + closeWait sync.WaitGroup +} + +func newTelnet( + l log.Logger, + w command.StreamResponder, + cfg command.Configuration, +) command.FSMMachine { + return &telnetClient{ + l: l, + w: w, + cfg: cfg, + remoteChan: make(chan net.Conn, 1), + remoteConn: nil, + closeWait: sync.WaitGroup{}, + } +} + +func parseTelnetConfig(p configuration.Preset) (configuration.Preset, error) { + oldHost := p.Host + + _, _, sErr := net.SplitHostPort(p.Host) + + if sErr != nil { + p.Host = net.JoinHostPort(p.Host, telnetDefaultPortString) + } + + if len(p.Host) <= 0 { + p.Host = oldHost + } + + return p, nil +} + +func (d *telnetClient) Bootup( + r *rw.LimitedReader, + b []byte) (command.FSMState, command.FSMError) { + addr, addrErr := ParseAddress(r.Read, b) + + if addrErr != nil { + return nil, command.ToFSMError( + addrErr, TelnetRequestErrorBadRemoteAddress) + } + + d.closeWait.Add(1) + go d.remote(addr.String()) + + return d.client, command.NoFSMError() +} + +func (d *telnetClient) remote(addr string) { + defer func() { + d.w.Signal(command.HeaderClose) + + close(d.remoteChan) + d.closeWait.Done() + }() + + buf := [4096]byte{} + + clientConn, clientConnErr := d.cfg.Dial("tcp", addr, d.cfg.DialTimeout) + + if clientConnErr != nil { + errLen := copy( + buf[d.w.HeaderSize():], clientConnErr.Error()) + d.w.HeaderSize() + d.w.SendManual(TelnetServerDialFailed, buf[:errLen]) + + return + } + + defer clientConn.Close() + + clientConnErr = d.w.SendManual( + TelnetServerDialConnected, + buf[:d.w.HeaderSize()], + ) + + if clientConnErr != nil { + return + } + + // Set timeout for writer, otherwise the Timeout writer will never + // be triggered + clientConn.SetWriteDeadline(time.Now().Add(d.cfg.DialTimeout)) + timeoutClientConn := network.NewWriteTimeoutConn( + clientConn, d.cfg.DialTimeout) + + d.remoteChan <- &timeoutClientConn + + for { + rLen, rErr := clientConn.Read(buf[d.w.HeaderSize():]) + + if rErr != nil { + return + } + + wErr := d.w.SendManual( + TelnetServerRemoteBand, buf[:rLen+d.w.HeaderSize()]) + + if wErr != nil { + return + } + } +} + +func (d *telnetClient) getRemote() (net.Conn, error) { + if d.remoteConn != nil { + return d.remoteConn, nil + } + + remoteConn, ok := <-d.remoteChan + + if !ok { + return nil, ErrTelnetUnableToReceiveRemoteConn + } + + d.remoteConn = remoteConn + + return d.remoteConn, nil +} + +func (d *telnetClient) client( + f *command.FSM, + r *rw.LimitedReader, + h command.StreamHeader, + b []byte, +) error { + remoteConn, remoteConnErr := d.getRemote() + + if remoteConnErr != nil { + return remoteConnErr + } + + // All Telnet requests are in-band, so we just directly send them all + // to the server + for !r.Completed() { + rBuf, rErr := r.Buffered() + + if rErr != nil { + return rErr + } + + _, wErr := remoteConn.Write(rBuf) + + if wErr != nil { + remoteConn.Close() + + d.l.Debug("Failed to write data to remote: %s", wErr) + } + } + + return nil +} + +func (d *telnetClient) Close() error { + remoteConn, remoteConnErr := d.getRemote() + + if remoteConnErr == nil { + remoteConn.Close() + } + + d.closeWait.Wait() + + return nil +} + +func (d *telnetClient) Release() error { + return nil +} diff --git a/application/configuration/common.go b/application/configuration/common.go new file mode 100644 index 0000000..16b3653 --- /dev/null +++ b/application/configuration/common.go @@ -0,0 +1,26 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package configuration + +func durationAtLeast(current, min int) int { + if current > min { + return current + } + + return min +} diff --git a/application/configuration/config.go b/application/configuration/config.go new file mode 100644 index 0000000..05e369a --- /dev/null +++ b/application/configuration/config.go @@ -0,0 +1,253 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package configuration + +import ( + "errors" + "fmt" + "net" + "time" + + "github.com/nirui/sshwifty/application/network" +) + +// Server contains configuration of a HTTP server +type Server struct { + ListenInterface string + ListenPort uint16 + InitialTimeout time.Duration + ReadTimeout time.Duration + WriteTimeout time.Duration + HeartbeatTimeout time.Duration + ReadDelay time.Duration + WriteDelay time.Duration + TLSCertificateFile string + TLSCertificateKeyFile string + ServerMessage string +} + +func (s Server) defaultListenInterface() string { + if len(s.ListenInterface) > 0 { + return s.ListenInterface + } + + return net.IPv4(127, 0, 0, 1).String() +} + +func (s Server) defaultListenPort() uint16 { + if s.ListenPort > 0 { + return s.ListenPort + } + + return 80 +} + +func (s Server) maxDur(cur, def time.Duration) time.Duration { + if cur > def { + return cur + } + + return def +} + +func (s Server) minDur(cur, def time.Duration) time.Duration { + if cur < def { + return cur + } + + return def +} + +// WithDefault build the configuration and fill the blank with default values +func (s Server) WithDefault() Server { + initialTimeout := s.maxDur(s.InitialTimeout, 1*time.Second) + + readTimeout := s.maxDur(initialTimeout, 3*time.Second) + readTimeout = s.maxDur(s.ReadTimeout, readTimeout) + + maxHeartBeatTimeout := time.Duration(float64(readTimeout) * 0.8) + heartBeatTimeout := s.minDur(s.HeartbeatTimeout, maxHeartBeatTimeout) + + if heartBeatTimeout <= 0 { + heartBeatTimeout = maxHeartBeatTimeout + } + + return Server{ + ListenInterface: s.defaultListenInterface(), + ListenPort: s.defaultListenPort(), + InitialTimeout: initialTimeout, + ReadTimeout: readTimeout, + WriteTimeout: s.maxDur(s.WriteTimeout, 3*time.Second), + HeartbeatTimeout: heartBeatTimeout, + ReadDelay: s.ReadDelay, + WriteDelay: s.WriteDelay, + TLSCertificateFile: s.TLSCertificateFile, + TLSCertificateKeyFile: s.TLSCertificateKeyFile, + ServerMessage: s.ServerMessage, + } +} + +// IsTLS returns whether or not TLS should be used +func (s Server) IsTLS() bool { + return len(s.TLSCertificateFile) > 0 && len(s.TLSCertificateKeyFile) > 0 +} + +// Verify verifies current configuration +func (s Server) Verify() error { + if net.ParseIP(s.ListenInterface) == nil { + return fmt.Errorf("invalid IP address \"%s\"", s.ListenInterface) + } + + if (len(s.TLSCertificateFile) > 0 && len(s.TLSCertificateKeyFile) <= 0) || + (len(s.TLSCertificateFile) <= 0 && len(s.TLSCertificateKeyFile) > 0) { + return errors.New("TLSCertificateFile and TLSCertificateKeyFile must " + + "both be specified in order to enable TLS") + } + + return nil +} + +// Meta contains data of a Key -> Value map which can be use to store +// dynamically structured configuration options +type Meta map[string]String + +// Concretize returns an concretized Meta as a `map[string]string` +func (m Meta) Concretize() (map[string]string, error) { + mm := make(map[string]string, len(m)) + + for k, v := range m { + result, err := v.Parse() + + if err != nil { + return nil, fmt.Errorf("unable to parse Meta \"%s\": %s", k, err) + } + + mm[k] = result + } + + return mm, nil +} + +// Preset contains data of a static remote host +type Preset struct { + Title string + Type string + Host string + Meta map[string]string +} + +// Configuration contains configuration of the application +type Configuration struct { + HostName string + SharedKey string + DialTimeout time.Duration + Socks5 string + Socks5User string + Socks5Password string + Servers []Server + Presets []Preset + OnlyAllowPresetRemotes bool +} + +// Common settings shared by mulitple servers +type Common struct { + HostName string + SharedKey string + Dialer network.Dial + DialTimeout time.Duration + Presets []Preset + OnlyAllowPresetRemotes bool +} + +// Verify verifies current setting +func (c Configuration) Verify() error { + if len(c.Servers) <= 0 { + return errors.New("must specify at least one server") + } + + for i, c := range c.Servers { + vErr := c.Verify() + + if vErr == nil { + continue + } + + return fmt.Errorf("invalid setting for server %d: %s", i, vErr) + } + + return nil +} + +// Dialer builds a Dialer +func (c Configuration) Dialer() network.Dial { + dialTimeout := c.DialTimeout + + if dialTimeout < 3 { + dialTimeout = 3 + } + + dialer := network.TCPDial() + + if len(c.Socks5) > 0 { + sDial, sDialErr := network.BuildSocks5Dial( + c.Socks5, c.Socks5User, c.Socks5Password) + + if sDialErr != nil { + panic("Unable to build Socks5 Dialer: " + sDialErr.Error()) + } + + dialer = sDial + } + + if c.OnlyAllowPresetRemotes { + accessList := make(network.AllowedHosts, len(c.Presets)) + + for _, k := range c.Presets { + if len(k.Host) <= 0 { + continue + } + + accessList[k.Host] = struct{}{} + } + + dialer = network.AccessControlDial(accessList, dialer) + } + + return dialer +} + +// Common returns common settings +func (c Configuration) Common() Common { + return Common{ + HostName: c.HostName, + SharedKey: c.SharedKey, + Dialer: c.Dialer(), + DialTimeout: c.DialTimeout, + Presets: c.Presets, + OnlyAllowPresetRemotes: c.OnlyAllowPresetRemotes, + } +} + +// DecideDialTimeout will return a reasonable timeout for dialing +func (c Common) DecideDialTimeout(max time.Duration) time.Duration { + if c.DialTimeout > max { + return max + } + + return c.DialTimeout +} diff --git a/application/configuration/loader.go b/application/configuration/loader.go new file mode 100644 index 0000000..e99f488 --- /dev/null +++ b/application/configuration/loader.go @@ -0,0 +1,28 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package configuration + +import ( + "github.com/nirui/sshwifty/application/log" +) + +// PresetReloader reloads preset +type PresetReloader func(p Preset) (Preset, error) + +// Loader Configuration loader +type Loader func(log log.Logger) (name string, cfg Configuration, err error) diff --git a/application/configuration/loader_direct.go b/application/configuration/loader_direct.go new file mode 100644 index 0000000..008bd5d --- /dev/null +++ b/application/configuration/loader_direct.go @@ -0,0 +1,34 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package configuration + +import ( + "github.com/nirui/sshwifty/application/log" +) + +const ( + directTypeName = "Direct" +) + +// Direct creates a loader that return raw configuration data directly. +// Good for integration. +func Direct(cfg Configuration) Loader { + return func(log log.Logger) (string, Configuration, error) { + return directTypeName, cfg, nil + } +} diff --git a/application/configuration/loader_enviro.go b/application/configuration/loader_enviro.go new file mode 100644 index 0000000..5dc56b0 --- /dev/null +++ b/application/configuration/loader_enviro.go @@ -0,0 +1,139 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package configuration + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/nirui/sshwifty/application/log" +) + +const ( + enviroTypeName = "Environment Variable" +) + +func parseEnv(name string) string { + v := os.Getenv(name) + + if !strings.HasPrefix(v, "SSHWIFTY_ENV_RENAMED:") { + return v + } + + return os.Getenv(v[21:]) +} + +// Enviro creates an environment variable based configuration loader +func Enviro() Loader { + return func(log log.Logger) (string, Configuration, error) { + log.Info("Loading configuration from environment variables ...") + + dialTimeout, _ := strconv.ParseUint( + parseEnv("SSHWIFTY_DIALTIMEOUT"), 10, 32) + + cfg, cfgErr := fileCfgCommon{ + HostName: parseEnv("SSHWIFTY_HOSTNAME"), + SharedKey: parseEnv("SSHWIFTY_SHAREDKEY"), + DialTimeout: int(dialTimeout), + Socks5: parseEnv("SSHWIFTY_SOCKS5"), + Socks5User: parseEnv("SSHWIFTY_SOCKS5_USER"), + Socks5Password: parseEnv("SSHWIFTY_SOCKS5_PASSWORD"), + Servers: nil, + Presets: nil, + OnlyAllowPresetRemotes: len( + parseEnv("SSHWIFTY_ONLYALLOWPRESETREMOTES")) > 0, + }.build() + + if cfgErr != nil { + return enviroTypeName, Configuration{}, fmt.Errorf( + "failed to build the configuration: %s", cfgErr) + } + + listenIface := parseEnv("SSHWIFTY_LISTENINTERFACE") + + listenPort, _ := strconv.ParseUint( + parseEnv("SSHWIFTY_LISTENPORT"), 10, 16) + + initialTimeout, _ := strconv.ParseUint( + parseEnv("SSHWIFTY_INITIALTIMEOUT"), 10, 32) + + readTimeout, _ := strconv.ParseUint( + parseEnv("SSHWIFTY_READTIMEOUT"), 10, 32) + + writeTimeout, _ := strconv.ParseUint( + parseEnv("SSHWIFTY_WRITETIMEOUT"), 10, 32) + + heartbeatTimeout, _ := strconv.ParseUint( + parseEnv("SSHWIFTY_HEARTBEATTIMEOUT"), 10, 32) + + readDelay, _ := strconv.ParseUint( + parseEnv("SSHWIFTY_READDELAY"), 10, 32) + + writeDelay, _ := strconv.ParseUint( + parseEnv("SSHWIFTY_WRITEELAY"), 10, 32) + + cfgSer := fileCfgServer{ + ListenInterface: listenIface, + ListenPort: uint16(listenPort), + InitialTimeout: int(initialTimeout), + ReadTimeout: int(readTimeout), + WriteTimeout: int(writeTimeout), + HeartbeatTimeout: int(heartbeatTimeout), + ReadDelay: int(readDelay), + WriteDelay: int(writeDelay), + TLSCertificateFile: parseEnv("SSHWIFTY_TLSCERTIFICATEFILE"), + TLSCertificateKeyFile: parseEnv("SSHWIFTY_TLSCERTIFICATEKEYFILE"), + ServerMessage: parseEnv("SSHWIFTY_SERVERMESSAGE"), + } + + presets := make(fileCfgPresets, 0, 16) + presetStr := strings.TrimSpace(parseEnv("SSHWIFTY_PRESETS")) + + if len(presetStr) > 0 { + jErr := json.Unmarshal([]byte(presetStr), &presets) + + if jErr != nil { + return enviroTypeName, Configuration{}, fmt.Errorf( + "invalid \"SSHWIFTY_PRESETS\": %s", jErr) + } + } + + concretizePresets, err := presets.concretize() + + if err != nil { + return enviroTypeName, Configuration{}, fmt.Errorf( + "unable to parse Preset data: %s", err) + } + + return enviroTypeName, Configuration{ + HostName: cfg.HostName, + SharedKey: cfg.SharedKey, + DialTimeout: time.Duration(cfg.DialTimeout) * time.Second, + Socks5: cfg.Socks5, + Socks5User: cfg.Socks5User, + Socks5Password: cfg.Socks5Password, + Servers: []Server{cfgSer.build()}, + Presets: concretizePresets, + OnlyAllowPresetRemotes: cfg.OnlyAllowPresetRemotes, + }, nil + } +} diff --git a/application/configuration/loader_file.go b/application/configuration/loader_file.go new file mode 100644 index 0000000..9d2c2f3 --- /dev/null +++ b/application/configuration/loader_file.go @@ -0,0 +1,270 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package configuration + +import ( + "encoding/json" + "fmt" + "os" + "os/user" + "path" + "path/filepath" + "strings" + "time" + + "github.com/nirui/sshwifty/application/log" +) + +const ( + fileTypeName = "File" +) + +type fileCfgServer struct { + ListenInterface string // Interface to listen to + ListenPort uint16 // Port to listen + InitialTimeout int // Client initial request timeout, in second + ReadTimeout int // Read operation timeout, in second + WriteTimeout int // Write operation timeout, in second + HeartbeatTimeout int // Client heartbeat interval, in second + ReadDelay int // Read delay, in millisecond + WriteDelay int // Write delay, in millisecond + TLSCertificateFile string // Location of TLS certificate file + TLSCertificateKeyFile string // Location of TLS certificate key + ServerMessage string // Server message displayed on the Home page +} + +func (f *fileCfgServer) build() Server { + iface := f.ListenInterface + + if len(iface) <= 0 { + iface = "127.0.0.1" + } + + return Server{ + ListenInterface: iface, + ListenPort: f.ListenPort, + InitialTimeout: time.Duration( + durationAtLeast(f.InitialTimeout, 5)) * time.Second, + ReadTimeout: time.Duration( + durationAtLeast(f.ReadTimeout, 30)) * time.Second, + WriteTimeout: time.Duration( + durationAtLeast(f.WriteTimeout, 30)) * time.Second, + HeartbeatTimeout: time.Duration( + durationAtLeast(f.HeartbeatTimeout, 10)) * time.Second, + ReadDelay: time.Duration( + durationAtLeast(f.ReadDelay, 0)) * time.Millisecond, + WriteDelay: time.Duration( + durationAtLeast(f.WriteDelay, 0)) * time.Millisecond, + TLSCertificateFile: f.TLSCertificateFile, + TLSCertificateKeyFile: f.TLSCertificateKeyFile, + ServerMessage: f.ServerMessage, + } +} + +type fileCfgPreset struct { + Title string + Type string + Host string + Meta Meta +} + +func (f fileCfgPreset) concretize() (Preset, error) { + m, err := f.Meta.Concretize() + + if err != nil { + return Preset{}, err + } + + return Preset{ + Title: f.Title, + Type: strings.TrimSpace(f.Type), + Host: f.Host, + Meta: m, + }, nil +} + +type fileCfgPresets []fileCfgPreset + +func (f fileCfgPresets) concretize() ([]Preset, error) { + ps := make([]Preset, 0, len(f)) + + for i, p := range f { + pp, err := p.concretize() + + if err != nil { + return nil, fmt.Errorf( + "unable to concretize Preset %d (titled \"%s\"): %s", + i+1, p.Title, err) + } + + ps = append(ps, pp) + } + + return ps, nil +} + +type fileCfgCommon struct { + // Host name + HostName string + + // Shared key, empty to enable public access + SharedKey string + + // DialTimeout, min 5s + DialTimeout int + + // Socks5 server address, optional + Socks5 string + + // Login user for socks5 server, optional + Socks5User string + + // Login pass for socks5 server, optional + Socks5Password string + + // Servers + Servers []*fileCfgServer + + // Remotes + Presets fileCfgPresets + + // Allow predefined remotes only + OnlyAllowPresetRemotes bool +} + +func (f fileCfgCommon) build() (fileCfgCommon, error) { + return fileCfgCommon{ + HostName: f.HostName, + SharedKey: f.SharedKey, + DialTimeout: durationAtLeast(f.DialTimeout, 5), + Socks5: f.Socks5, + Socks5User: f.Socks5User, + Socks5Password: f.Socks5Password, + Servers: f.Servers, + Presets: f.Presets, + OnlyAllowPresetRemotes: f.OnlyAllowPresetRemotes, + }, nil +} + +func loadFile(filePath string) (string, Configuration, error) { + f, fErr := os.Open(filePath) + + if fErr != nil { + return fileTypeName, Configuration{}, fErr + } + + defer f.Close() + + cfg := fileCfgCommon{} + + jDecoder := json.NewDecoder(f) + jDecodeErr := jDecoder.Decode(&cfg) + + if jDecodeErr != nil { + return fileTypeName, Configuration{}, jDecodeErr + } + + finalCfg, cfgErr := cfg.build() + + if cfgErr != nil { + return fileTypeName, Configuration{}, cfgErr + } + + servers := make([]Server, len(finalCfg.Servers)) + + for i := range servers { + servers[i] = finalCfg.Servers[i].build() + } + + presets, err := finalCfg.Presets.concretize() + + if err != nil { + return fileTypeName, Configuration{}, err + } + + return fileTypeName, Configuration{ + HostName: finalCfg.HostName, + SharedKey: finalCfg.SharedKey, + DialTimeout: time.Duration(finalCfg.DialTimeout) * + time.Second, + Socks5: cfg.Socks5, + Socks5User: cfg.Socks5User, + Socks5Password: cfg.Socks5Password, + Servers: servers, + Presets: presets, + OnlyAllowPresetRemotes: cfg.OnlyAllowPresetRemotes, + }, nil +} + +// File creates a configuration file loader +func File(customPath string) Loader { + return func(log log.Logger) (string, Configuration, error) { + if len(customPath) > 0 { + log.Info("Loading configuration from: %s", customPath) + + return loadFile(customPath) + } + + log.Info("Loading configuration from one of the default " + + "configuration files ...") + + fallbackFileSearchList := make([]string, 0, 3) + + // ~/.config/sshwifty.conf.json + u, userErr := user.Current() + if userErr == nil { + fallbackFileSearchList = append( + fallbackFileSearchList, + path.Join(u.HomeDir, ".config", "sshwifty.conf.json")) + } + + // /etc/sshwifty.conf.json + fallbackFileSearchList = append( + fallbackFileSearchList, "/etc/sshwifty.conf.json") + + // sshwifty.conf.json located at the same directory as Sshwifty bin + ex, exErr := os.Executable() + if exErr == nil { + fallbackFileSearchList = append( + fallbackFileSearchList, + path.Join(filepath.Dir(ex), "sshwifty.conf.json")) + } + + for f := range fallbackFileSearchList { + fInfo, fErr := os.Stat(fallbackFileSearchList[f]) + + if fErr != nil { + continue + } + + if fInfo.IsDir() { + continue + } + + log.Info("Configuration file \"%s\" has been selected", + fallbackFileSearchList[f]) + + return loadFile(fallbackFileSearchList[f]) + } + + return fileTypeName, Configuration{}, fmt.Errorf( + "Configuration file was not specified. Also tried fallback files "+ + "\"%s\", but none of it was available", + strings.Join(fallbackFileSearchList, "\", \"")) + } +} diff --git a/application/configuration/loader_redundant.go b/application/configuration/loader_redundant.go new file mode 100644 index 0000000..5d5ad40 --- /dev/null +++ b/application/configuration/loader_redundant.go @@ -0,0 +1,52 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package configuration + +import ( + "fmt" + + "github.com/nirui/sshwifty/application/log" +) + +const ( + redundantTypeName = "Redundant" +) + +// Redundant creates a group of loaders. They will be executed one by one until +// one of it successfully returned a configuration +func Redundant(loaders ...Loader) Loader { + return func(log log.Logger) (string, Configuration, error) { + ll := log.Context("Redundant") + + for i := range loaders { + lLoaderName, lCfg, lErr := loaders[i](ll) + + if lErr != nil { + ll.Warning("Unable to load configuration from \"%s\": %s", + lLoaderName, lErr) + + continue + } + + return lLoaderName, lCfg, nil + } + + return redundantTypeName, Configuration{}, fmt.Errorf( + "all existing redundant loader has failed") + } +} diff --git a/application/configuration/string.go b/application/configuration/string.go new file mode 100644 index 0000000..c363ac1 --- /dev/null +++ b/application/configuration/string.go @@ -0,0 +1,79 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package configuration + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// String represents a config string +type String string + +// Parse parses current string and return the parsed result +func (s String) Parse() (string, error) { + ss := string(s) + + sSchemeLeadIdx := strings.Index(ss, "://") + + if sSchemeLeadIdx < 0 { + return ss, nil + } + + sSchemeLeadEnd := sSchemeLeadIdx + 3 + + switch strings.ToLower(ss[:sSchemeLeadIdx]) { + case "file": + fPath, e := filepath.Abs(ss[sSchemeLeadEnd:]) + + if e != nil { + return ss, e + } + + f, e := os.Open(fPath) + + if e != nil { + return "", fmt.Errorf("unable to open %s: %s", fPath, e) + } + + defer f.Close() + + fData, e := ioutil.ReadAll(f) + + if e != nil { + return "", fmt.Errorf("unable to read from %s: %s", fPath, e) + } + + return string(fData), nil + + case "enviroment": // You see what I did there. Remove this a later + fallthrough + case "environment": + return os.Getenv(ss[sSchemeLeadEnd:]), nil + + case "literal": + return ss[sSchemeLeadEnd:], nil + + default: + return "", fmt.Errorf( + "scheme \"%s\" was unsupported", ss[:sSchemeLeadIdx]) + } +} diff --git a/application/configuration/string_test.go b/application/configuration/string_test.go new file mode 100644 index 0000000..732f4a8 --- /dev/null +++ b/application/configuration/string_test.go @@ -0,0 +1,94 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package configuration + +import ( + "os" + "testing" +) + +func TestStringString(t *testing.T) { + ss := String("aaaaaaaaaaaaa") + + result, err := ss.Parse() + + if err != nil { + t.Error("Unable to parse:", err) + + return + } + + if result != "aaaaaaaaaaaaa" { + t.Errorf( + "Expecting the result to be %s, got %s instead", + "aaaaaaaaaaaaa", + result, + ) + + return + } +} + +func TestStringFile(t *testing.T) { + const testFilename = "sshwifty.configuration.test.string.file.tmp" + + filePath := os.TempDir() + string(os.PathSeparator) + testFilename + + f, err := os.Create(filePath) + + if err != nil { + t.Error("Unable to create file:", err) + + return + } + + defer os.Remove(filePath) + + f.WriteString("TestAAAA") + f.Close() + + ss := String("file://" + filePath) + + result, err := ss.Parse() + + if err != nil { + t.Error("Unable to parse:", err) + + return + } + + if result != "TestAAAA" { + t.Errorf( + "Expecting the result to be %s, got %s instead", + "TestAAAA", + result, + ) + + return + } + + ss = String("file://" + filePath + ".notexist") + + _, err = ss.Parse() + + if err == nil { + t.Error("Parsing an non-existing file should result an error") + + return + } +} diff --git a/application/controller/base.go b/application/controller/base.go new file mode 100644 index 0000000..1eff7ff --- /dev/null +++ b/application/controller/base.go @@ -0,0 +1,132 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package controller + +import ( + "net/http" + "strings" + + "github.com/nirui/sshwifty/application/log" +) + +// Error +var ( + ErrControllerNotImplemented = NewError( + http.StatusNotImplemented, "Server does not know how to handle the "+ + "request") +) + +type controller interface { + Get(w http.ResponseWriter, r *http.Request, l log.Logger) error + Head(w http.ResponseWriter, r *http.Request, l log.Logger) error + Post(w http.ResponseWriter, r *http.Request, l log.Logger) error + Put(w http.ResponseWriter, r *http.Request, l log.Logger) error + Delete(w http.ResponseWriter, r *http.Request, l log.Logger) error + Connect(w http.ResponseWriter, r *http.Request, l log.Logger) error + Options(w http.ResponseWriter, r *http.Request, l log.Logger) error + Trace(w http.ResponseWriter, r *http.Request, l log.Logger) error + Patch(w http.ResponseWriter, r *http.Request, l log.Logger) error + Other( + method string, + w http.ResponseWriter, + r *http.Request, + l log.Logger, + ) error +} + +type baseController struct{} + +func (b baseController) Get( + w http.ResponseWriter, r *http.Request, l log.Logger) error { + return ErrControllerNotImplemented +} + +func (b baseController) Head( + w http.ResponseWriter, r *http.Request, l log.Logger) error { + return ErrControllerNotImplemented +} + +func (b baseController) Post( + w http.ResponseWriter, r *http.Request, l log.Logger) error { + return ErrControllerNotImplemented +} + +func (b baseController) Put( + w http.ResponseWriter, r *http.Request, l log.Logger) error { + return ErrControllerNotImplemented +} + +func (b baseController) Delete( + w http.ResponseWriter, r *http.Request, l log.Logger) error { + return ErrControllerNotImplemented +} + +func (b baseController) Connect( + w http.ResponseWriter, r *http.Request, l log.Logger) error { + return ErrControllerNotImplemented +} + +func (b baseController) Options( + w http.ResponseWriter, r *http.Request, l log.Logger) error { + return ErrControllerNotImplemented +} + +func (b baseController) Trace( + w http.ResponseWriter, r *http.Request, l log.Logger) error { + return ErrControllerNotImplemented +} + +func (b baseController) Patch( + w http.ResponseWriter, r *http.Request, l log.Logger) error { + return ErrControllerNotImplemented +} + +func (b baseController) Other( + method string, w http.ResponseWriter, r *http.Request, l log.Logger) error { + return ErrControllerNotImplemented +} + +func serveController( + c controller, + w http.ResponseWriter, + r *http.Request, + l log.Logger, +) error { + switch strings.ToUpper(r.Method) { + case "GET": + return c.Get(w, r, l) + case "HEAD": + return c.Head(w, r, l) + case "POST": + return c.Post(w, r, l) + case "PUT": + return c.Put(w, r, l) + case "DELETE": + return c.Delete(w, r, l) + case "CONNECT": + return c.Connect(w, r, l) + case "OPTIONS": + return c.Options(w, r, l) + case "TRACE": + return c.Trace(w, r, l) + case "PATCH": + return c.Patch(w, r, l) + default: + return c.Other(r.Method, w, r, l) + } +} diff --git a/application/controller/common.go b/application/controller/common.go new file mode 100644 index 0000000..c13a299 --- /dev/null +++ b/application/controller/common.go @@ -0,0 +1,61 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package controller + +import ( + "net/http" + "regexp" + "strings" +) + +func clientSupportGZIP(r *http.Request) bool { + // Should be good enough + return strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") +} + +var ( + serverMessageFormatLink = regexp.MustCompile(`\[(.*?)\]\((.*?)\)`) +) + +func parseServerMessage(input string) (result string) { + // Yep, this is a new low, throwing regexp at a flat text format now...will + // rewrite the entire thing in a new version with a proper parser, maybe + // Con: Barely work when we only need to support exactly one text format + // Pro: Expecting a debugging battle, wrote the thing in one go instead + found := serverMessageFormatLink.FindAllStringSubmatchIndex(input, -1) + if len(found) <= 0 { + return input + } + currentStart := 0 + for _, f := range found { + if len(f) != 6 { // Expecting 6 parameters from the given expression + return input + } + segStart, segEnd, titleStart, titleEnd, linkStart, linkEnd := + f[0], f[1], f[2], f[3], f[4], f[5] + result += input[currentStart:segStart] + result += "" + + input[titleStart:titleEnd] + + "" + currentStart = segEnd + } + result += input[currentStart:] + return +} diff --git a/application/controller/common_test.go b/application/controller/common_test.go new file mode 100644 index 0000000..8cb769c --- /dev/null +++ b/application/controller/common_test.go @@ -0,0 +1,54 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package controller + +import ( + "html" + "testing" +) + +func TestParseServerMessage(t *testing.T) { + for _, test := range [][]string{ + { + "This is a [测试](http://nirui.org) " + + "[for link support](http://nirui.org).", + "<b>This is a " + + "测试 " + + "for link support" + + "</b>.", + }, + { + "[测试](http://nirui.org)", + "测试", + }, + { + "[测试](http://nirui.org).", + "测试.", + }, + { + ".[测试](http://nirui.org)", + ".测试", + }, + } { + result := parseServerMessage(html.EscapeString(test[0])) + if result != test[1] { + t.Errorf("Expecting %v, got %v instead", test[1], result) + return + } + } +} diff --git a/application/controller/controller.go b/application/controller/controller.go new file mode 100644 index 0000000..015b2ec --- /dev/null +++ b/application/controller/controller.go @@ -0,0 +1,172 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package controller + +import ( + "net/http" + "strings" + "time" + + "github.com/nirui/sshwifty/application/command" + "github.com/nirui/sshwifty/application/configuration" + "github.com/nirui/sshwifty/application/log" + "github.com/nirui/sshwifty/application/server" +) + +// Errors +var ( + ErrNotFound = NewError( + http.StatusNotFound, "Page not found") +) + +const ( + assetsURLPrefix = "/sshwifty/assets/" + assetsURLPrefixLen = len(assetsURLPrefix) +) + +// handler is the main service dispatcher +type handler struct { + hostNameChecker string + commonCfg configuration.Common + logger log.Logger + homeCtl home + socketCtl socket + socketVerifyCtl socketVerification +} + +func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var err error + + clientLogger := h.logger.Context("Client (%s)", r.RemoteAddr) + + if len(h.commonCfg.HostName) > 0 { + hostPort := r.Host + + if len(hostPort) <= 0 { + hostPort = r.URL.Host + } + + if h.commonCfg.HostName != hostPort && + !strings.HasPrefix(hostPort, h.hostNameChecker) { + clientLogger.Warning("Requested invalid host \"%s\", denied access", + r.Host) + + serveFailure( + NewError(http.StatusForbidden, "Invalid host"), w, r, h.logger) + + return + } + } + + w.Header().Add("Date", time.Now().UTC().Format(time.RFC1123)) + + switch r.URL.Path { + case "/": + err = serveController(h.homeCtl, w, r, clientLogger) + + case "/sshwifty/socket": + err = serveController(h.socketCtl, w, r, clientLogger) + case "/sshwifty/socket/verify": + err = serveController(h.socketVerifyCtl, w, r, clientLogger) + + case "/robots.txt": + err = serveStaticCacheData( + "robots.txt", + staticFileExt(".txt"), + w, + r, + clientLogger) + + case "/favicon.ico": + err = serveStaticCacheData( + "favicon.ico", + staticFileExt(".ico"), + w, + r, + clientLogger) + + case "/manifest.json": + err = serveStaticCacheData( + "manifest.json", + staticFileExt(".json"), + w, + r, + clientLogger) + + case "/browserconfig.xml": + err = serveStaticCacheData( + "browserconfig.xml", + staticFileExt(".xml"), + w, + r, + clientLogger) + + default: + if strings.HasPrefix(r.URL.Path, assetsURLPrefix) && + strings.ToUpper(r.Method) == "GET" { + err = serveStaticCacheData( + r.URL.Path[assetsURLPrefixLen:], + staticFileExt(r.URL.Path[assetsURLPrefixLen:]), + w, + r, + clientLogger) + } else { + err = ErrNotFound + } + } + + if err == nil { + clientLogger.Info("Request completed: %s", r.URL.String()) + + return + } + + clientLogger.Warning("Request ended with error: %s: %s", + r.URL.String(), err) + + controllerErr, isControllerErr := err.(Error) + + if isControllerErr { + serveFailure(controllerErr, w, r, h.logger) + + return + } + + serveFailure( + NewError(http.StatusInternalServerError, err.Error()), w, r, h.logger) +} + +// Builder returns a http controller builder +func Builder(cmds command.Commands) server.HandlerBuilder { + return func( + commonCfg configuration.Common, + cfg configuration.Server, + logger log.Logger, + ) http.Handler { + socketCtl := newSocketCtl(commonCfg, cfg, cmds) + + return handler{ + hostNameChecker: commonCfg.HostName + ":", + commonCfg: commonCfg, + logger: logger, + homeCtl: home{}, + socketCtl: socketCtl, + socketVerifyCtl: newSocketVerification(socketCtl, cfg, commonCfg), + } + } +} diff --git a/application/controller/error.go b/application/controller/error.go new file mode 100644 index 0000000..401fe10 --- /dev/null +++ b/application/controller/error.go @@ -0,0 +1,44 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package controller + +import "fmt" + +// Error Controller error +type Error struct { + code int + message string +} + +// NewError creates a new Error +func NewError(code int, message string) Error { + return Error{ + code: code, + message: message, + } +} + +// Code return the error code +func (f Error) Code() int { + return f.code +} + +// Error returns the error message +func (f Error) Error() string { + return fmt.Sprintf("HTTP Error (%d): %s", f.code, f.message) +} diff --git a/application/controller/failure.go b/application/controller/failure.go new file mode 100644 index 0000000..4f62450 --- /dev/null +++ b/application/controller/failure.go @@ -0,0 +1,33 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package controller + +import ( + "net/http" + + "github.com/nirui/sshwifty/application/log" +) + +func serveFailure( + err Error, + w http.ResponseWriter, + r *http.Request, + l log.Logger, +) error { + return serveStaticPage("error.html", err.Code(), w, r, l) +} diff --git a/application/controller/home.go b/application/controller/home.go new file mode 100644 index 0000000..7027d18 --- /dev/null +++ b/application/controller/home.go @@ -0,0 +1,33 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package controller + +import ( + "net/http" + + "github.com/nirui/sshwifty/application/log" +) + +// home controller +type home struct { + baseController +} + +func (h home) Get(w http.ResponseWriter, r *http.Request, l log.Logger) error { + return serveStaticPage("index.html", http.StatusOK, w, r, l) +} diff --git a/application/controller/socket.go b/application/controller/socket.go new file mode 100644 index 0000000..75d8b45 --- /dev/null +++ b/application/controller/socket.go @@ -0,0 +1,393 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package controller + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha512" + "fmt" + "io" + "net/http" + "strconv" + "sync" + "time" + + "github.com/gorilla/websocket" + + "github.com/nirui/sshwifty/application/command" + "github.com/nirui/sshwifty/application/configuration" + "github.com/nirui/sshwifty/application/log" + "github.com/nirui/sshwifty/application/rw" +) + +// Errors +var ( + ErrSocketInvalidAuthKey = NewError( + http.StatusForbidden, + "To use Websocket interface, a valid Auth Key must be provided") + + ErrSocketAuthFailed = NewError( + http.StatusForbidden, + "Authentication has failed with provided Auth Key") + + ErrSocketUnableToGenerateKey = NewError( + http.StatusInternalServerError, + "Unable to generate crypto key") + + ErrSocketInvalidDataPackage = NewError( + http.StatusBadRequest, "Invalid data package") +) + +const ( + socketGCMStandardNonceSize = 12 +) + +type socket struct { + baseController + + commonCfg configuration.Common + serverCfg configuration.Server + upgrader websocket.Upgrader + commander command.Commander +} + +func hashCombineSocketKeys(addedKey string, privateKey string) []byte { + h := hmac.New(sha512.New, []byte(privateKey)) + + h.Write([]byte(addedKey)) + + return h.Sum(nil) +} + +func newSocketCtl( + commonCfg configuration.Common, + cfg configuration.Server, + cmds command.Commands, +) socket { + return socket{ + commonCfg: commonCfg, + serverCfg: cfg, + upgrader: buildWebsocketUpgrader(cfg), + commander: command.New(cmds), + } +} + +type websocketWriter struct { + *websocket.Conn +} + +func (w websocketWriter) Write(b []byte) (int, error) { + wErr := w.WriteMessage(websocket.BinaryMessage, b) + + if wErr != nil { + return 0, wErr + } + + return len(b), nil +} + +type socketPackageWriter struct { + w websocketWriter + packager func(w websocketWriter, b []byte) error +} + +func (s socketPackageWriter) Write(b []byte) (int, error) { + packageWriteErr := s.packager(s.w, b) + + if packageWriteErr != nil { + return 0, packageWriteErr + } + + return len(b), nil +} + +func buildWebsocketUpgrader(cfg configuration.Server) websocket.Upgrader { + return websocket.Upgrader{ + HandshakeTimeout: cfg.InitialTimeout, + CheckOrigin: func(r *http.Request) bool { + return true + }, + Error: func( + w http.ResponseWriter, + r *http.Request, + status int, + reason error, + ) { + }, + } +} + +func (s socket) Options( + w http.ResponseWriter, r *http.Request, l log.Logger) error { + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Headers", "X-Key") + + return nil +} + +func (s socket) buildWSFetcher(c *websocket.Conn) rw.FetchReaderFetcher { + return func() ([]byte, error) { + for { + mt, message, err := c.ReadMessage() + + if err != nil { + return nil, err + } + + if mt != websocket.BinaryMessage { + return nil, NewError( + http.StatusBadRequest, + fmt.Sprintf("Received unknown type of data: %d", message)) + } + + return message, nil + } + } +} + +func (s socket) generateNonce(nonce []byte) error { + _, rErr := io.ReadFull(rand.Reader, nonce[:socketGCMStandardNonceSize]) + + return rErr +} + +func (s socket) increaseNonce(nonce []byte) { + for i := len(nonce); i > 0; i-- { + nonce[i-1]++ + + if nonce[i-1] <= 0 { + continue + } + + break + } +} + +func (s socket) createCipher(key []byte) (cipher.AEAD, cipher.AEAD, error) { + readCipher, readCipherErr := aes.NewCipher(key) + + if readCipherErr != nil { + return nil, nil, readCipherErr + } + + writeCipher, writeCipherErr := aes.NewCipher(key) + + if writeCipherErr != nil { + return nil, nil, writeCipherErr + } + + gcmRead, gcmReadErr := cipher.NewGCMWithNonceSize( + readCipher, socketGCMStandardNonceSize) + + if gcmReadErr != nil { + return nil, nil, gcmReadErr + } + + gcmWrite, gcmWriteErr := cipher.NewGCMWithNonceSize( + writeCipher, socketGCMStandardNonceSize) + + if gcmWriteErr != nil { + return nil, nil, gcmWriteErr + } + + return gcmRead, gcmWrite, nil +} + +func (s socket) mixerKey(r *http.Request) []byte { + return hashCombineSocketKeys( + r.UserAgent(), s.commonCfg.SharedKey+"+"+s.commonCfg.HostName) +} + +const keyTimeTruncater = 100 + +func (s socket) buildCipherKey(r *http.Request) [16]byte { + key := [16]byte{} + + copy(key[:], hashCombineSocketKeys( + strconv.FormatInt(time.Now().Unix()/keyTimeTruncater, 10), + string(s.mixerKey(r))+"+"+s.commonCfg.SharedKey, + )) + + return key +} + +func (s socket) Get( + w http.ResponseWriter, r *http.Request, l log.Logger) error { + // Error will not be returned when Websocket already handled + // (i.e. returned the error to client). We just log the error and that's it + c, err := s.upgrader.Upgrade(w, r, nil) + + if err != nil { + return NewError(http.StatusBadRequest, err.Error()) + } + + defer c.Close() + + wsReader := rw.NewFetchReader(s.buildWSFetcher(c)) + wsWriter := websocketWriter{Conn: c} + + // Initialize ciphers + // + // WARNING: The AES-GCM cipher is here for authenticating user login. Yeah + // it is overkill and probably not correct. But I eventually decide + // to keep it as long as it authenticates (Hopefully in a safe and + // secured way). + // + // DO NOT rely on this if you want to secured communitcation, in + // that case, you need to use HTTPS. + // + readNonce := [socketGCMStandardNonceSize]byte{} + _, nonceReadErr := io.ReadFull(&wsReader, readNonce[:]) + + if nonceReadErr != nil { + return NewError(http.StatusBadRequest, fmt.Sprintf( + "Unable to read initial client nonce: %s", nonceReadErr.Error())) + } + + writeNonce := [socketGCMStandardNonceSize]byte{} + nonceReadErr = s.generateNonce(writeNonce[:]) + + if nonceReadErr != nil { + return NewError(http.StatusBadRequest, fmt.Sprintf( + "Unable to generate initial server nonce: %s", + nonceReadErr.Error())) + } + + _, nonceSendErr := wsWriter.Write(writeNonce[:]) + + if nonceSendErr != nil { + return NewError(http.StatusBadRequest, fmt.Sprintf( + "Unable to send server nonce to client: %s", nonceSendErr.Error())) + } + + cipherKey := s.buildCipherKey(r) + + readCipher, writeCipher, cipherCreationErr := s.createCipher(cipherKey[:]) + + if cipherCreationErr != nil { + return NewError(http.StatusInternalServerError, fmt.Sprintf( + "Unable to create cipher: %s", cipherCreationErr.Error())) + } + + // Start service + const cipherReadBufSize = 4096 + + cipherReadBuf := [cipherReadBufSize]byte{} + cipherWriteBuf := [cipherReadBufSize]byte{} + maxWriteLen := int(cipherReadBufSize) - (writeCipher.Overhead() + 2) + + senderLock := sync.Mutex{} + cmdExec, cmdExecErr := s.commander.New( + command.Configuration{ + Dial: s.commonCfg.Dialer, + DialTimeout: s.commonCfg.DecideDialTimeout(s.serverCfg.ReadTimeout), + }, + rw.NewFetchReader(func() ([]byte, error) { + defer s.increaseNonce(readNonce[:]) + + // Size is unencrypted + _, rErr := io.ReadFull(&wsReader, cipherReadBuf[:2]) + + if rErr != nil { + return nil, rErr + } + + // Read full size + packageSize := uint16(cipherReadBuf[0]) + packageSize <<= 8 + packageSize |= uint16(cipherReadBuf[1]) + + if packageSize <= 0 || packageSize > cipherReadBufSize { + return nil, ErrSocketInvalidDataPackage + } + + if int(packageSize) <= wsReader.Remain() { + rData, rErr := wsReader.Export(int(packageSize)) + + if rErr != nil { + return nil, rErr + } + + return readCipher.Open( + cipherReadBuf[:0], readNonce[:], rData, nil) + } + + _, rErr = io.ReadFull(&wsReader, cipherReadBuf[:packageSize]) + + if rErr != nil { + return nil, rErr + } + + return readCipher.Open( + cipherReadBuf[:0], + readNonce[:], + cipherReadBuf[:packageSize], + nil) + }), + socketPackageWriter{ + w: wsWriter, + packager: func(w websocketWriter, b []byte) error { + start := 0 + bLen := len(b) + readLen := bLen + + for start < bLen { + if readLen > maxWriteLen { + readLen = maxWriteLen + } + + encrypted := writeCipher.Seal( + cipherWriteBuf[2:2], + writeNonce[:], + b[start:start+readLen], + nil) + + s.increaseNonce(writeNonce[:]) + + encryptedSize := uint16(len(encrypted)) + + if encryptedSize <= 0 { + return ErrSocketInvalidDataPackage + } + + cipherWriteBuf[0] = byte(encryptedSize >> 8) + cipherWriteBuf[1] = byte(encryptedSize) + + _, wErr := w.Write(cipherWriteBuf[:encryptedSize+2]) + + if wErr != nil { + return wErr + } + + start += readLen + readLen = bLen - start + } + + return nil + }, + }, &senderLock, s.serverCfg.ReadDelay, s.serverCfg.WriteDelay, l) + + if cmdExecErr != nil { + return NewError(http.StatusBadRequest, cmdExecErr.Error()) + } + + return cmdExec.Handle() +} diff --git a/application/controller/socket_verify.go b/application/controller/socket_verify.go new file mode 100644 index 0000000..551fc6f --- /dev/null +++ b/application/controller/socket_verify.go @@ -0,0 +1,180 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package controller + +import ( + "crypto/hmac" + "encoding/base64" + "encoding/json" + "fmt" + "html" + "net/http" + "strconv" + "time" + + "github.com/nirui/sshwifty/application/configuration" + "github.com/nirui/sshwifty/application/log" +) + +type socketVerification struct { + socket + + heartbeat string + timeout string + configRspBody []byte +} + +type socketRemotePreset struct { + Title string `json:"title"` + Type string `json:"type"` + Host string `json:"host"` + Meta map[string]string `json:"meta"` +} + +type socketAccessConfiguration struct { + Presets []socketRemotePreset `json:"presets"` + ServerMessage string `json:"server_message"` +} + +func newSocketAccessConfiguration( + remotes []configuration.Preset, + serverMessage string, +) socketAccessConfiguration { + presets := make([]socketRemotePreset, len(remotes)) + for i := range presets { + presets[i] = socketRemotePreset{ + Title: remotes[i].Title, + Type: remotes[i].Type, + Host: remotes[i].Host, + Meta: remotes[i].Meta, + } + } + return socketAccessConfiguration{ + Presets: presets, + ServerMessage: parseServerMessage(html.EscapeString(serverMessage)), + } +} + +func buildAccessConfigRespondBody(accessCfg socketAccessConfiguration) []byte { + mData, mErr := json.Marshal(accessCfg) + if mErr != nil { + panic(fmt.Errorf("unable to marshal remote data: %s", mErr)) + } + return mData +} + +func newSocketVerification( + s socket, + srvCfg configuration.Server, + commCfg configuration.Common, +) socketVerification { + return socketVerification{ + socket: s, + heartbeat: strconv.FormatFloat( + srvCfg.HeartbeatTimeout.Seconds(), 'g', 2, 64), + timeout: strconv.FormatFloat( + srvCfg.ReadTimeout.Seconds(), 'g', 2, 64), + configRspBody: buildAccessConfigRespondBody( + newSocketAccessConfiguration( + commCfg.Presets, + srvCfg.ServerMessage, + ), + ), + } +} + +func (s socketVerification) authKey(r *http.Request) []byte { + timeMixer := strconv.FormatInt(time.Now().Unix()/100, 10) + + if len(s.commonCfg.SharedKey) > 0 { + return hashCombineSocketKeys( + timeMixer, + s.commonCfg.SharedKey, + )[:32] + } + + return hashCombineSocketKeys( + timeMixer, + "DEFAULT VERIFY KEY", + )[:32] +} + +func (s socketVerification) setServerConfigRespond( + hd *http.Header, w http.ResponseWriter) { + hd.Add("X-Heartbeat", s.heartbeat) + hd.Add("X-Timeout", s.timeout) + + if s.commonCfg.OnlyAllowPresetRemotes { + hd.Add("X-OnlyAllowPresetRemotes", "yes") + } + + hd.Add("Content-Type", "text/json; charset=utf-8") + + w.Write(s.configRspBody) +} + +func (s socketVerification) Get( + w http.ResponseWriter, r *http.Request, l log.Logger) error { + hd := w.Header() + hd.Add("Cache-Control", "no-store") + hd.Add("Pragma", "no-store") + + key := r.Header.Get("X-Key") + + if len(key) <= 0 { + hd.Add("X-Key", base64.StdEncoding.EncodeToString(s.mixerKey(r))) + + if len(s.commonCfg.SharedKey) <= 0 { + s.setServerConfigRespond(&hd, w) + + return nil + } + + return ErrSocketInvalidAuthKey + } + + if len(key) > 64 { + return ErrSocketInvalidAuthKey + } + + // Delay the brute force attack. Use it with connection limits (via + // iptables or nginx etc) + time.Sleep(500 * time.Millisecond) + + decodedKey, decodedKeyErr := base64.StdEncoding.DecodeString(key) + + if decodedKeyErr != nil { + return NewError(http.StatusBadRequest, decodedKeyErr.Error()) + } + + authKey := s.authKey(r) + + if !hmac.Equal(authKey, decodedKey) { + return ErrSocketAuthFailed + } + + hd.Add("X-Key", base64.StdEncoding.EncodeToString(s.mixerKey(r))) + s.setServerConfigRespond(&hd, w) + + return nil +} + +func (s socketVerification) Options( + w http.ResponseWriter, r *http.Request, l log.Logger) error { + return nil +} diff --git a/application/controller/static.go b/application/controller/static.go new file mode 100644 index 0000000..de9b315 --- /dev/null +++ b/application/controller/static.go @@ -0,0 +1,125 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//go:generate go run ./static_page_generater ../../.tmp/dist ./static_pages.go +//go:generate go fmt ./static_pages.go + +package controller + +import ( + "net/http" + "strconv" + "strings" + "time" + + "github.com/nirui/sshwifty/application/log" +) + +type staticData struct { + data []byte + dataHash string + compressd []byte + compressdHash string + created time.Time + contentType string +} + +func (s staticData) hasCompressed() bool { + return len(s.compressd) > 0 +} + +func staticFileExt(fileName string) string { + extIdx := strings.LastIndex(fileName, ".") + if extIdx < 0 { + return "" + } + return strings.ToLower(fileName[extIdx:]) +} + +func serveStaticCacheData( + dataName string, + fileExt string, + w http.ResponseWriter, + r *http.Request, + l log.Logger, +) error { + if fileExt == ".html" || fileExt == ".htm" { + return ErrNotFound + } + return serveStaticCachePage(dataName, w, r, l) +} + +func serveStaticCachePage( + dataName string, + w http.ResponseWriter, + r *http.Request, + l log.Logger, +) error { + d, dFound := staticPages[dataName] + if !dFound { + return ErrNotFound + } + selectedData := d.data + selectedLength := len(d.data) + compressEnabled := false + if clientSupportGZIP(r) && d.hasCompressed() { + selectedData = d.compressd + selectedLength = len(d.compressd) + compressEnabled = true + w.Header().Add("Vary", "Accept-Encoding") + } + w.Header().Add("Cache-Control", "public, max-age=5184000") + w.Header().Add("Content-Type", d.contentType) + if compressEnabled { + w.Header().Add("Content-Encoding", "gzip") + } + w.Header().Add("Content-Length", + strconv.FormatInt(int64(selectedLength), 10)) + _, wErr := w.Write(selectedData) + return wErr +} + +func serveStaticPage( + dataName string, + code int, + w http.ResponseWriter, + r *http.Request, + l log.Logger, +) error { + d, dFound := staticPages[dataName] + if !dFound { + return ErrNotFound + } + selectedData := d.data + selectedLength := len(d.data) + compressEnabled := false + if clientSupportGZIP(r) && d.hasCompressed() { + selectedData = d.compressd + selectedLength = len(d.compressd) + compressEnabled = true + w.Header().Add("Vary", "Accept-Encoding") + } + w.Header().Add("Content-Type", d.contentType) + if compressEnabled { + w.Header().Add("Content-Encoding", "gzip") + } + w.Header().Add("Content-Length", + strconv.FormatInt(int64(selectedLength), 10)) + w.WriteHeader(code) + _, wErr := w.Write(selectedData) + return wErr +} diff --git a/application/controller/static_page_generater/main.go b/application/controller/static_page_generater/main.go new file mode 100644 index 0000000..f4728b6 --- /dev/null +++ b/application/controller/static_page_generater/main.go @@ -0,0 +1,414 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "mime" + "os" + "path/filepath" + "strconv" + "strings" + "text/template" + "time" +) + +const ( + parentPackage = "github.com/nirui/sshwifty/application/controller" +) + +const ( + staticListHeader = `// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package controller + +` + + staticListTemplate = `import "time" + +var ( + staticPages = map[string]staticData{ + {{ range . }}"{{ .Name }}": + parseStaticData({{ .GOPackage }}.{{ .GOVariableName }}()), + {{ end }} + } +) + +// parseStaticData parses result from a static file returner and generate +// a new ` + "`" + `staticData` + "`" + ` item +func parseStaticData( + fileStart int, + fileEnd int, + compressedStart int, + compressedEnd int, + creation time.Time, + data []byte, + contentType string, +) staticData { + return staticData{ + data: data[fileStart:fileEnd], + compressd: data[compressedStart:compressedEnd], + created: creation, + contentType: contentType, + } +} +` + + staticListTemplateDev = `import "io/ioutil" +import "bytes" +import "fmt" +import "compress/gzip" +import "time" +import "mime" +import "strings" + +// WARNING: THIS GENERATION IS FOR DEBUG / DEVELOPMENT ONLY, DO NOT +// USE IT IN PRODUCTION! + +func getMimeTypeByExtension(ext string) string { + switch ext { + case ".ico": + return "image/x-icon" + case ".md": + return "text/markdown" + default: + return mime.TypeByExtension(ext) + } +} + +func staticFileGen(fileName, filePath string) staticData { + content, readErr := ioutil.ReadFile(filePath) + if readErr != nil { + panic(fmt.Sprintln("Cannot read file:", readErr)) + } + compressed := bytes.NewBuffer(make([]byte, 0, 1024)) + compresser, compresserBuildErr := gzip.NewWriterLevel( + compressed, gzip.BestSpeed) + if compresserBuildErr != nil { + panic(fmt.Sprintln("Cannot build data compresser:", compresserBuildErr)) + } + contentLen := len(content) + _, compressErr := compresser.Write(content) + if compressErr != nil { + panic(fmt.Sprintln("Cannot write compressed data:", compressErr)) + } + compressErr = compresser.Flush() + if compressErr != nil { + panic(fmt.Sprintln("Cannot write compressed data:", compressErr)) + } + content = append(content, compressed.Bytes()...) + fileExtDotIdx := strings.LastIndex(fileName, ".") + fileExt := "" + if fileExtDotIdx >= 0 { + fileExt = fileName[fileExtDotIdx:len(fileName)] + } + mimeType := getMimeTypeByExtension(fileExt) + if len(mimeType) <= 0 { + mimeType = "application/binary" + } + return staticData{ + data: content[0:contentLen], + contentType: mimeType, + compressd: content[contentLen:], + created: time.Now(), + } +} + +var ( + staticPages = map[string]staticData{ + {{ range . }}"{{ .Name }}": staticFileGen( + "{{ .Name }}", "{{ .Path }}", + ), + {{ end }} + } +)` + + staticPageTemplate = `package {{ .GOPackage }} + +// This file is part of Sshwifty Project +// +// Copyright (C) {{ .Date.Year }} Ni Rui (ranqus@gmail.com) +// +// https://github.com/nirui/sshwifty +// +// This file is generated at {{ .Date.Format "Mon, 02 Jan 2006 15:04:05 MST" }} +// by "go generate", DO NOT EDIT! Also, do not open this file, it maybe too large +// for your editor. You've been warned. +// +// This file may contain third-party binaries. See DEPENDENCIES.md for detail. + +import ( + "time" +) + +// {{ .GOVariableName }} returns static file +func {{ .GOVariableName }}() ( + int, // FileStart + int, // FileEnd + int, // CompressedStart + int, // CompressedEnd + time.Time, // Time of creation + []byte, // Data + string, // ContentType +) { + created, createErr := time.Parse( + time.RFC1123, "{{ .Date.Format "Mon, 02 Jan 2006 15:04:05 MST" }}") + if createErr != nil { + panic(createErr) + } + return {{ .FileStart }}, {{ .FileEnd }}, + {{ .CompressedStart }}, {{ .CompressedEnd }}, + created, []byte({{ .Data }}), "{{ .ContentType }}" +} +` +) + +const ( + templateStarts = "//go:generate" +) + +type parsedFile struct { + Name string + GOVariableName string + GOFileName string + GOPackage string + Path string + Data string + Type string + FileStart int + FileEnd int + CompressedStart int + CompressedEnd int + ContentType string + Date time.Time +} + +func buildListFile(w io.Writer, data interface{}) error { + tpl := template.Must(template.New( + "StaticPageList").Parse(staticListTemplate)) + return tpl.Execute(w, data) +} + +func buildListFileDev(w io.Writer, data interface{}) error { + tpl := template.Must(template.New( + "StaticPageList").Parse(staticListTemplateDev)) + return tpl.Execute(w, data) +} + +func buildDataFile(w io.Writer, data interface{}) error { + tpl := template.Must(template.New( + "StaticPageData").Parse(staticPageTemplate)) + return tpl.Execute(w, data) +} + +func byteToQuotedString(b []byte) string { + return fmt.Sprintf("%q", b) +} + +func getMimeTypeByExtension(ext string) string { + switch ext { + case ".ico": + return "image/x-icon" + case ".md": + return "text/markdown" + case ".map": + return "text/plain" + case ".txt": + return "text/plain" + case ".woff": + return "application/font-woff" + case ".woff2": + return "application/font-woff2" + default: + return mime.TypeByExtension(ext) + } +} + +func parseFile( + id int, name string, filePath string, packageName string) parsedFile { + content, readErr := ioutil.ReadFile(filePath) + if readErr != nil { + panic(fmt.Sprintln("Cannot read file:", readErr)) + } + contentLen := len(content) + fileExtDotIdx := strings.LastIndex(name, ".") + fileExt := "" + if fileExtDotIdx >= 0 { + fileExt = name[fileExtDotIdx:] + } + mimeType := getMimeTypeByExtension(fileExt) + if len(mimeType) <= 0 { + mimeType = "application/binary" + } + if strings.HasPrefix(mimeType, "image/") { + // Don't compress images + } else if strings.HasPrefix(mimeType, "application/font-woff") { + // Don't compress web fonts + } else if mimeType == "text/plain" { + // Don't compress plain text + } else { + compressed := bytes.NewBuffer(make([]byte, 0, 1024)) + compresser, compresserBuildErr := gzip.NewWriterLevel( + compressed, gzip.BestCompression) + if compresserBuildErr != nil { + panic(fmt.Sprintln( + "Cannot build data compresser:", compresserBuildErr)) + } + _, compressErr := compresser.Write(content) + if compressErr != nil { + panic(fmt.Sprintln("Cannot write compressed data:", compressErr)) + } + compressErr = compresser.Flush() + if compressErr != nil { + panic(fmt.Sprintln("Cannot write compressed data:", compressErr)) + } + content = append(content, compressed.Bytes()...) + } + goFileName := "Static" + strconv.FormatInt(int64(id), 10) + return parsedFile{ + Name: name, + GOVariableName: strings.Title(goFileName), + GOFileName: strings.ToLower(goFileName) + "_generated.go", + GOPackage: packageName, + Path: filePath, + Data: byteToQuotedString(content), + FileStart: 0, + FileEnd: contentLen, + CompressedStart: contentLen, + CompressedEnd: len(content), + ContentType: mimeType, + Date: time.Now(), + } +} + +func main() { + if len(os.Args) < 3 { + panic("Usage: <(Destination) List File>") + } + sourcePath, sourcePathErr := filepath.Abs(os.Args[1]) + if sourcePathErr != nil { + panic(fmt.Sprintf("Invalid source folder path %s: %s", + os.Args[1], sourcePathErr)) + } + listFilePath, listFilePathErr := filepath.Abs(os.Args[2]) + if listFilePathErr != nil { + panic(fmt.Sprintf("Invalid destination list file path %s: %s", + os.Args[2], listFilePathErr)) + } + listFileName := filepath.Base(listFilePath) + destFolderPackage := strings.TrimSuffix( + listFileName, filepath.Ext(listFileName)) + destFolderPath := filepath.Join( + filepath.Dir(listFilePath), destFolderPackage) + destFolderPathErr := os.RemoveAll(destFolderPath) + if destFolderPathErr != nil { + panic(fmt.Sprintf("Unable to remove data destination folder %s: %s", + destFolderPath, destFolderPathErr)) + } + destFolderPathErr = os.Mkdir(destFolderPath, 0777) + if destFolderPathErr != nil { + panic(fmt.Sprintf("Unable to build data destination folder %s: %s", + destFolderPath, destFolderPathErr)) + } + listFile, listFileErr := os.Create(listFilePath) + if listFileErr != nil { + panic(fmt.Sprintf("Unable to open destination list file %s: %s", + listFilePath, listFileErr)) + } + defer listFile.Close() + files, dirOpenErr := ioutil.ReadDir(sourcePath) + if dirOpenErr != nil { + panic(fmt.Sprintf("Unable to open dir: %s", dirOpenErr)) + } + listFile.WriteString(staticListHeader) + listFile.WriteString("\n// This file is generated by `go generate` at " + + time.Now().Format(time.RFC1123) + "\n// DO NOT EDIT!\n\n") + switch os.Getenv("NODE_ENV") { + case "development": + type sourceFiles struct { + Name string + Path string + } + var sources []sourceFiles + for f := range files { + if !files[f].Mode().IsRegular() { + continue + } + sources = append(sources, sourceFiles{ + Name: files[f].Name(), + Path: filepath.Join(sourcePath, files[f].Name()), + }) + } + tempBuildErr := buildListFileDev(listFile, sources) + if tempBuildErr != nil { + panic(fmt.Sprintf( + "Unable to build destination file due to error: %s", + tempBuildErr)) + } + default: + var parsedFiles []parsedFile + for f := range files { + if !files[f].Mode().IsRegular() { + continue + } + currentFilePath := filepath.Join(sourcePath, files[f].Name()) + parsedFiles = append(parsedFiles, parseFile( + f, files[f].Name(), currentFilePath, destFolderPackage)) + } + for f := range parsedFiles { + fn := filepath.Join(destFolderPath, parsedFiles[f].GOFileName) + ff, ffErr := os.Create(fn) + if ffErr != nil { + panic(fmt.Sprintf("Unable to create static page file %s: %s", + fn, ffErr)) + } + bErr := buildDataFile(ff, parsedFiles[f]) + if bErr != nil { + panic(fmt.Sprintf("Unable to build static page file %s: %s", + fn, bErr)) + } + } + listFile.WriteString( + "\nimport \"" + parentPackage + "/" + destFolderPackage + "\"\n") + tempBuildErr := buildListFile(listFile, parsedFiles) + if tempBuildErr != nil { + panic(fmt.Sprintf( + "Unable to build destination file due to error: %s", + tempBuildErr)) + } + } +} diff --git a/application/log/ditch.go b/application/log/ditch.go new file mode 100644 index 0000000..1105dcf --- /dev/null +++ b/application/log/ditch.go @@ -0,0 +1,48 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package log + +// Ditch ditch all logs +type Ditch struct{} + +// NewDitch creates a new Ditch +func NewDitch() Ditch { + return Ditch{} +} + +// Context build a new Sub context +func (w Ditch) Context(name string, params ...interface{}) Logger { + return w +} + +// Write writes default error +func (w Ditch) Write(b []byte) (int, error) { + return len(b), nil +} + +// Info write an info message +func (w Ditch) Info(msg string, params ...interface{}) {} + +// Debug write an debug message +func (w Ditch) Debug(msg string, params ...interface{}) {} + +// Warning write an warning message +func (w Ditch) Warning(msg string, params ...interface{}) {} + +// Error write an error message +func (w Ditch) Error(msg string, params ...interface{}) {} diff --git a/application/log/log.go b/application/log/log.go new file mode 100644 index 0000000..73c1c9b --- /dev/null +++ b/application/log/log.go @@ -0,0 +1,28 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package log + +// Logger represents a logger +type Logger interface { + Context(name string, params ...interface{}) Logger + Write(b []byte) (int, error) + Info(msg string, params ...interface{}) + Debug(msg string, params ...interface{}) + Warning(msg string, params ...interface{}) + Error(msg string, params ...interface{}) +} diff --git a/application/log/writer.go b/application/log/writer.go new file mode 100644 index 0000000..81e8821 --- /dev/null +++ b/application/log/writer.go @@ -0,0 +1,80 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package log + +import ( + "fmt" + "io" + "time" +) + +// Writer will write logs to the underlaying writer +type Writer struct { + c string + w io.Writer +} + +// NewWriter creates a new Writer +func NewWriter(context string, w io.Writer) Writer { + return Writer{ + c: context, + w: w, + } +} + +// Context build a new Sub context +func (w Writer) Context(name string, params ...interface{}) Logger { + return NewWriter(w.c+" > "+fmt.Sprintf(name, params...), w.w) +} + +// Write writes default error +func (w Writer) Write(b []byte) (int, error) { + _, wErr := w.write("DEF", string(b)) + + if wErr != nil { + return 0, wErr + } + + return len(b), nil +} + +func (w Writer) write( + prefix string, msg string, params ...interface{}) (int, error) { + return fmt.Fprintf(w.w, "["+prefix+"] "+ + time.Now().Format(time.RFC1123)+" "+w.c+": "+msg+"\r\n", params...) +} + +// Info write an info message +func (w Writer) Info(msg string, params ...interface{}) { + w.write("INF", msg, params...) +} + +// Debug write an debug message +func (w Writer) Debug(msg string, params ...interface{}) { + w.write("DBG", msg, params...) +} + +// Warning write an warning message +func (w Writer) Warning(msg string, params ...interface{}) { + w.write("WRN", msg, params...) +} + +// Error write an error message +func (w Writer) Error(msg string, params ...interface{}) { + w.write("ERR", msg, params...) +} diff --git a/application/log/writer_nodebug.go b/application/log/writer_nodebug.go new file mode 100644 index 0000000..72866dc --- /dev/null +++ b/application/log/writer_nodebug.go @@ -0,0 +1,54 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package log + +import ( + "fmt" + "io" +) + +// NonDebugWriter will write logs to the underlaying writer +type NonDebugWriter struct { + Writer +} + +// NewNonDebugWriter creates a new Writer with debug output disabled +func NewNonDebugWriter(context string, w io.Writer) NonDebugWriter { + return NonDebugWriter{ + Writer: NewWriter(context, w), + } +} + +// NewDebugOrNonDebugWriter creates debug or nondebug log depends on +// given `useDebug` +func NewDebugOrNonDebugWriter( + useDebug bool, context string, w io.Writer) Logger { + if useDebug { + return NewWriter(context, w) + } + + return NewNonDebugWriter(context, w) +} + +// Context build a new Sub context +func (w NonDebugWriter) Context(name string, params ...interface{}) Logger { + return NewNonDebugWriter(w.c+" > "+fmt.Sprintf(name, params...), w.w) +} + +// Debug ditchs debug operation +func (w NonDebugWriter) Debug(msg string, params ...interface{}) {} diff --git a/application/network/conn.go b/application/network/conn.go new file mode 100644 index 0000000..42fbd37 --- /dev/null +++ b/application/network/conn.go @@ -0,0 +1,26 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package network + +import ( + "time" +) + +var ( + emptyTime = time.Time{} +) diff --git a/application/network/conn_timeout.go b/application/network/conn_timeout.go new file mode 100644 index 0000000..becdb02 --- /dev/null +++ b/application/network/conn_timeout.go @@ -0,0 +1,221 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package network + +import ( + "net" + "time" +) + +// TimeoutConn read write +type TimeoutConn struct { + net.Conn + + readTimeout time.Duration + disableNextReadTimeout bool + writeTimeout time.Duration + disableNextWriteTimeout bool +} + +// NewTimeoutConn creates a new TimeoutConn +func NewTimeoutConn( + c net.Conn, + rTimeout time.Duration, + wTimeout time.Duration, +) TimeoutConn { + return TimeoutConn{ + Conn: c, + readTimeout: rTimeout, + disableNextReadTimeout: false, + writeTimeout: wTimeout, + disableNextWriteTimeout: false, + } +} + +// SetReadTimeout sets read timeout +func (c *TimeoutConn) SetReadTimeout(t time.Duration) { + c.readTimeout = t +} + +// SetReadDeadline sets the next read deadline +func (c *TimeoutConn) SetReadDeadline(t time.Time) error { + c.disableNextReadTimeout = t.Before(time.Now()) + + if t.Equal(emptyTime) { + return c.Conn.SetReadDeadline(time.Now().Add(c.readTimeout)) + } + + return c.Conn.SetReadDeadline(t) +} + +// Read reads data +func (c *TimeoutConn) Read(b []byte) (int, error) { + defer func() { + c.disableNextReadTimeout = false + }() + + cLen, cErr := c.Conn.Read(b) + + if cErr == nil { + return cLen, nil + } + + netErr, isNetErr := cErr.(net.Error) + + if !isNetErr || + c.disableNextReadTimeout || + c.readTimeout <= 0 || + !netErr.Timeout() { + return cLen, cErr + } + + cErr = c.Conn.SetReadDeadline(time.Now().Add(c.readTimeout)) + + if cErr != nil { + return cLen, cErr + } + + tryCLen, cErr := c.Conn.Read(b[cLen:]) + + return tryCLen + cLen, cErr +} + +// SetWriteTimeout sets write timeout +func (c *TimeoutConn) SetWriteTimeout(t time.Duration) { + c.writeTimeout = t +} + +// SetWriteDeadline sets the next read deadline +func (c *TimeoutConn) SetWriteDeadline(t time.Time) error { + c.disableNextWriteTimeout = t.Before(time.Now()) + + if t.Equal(emptyTime) { + return c.Conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) + } + + return c.Conn.SetWriteDeadline(t) +} + +// Write writes data +func (c *TimeoutConn) Write(b []byte) (int, error) { + defer func() { + c.disableNextWriteTimeout = false + }() + + cLen, cErr := c.Conn.Write(b) + + if cErr == nil { + return cLen, nil + } + + netErr, isNetErr := cErr.(net.Error) + + if !isNetErr || + c.disableNextWriteTimeout || + c.writeTimeout <= 0 || + !netErr.Timeout() { + return cLen, cErr + } + + cErr = c.Conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) + + if cErr != nil { + return cLen, cErr + } + + tryCLen, cErr := c.Conn.Write(b[cLen:]) + + return tryCLen + cLen, cErr +} + +// SetDeadline sets read and write deadline +func (c *TimeoutConn) SetDeadline(t time.Time) error { + c.SetReadDeadline(t) + c.SetWriteDeadline(t) + + return nil +} + +// ReadTimeoutConn is a reader that will enforce a timeout rules +type ReadTimeoutConn struct { + net.Conn + + reader TimeoutConn +} + +// NewReadTimeoutConn creates a ReadTimeoutConn +func NewReadTimeoutConn(c net.Conn, timeout time.Duration) ReadTimeoutConn { + return ReadTimeoutConn{ + Conn: c, + reader: TimeoutConn{ + Conn: c, + readTimeout: timeout, + writeTimeout: 0, + }, + } +} + +// SetReadDeadline sets read deadline +func (c *ReadTimeoutConn) SetReadDeadline(t time.Time) error { + return c.reader.SetReadDeadline(t) +} + +// SetReadTimeout sets write timeout +func (c *ReadTimeoutConn) SetReadTimeout(t time.Duration) { + c.reader.SetReadTimeout(t) +} + +// Read writes data +func (c ReadTimeoutConn) Read(b []byte) (int, error) { + return c.reader.Read(b) +} + +// WriteTimeoutConn is a writer that will enforce a timeout rules onto a +// net.Conn +type WriteTimeoutConn struct { + net.Conn + + writer TimeoutConn +} + +// NewWriteTimeoutConn creates a WriteTimeoutConnWriter +func NewWriteTimeoutConn(c net.Conn, timeout time.Duration) WriteTimeoutConn { + return WriteTimeoutConn{ + Conn: c, + writer: TimeoutConn{ + Conn: c, + readTimeout: 0, + writeTimeout: timeout, + }, + } +} + +// SetWriteDeadline sets write deadline +func (c *WriteTimeoutConn) SetWriteDeadline(t time.Time) error { + return c.writer.SetWriteDeadline(t) +} + +// SetWriteTimeout sets write timeout +func (c *WriteTimeoutConn) SetWriteTimeout(t time.Duration) { + c.writer.SetWriteTimeout(t) +} + +// Write writes data +func (c WriteTimeoutConn) Write(b []byte) (int, error) { + return c.writer.Write(b) +} diff --git a/application/network/dial.go b/application/network/dial.go new file mode 100644 index 0000000..f34d457 --- /dev/null +++ b/application/network/dial.go @@ -0,0 +1,38 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package network + +import ( + "net" + "time" +) + +// Dial dial to remote machine +type Dial func( + network string, address string, timeout time.Duration) (net.Conn, error) + +// TCPDial build a TCP dialer +func TCPDial() Dial { + return func( + network string, + address string, + timeout time.Duration, + ) (net.Conn, error) { + return net.DialTimeout(network, address, timeout) + } +} diff --git a/application/network/dial_ac.go b/application/network/dial_ac.go new file mode 100644 index 0000000..f50e8ab --- /dev/null +++ b/application/network/dial_ac.go @@ -0,0 +1,60 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package network + +import ( + "errors" + "net" + "time" +) + +// Errors +var ( + ErrAccessControlDialTargetHostNotAllowed = errors.New( + "unable to dial to the specified remote host due to restriction") +) + +// AllowedHosts contains a map of allowed remote hosts +type AllowedHosts map[string]struct{} + +// Allowed returns whether or not given host is allowed +func (a AllowedHosts) Allowed(host string) bool { + _, ok := a[host] + + return ok +} + +// AllowedHost returns whether or not give host is allowed +type AllowedHost interface { + Allowed(host string) bool +} + +// AccessControlDial creates an access controlled Dial +func AccessControlDial(allowed AllowedHost, dial Dial) Dial { + return func( + network string, + address string, + timeout time.Duration, + ) (net.Conn, error) { + if !allowed.Allowed(address) { + return nil, ErrAccessControlDialTargetHostNotAllowed + } + + return dial(network, address, timeout) + } +} diff --git a/application/network/dial_socks5.go b/application/network/dial_socks5.go new file mode 100644 index 0000000..0f90fc5 --- /dev/null +++ b/application/network/dial_socks5.go @@ -0,0 +1,94 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package network + +import ( + "context" + "net" + "time" + + "golang.org/x/net/proxy" +) + +type socks5Dial struct { + net.Dialer +} + +func (s socks5Dial) Dial( + network, address string) (net.Conn, error) { + conn, dErr := s.Dialer.Dial(network, address) + + if dErr == nil { + conn.SetReadDeadline(time.Now().Add(s.Dialer.Timeout)) + } + + return conn, dErr +} + +func (s socks5Dial) DialContext( + ctx context.Context, network, address string) (net.Conn, error) { + conn, dErr := s.Dialer.DialContext(ctx, network, address) + + if dErr == nil { + conn.SetReadDeadline(time.Now().Add(s.Dialer.Timeout)) + } + + return conn, dErr +} + +// BuildSocks5Dial builds a Socks5 dialer +func BuildSocks5Dial( + socks5Address string, userName string, password string) (Dial, error) { + var auth *proxy.Auth + + if len(userName) > 0 || len(password) > 0 { + auth = &proxy.Auth{ + User: userName, + Password: password, + } + } + + return func( + network string, + address string, + timeout time.Duration, + ) (net.Conn, error) { + dialCfg := socks5Dial{ + Dialer: net.Dialer{ + Timeout: timeout, + Deadline: time.Now().Add(timeout), + }, + } + + dial, dialErr := proxy.SOCKS5("tcp", socks5Address, auth, &dialCfg) + + if dialErr != nil { + return nil, dialErr + } + + dialConn, dialErr := dial.Dial(network, address) + + if dialErr != nil { + return nil, dialErr + } + + dialConn.SetReadDeadline(emptyTime) + + return dialConn, nil + }, nil +} diff --git a/application/plate.go b/application/plate.go new file mode 100644 index 0000000..ad83f71 --- /dev/null +++ b/application/plate.go @@ -0,0 +1,36 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package application + +// Plate information +const ( + Name = "Sshwifty" + FullName = "Sshwifty Web SSH Client" + Author = "Ni Rui " + URL = "https://github.com/nirui/sshwifty" +) + +// Banner message +const ( + banner = "\r\n %s %s\r\n\r\n Copyright (C) %s\r\n %s\r\n\r\n" +) + +// Version +var ( + version = "dev" +) diff --git a/application/rw/fetch.go b/application/rw/fetch.go new file mode 100644 index 0000000..3b83834 --- /dev/null +++ b/application/rw/fetch.go @@ -0,0 +1,146 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package rw + +import "errors" + +// Errors +var ( + ErrFetchReaderNotEnoughBuffer = errors.New( + "not enough buffer") +) + +// FetchReaderFetcher generates data for SourceReader +type FetchReaderFetcher func() ([]byte, error) + +// FetchReader read from the source and increase your lifespan if used correctly +type FetchReader struct { + source FetchReaderFetcher // Source data fetcher + data []byte // Fetched source data + dataUsed int // Used source data +} + +// Fetch fetchs +type Fetch func(n int) ([]byte, error) + +// FetchOneByte fetchs one byte from the Fetch, or return an error when it fails +func FetchOneByte(f Fetch) ([]byte, error) { + for { + d, dErr := f(1) + + if dErr != nil { + return nil, dErr + } + + if len(d) <= 0 { + continue + } + + return d, nil + } +} + +// NewFetchReader creates a new FetchReader +func NewFetchReader(g FetchReaderFetcher) FetchReader { + return FetchReader{ + source: g, + data: nil, + dataUsed: 0, + } +} + +func (r FetchReader) dataRemain() int { + return len(r.data) - r.dataUsed +} + +// Remain Returns how many bytes is waiting to be readed +func (r *FetchReader) Remain() int { + return r.dataRemain() +} + +// Export directly exports from buffer, never read from source +// +// Params: +// - n: Exact amount of bytes to fetch (0 to n, n included). If number n is +// unreachable, an error will be returned, and no internal status will +// be changed +// +// Returns: +// - Fetched data +// - Read error +func (r *FetchReader) Export(n int) ([]byte, error) { + remain := r.dataRemain() + + if n > remain { + return nil, ErrFetchReaderNotEnoughBuffer + } + + newUsed := r.dataUsed + n + data := r.data[r.dataUsed:newUsed] + + r.dataUsed = newUsed + + return data, nil +} + +// Fetch fetchs data from the source +// +// Params: +// - n: Max bytes to fetch (0 to n, n included) +// +// Returns: +// - Fetched data +// - Read error +func (r *FetchReader) Fetch(n int) ([]byte, error) { + remain := r.dataRemain() + + if remain <= 0 { + data, dataFetchErr := r.source() + + if dataFetchErr != nil { + return nil, dataFetchErr + } + + r.data = data + r.dataUsed = 0 + + remain = r.dataRemain() + } + + if n > remain { + n = remain + } + + newUsed := r.dataUsed + n + data := r.data[r.dataUsed:newUsed] + + r.dataUsed = newUsed + + return data, nil +} + +// Read implements io.Read +func (r *FetchReader) Read(b []byte) (int, error) { + d, dErr := r.Fetch(len(b)) + + if dErr != nil { + return 0, dErr + } + + return copy(b, d), nil +} diff --git a/application/rw/fetch_test.go b/application/rw/fetch_test.go new file mode 100644 index 0000000..39f4991 --- /dev/null +++ b/application/rw/fetch_test.go @@ -0,0 +1,59 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package rw + +import ( + "bytes" + "io" + "testing" +) + +func testDummyFetchGen(data []byte) FetchReaderFetcher { + current := 0 + + return func() ([]byte, error) { + if current >= len(data) { + return nil, io.EOF + } + + oldCurrent := current + current = oldCurrent + 1 + + return data[oldCurrent:current], nil + } +} + +func TestFetchReader(t *testing.T) { + r := NewFetchReader(testDummyFetchGen([]byte("Hello World"))) + b := make([]byte, 11) + + _, rErr := io.ReadFull(&r, b) + + if rErr != nil { + t.Error("Failed to read due to error:", rErr) + + return + } + + if !bytes.Equal(b, []byte("Hello World")) { + t.Errorf("Expecting data to be %s, got %s instead", + []byte("Hello World"), b) + + return + } +} diff --git a/application/rw/limited.go b/application/rw/limited.go new file mode 100644 index 0000000..161c61d --- /dev/null +++ b/application/rw/limited.go @@ -0,0 +1,130 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package rw + +import ( + "errors" + "io" +) + +// Errors +var ( + ErrReadUntilCompletedBufferFull = errors.New( + "cannot read more, not enough data buffer") +) + +// LimitedReader reads only n bytes of data +type LimitedReader struct { + r *FetchReader + n int +} + +// ReadUntilCompleted read until the reader is completed +func ReadUntilCompleted(r *LimitedReader, b []byte) (int, error) { + bCur := 0 + bLen := len(b) + + for !r.Completed() { + if bCur >= bLen { + return bCur, ErrReadUntilCompletedBufferFull + } + + rLen, rErr := r.Read(b[bCur:]) + + if rErr != nil { + return bCur + rLen, rErr + } + + bCur += rLen + } + + return bCur, nil +} + +// NewLimitedReader creates a new LimitedReader +func NewLimitedReader(r *FetchReader, n int) LimitedReader { + return LimitedReader{ + r: r, + n: n, + } +} + +// Buffered exports the internal buffer +func (l *LimitedReader) Buffered() ([]byte, error) { + return l.Fetch(l.Remains()) +} + +// Fetch fetchs max n bytes from buffer +func (l *LimitedReader) Fetch(n int) ([]byte, error) { + if l.Completed() { + return nil, io.EOF + } + + if n > l.Remains() { + n = l.Remains() + } + + exported, eErr := l.r.Fetch(n) + + l.n -= len(exported) + + return exported, eErr +} + +// Read read from the LimitedReader +func (l *LimitedReader) Read(b []byte) (int, error) { + if l.Completed() { + return 0, io.EOF + } + + toRead := len(b) + + if toRead > l.Remains() { + toRead = l.Remains() + } + + rLen, rErr := l.r.Read(b[:toRead]) + + l.n -= rLen + + return rLen, rErr +} + +// Ditch ditchs all remaining data. Data will be written and overwritten to +// the given buf when ditching +func (l *LimitedReader) Ditch(buf []byte) error { + for !l.Completed() { + _, rErr := l.Read(buf) + + if rErr != nil { + return rErr + } + } + + return nil +} + +// Remains returns how many bytes is waiting to be read +func (l LimitedReader) Remains() int { + return l.n +} + +// Completed returns whether or not current reader is completed +func (l LimitedReader) Completed() bool { + return l.n <= 0 +} diff --git a/application/rw/rw.go b/application/rw/rw.go new file mode 100644 index 0000000..f17748d --- /dev/null +++ b/application/rw/rw.go @@ -0,0 +1,41 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package rw + +// ReaderFunc function of io.Reader +type ReaderFunc func(b []byte) (int, error) + +// ReadFull Read until given b is fully loaded +func ReadFull(r ReaderFunc, b []byte) (int, error) { + bLen := len(b) + readed := 0 + + for { + rLen, rErr := r(b[readed:]) + + readed += rLen + + if rErr != nil { + return readed, rErr + } + + if readed >= bLen { + return readed, nil + } + } +} diff --git a/application/server/conn.go b/application/server/conn.go new file mode 100644 index 0000000..7c3ec72 --- /dev/null +++ b/application/server/conn.go @@ -0,0 +1,85 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package server + +import ( + "net" + "time" + + "github.com/nirui/sshwifty/application/network" +) + +type listener struct { + *net.TCPListener + + readTimeout time.Duration + writeTimeout time.Duration +} + +func (l listener) Accept() (net.Conn, error) { + acc, accErr := l.TCPListener.Accept() + + if accErr != nil { + return nil, accErr + } + + timeoutConn := network.NewTimeoutConn(acc, l.readTimeout, l.writeTimeout) + + return conn{ + TimeoutConn: &timeoutConn, + readTimeout: l.readTimeout, + writeTimeout: l.writeTimeout, + }, nil +} + +// conn is a net.Conn hack, we use it prevent the upper to alter some important +// configuration of the connection, mainly the timeouts. +type conn struct { + *network.TimeoutConn + + readTimeout time.Duration + writeTimeout time.Duration +} + +func (c conn) normalizeTimeout(t time.Time, m time.Duration) time.Time { + max := time.Now().Add(m) + + // You cannot set timeout that is longer than the given m + if t.After(max) { + return max + } + + return t +} + +func (c conn) SetDeadline(dl time.Time) error { + c.SetReadDeadline(dl) + c.SetWriteDeadline(dl) + + return nil +} + +func (c conn) SetReadDeadline(dl time.Time) error { + return c.TimeoutConn.SetReadDeadline( + c.normalizeTimeout(dl, c.readTimeout)) +} + +func (c conn) SetWriteDeadline(dl time.Time) error { + return c.TimeoutConn.SetWriteDeadline( + c.normalizeTimeout(dl, c.writeTimeout)) +} diff --git a/application/server/server.go b/application/server/server.go new file mode 100644 index 0000000..3cd8c89 --- /dev/null +++ b/application/server/server.go @@ -0,0 +1,180 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package server + +import ( + "context" + "crypto/tls" + "errors" + goLog "log" + "net" + "net/http" + "strconv" + "sync" + "time" + + "github.com/nirui/sshwifty/application/command" + "github.com/nirui/sshwifty/application/configuration" + "github.com/nirui/sshwifty/application/log" +) + +type dumpWrite struct{} + +func (d dumpWrite) Write(b []byte) (int, error) { + return len(b), nil +} + +// Errors +var ( + ErrInvalidIPAddress = errors.New( + "invalid IP address") +) + +// HandlerBuilder builds a HTTP handler +type HandlerBuilder func( + commonCfg configuration.Common, + cfg configuration.Server, + logger log.Logger) http.Handler + +// HandlerBuilderBuilder builds HandlerBuilder +type HandlerBuilderBuilder func(command.Commands) HandlerBuilder + +// CloseCallback will be called when the server has closed +type CloseCallback func(error) + +// Server represents a server +type Server struct { + logger log.Logger + shutdownWait *sync.WaitGroup +} + +// Serving represents a server that is serving for requests +type Serving struct { + server http.Server + shutdownWait *sync.WaitGroup +} + +// New creates a new Server builder +func New(logger log.Logger) Server { + return Server{ + logger: logger, + shutdownWait: &sync.WaitGroup{}, + } +} + +// Serve starts serving +func (s Server) Serve( + commonCfg configuration.Common, + serverCfg configuration.Server, + closeCallback CloseCallback, + handlerBuilder HandlerBuilder, +) *Serving { + ssCfg := serverCfg.WithDefault() + l := s.logger.Context( + "Server (%s:%d)", ssCfg.ListenInterface, ssCfg.ListenPort) + ss := &Serving{ + server: http.Server{ + Handler: handlerBuilder(commonCfg, ssCfg, l), + TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}, + ReadTimeout: ssCfg.ReadTimeout, + ReadHeaderTimeout: ssCfg.InitialTimeout, + WriteTimeout: ssCfg.WriteTimeout, + IdleTimeout: ssCfg.ReadTimeout, + MaxHeaderBytes: http.DefaultMaxHeaderBytes, + ErrorLog: goLog.New(dumpWrite{}, "", 0), + }, + shutdownWait: s.shutdownWait, + } + s.shutdownWait.Add(1) + go ss.run(l, ssCfg, closeCallback) + return ss +} + +// Wait waits until all server is closed +func (s Server) Wait() { + s.shutdownWait.Wait() +} + +func (s *Serving) buildListener( + ip string, + port uint16, + readTimeout time.Duration, + writeTimeout time.Duration, +) (listener, error) { + ipAddr := net.ParseIP(ip) + if ipAddr == nil { + return listener{}, ErrInvalidIPAddress + } + ipPort := net.JoinHostPort( + ipAddr.String(), strconv.FormatInt(int64(port), 10)) + addr, addrErr := net.ResolveTCPAddr("tcp", ipPort) + if addrErr != nil { + return listener{}, addrErr + } + ll, llErr := net.ListenTCP("tcp", addr) + if llErr != nil { + return listener{}, llErr + } + return listener{ + TCPListener: ll, + readTimeout: readTimeout, + writeTimeout: writeTimeout, + }, nil +} + +// run starts the server +func (s *Serving) run( + logger log.Logger, + cfg configuration.Server, + closeCallback CloseCallback, +) error { + var err error + defer func() { + if err == nil || err == http.ErrServerClosed { + logger.Info("Closed") + } else { + logger.Warning("Failed to serve due to error: %s", err) + } + s.shutdownWait.Done() + closeCallback(err) + }() + ls, err := s.buildListener( + cfg.ListenInterface, + cfg.ListenPort, + cfg.ReadTimeout, + cfg.WriteTimeout, + ) + if err != nil { + return err + } + defer ls.Close() + if !cfg.IsTLS() { + logger.Info("Serving") + err = s.server.Serve(ls) + } else { + logger.Info("Serving TLS") + err = s.server.ServeTLS( + ls, cfg.TLSCertificateFile, cfg.TLSCertificateKeyFile) + } + return err +} + +// Close close the server +func (s *Serving) Close() error { + return s.server.Shutdown(context.TODO()) +} diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..cc799d7 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,25 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +module.exports = function (api) { + api.cache(true); + + return { + presets: ["@babel/preset-env"], + plugins: [["@babel/plugin-transform-runtime"]], + }; +}; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4b0102e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,73 @@ +version: "3.9" + +services: +# SSHWIFTY + sshwifty: + container_name: sshwifty + user: "nobody:nobody" + restart: always + build: + context: . + ports: + - 9133:8182 + deploy: + labels: + - traefik.frontend.rule=Host:ssh.legaragenumerique.fr + - traefik.port=80 + - traefik.frontend.auth.forward.address=http://traefik-forward-auth:4181 + - traefik.frontend.auth.forward.authResponseHeaders=X-Forwarded-User + - traefik.frontend.auth.forward.trustForwardHeader=true + +# TRAEFIK + traefik: + image: traefik:v2.10 + container_name: traefik + restart: always + command: + # For web ui traefik DEV + # - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.swarmmode=false" + - "--log.level=DEBUG" + - "--providers.docker.exposedByDefault=false" + + - "--entryPoints.web.address=:80" + # - "--entryPoints.websecure.address=:443" + # - "--certificatesResolvers.le.acme.email=${ACME_EMAIL}" + # - "--certificatesResolvers.le.acme.storage=/acme/acme.json" + # - "--certificatesResolvers.le.acme.httpChallenge=true" + # - "--certificatesResolvers.le.acme.httpChallenge.entryPoint=web" + # - "--certificatesresolvers.le.acme.caserver=https://acme-v02.api.letsencrypt.org/directory" + ports: + # - "4443:443" + - "8880:80" + # The Web UI (enabled by --api.insecure=true) DEV + # - "8082:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + # - ./acme:/acme + networks: + traefik_net: + +# GATE KEEPER + traefik-forward-auth: + image: funkypenguin/traefik-forward-auth + restart: always + env_file: ./traefik-forward-auth.env + networks: + - traefik_net + deploy: + labels: + - traefik.port=4181 + - traefik.frontend.rule=Host:id/legaragenumerique.fr + - traefik.frontend.auth.forward.address=http://traefik-forward-auth:4181 + - traefik.frontend.auth.forward.trustForwardHeader=true + + +# NETWORKS +networks: + traefik_net: + +# VOLUMES +# volumes: +# acme: {} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b00dd68 --- /dev/null +++ b/go.mod @@ -0,0 +1,28 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +module github.com/nirui/sshwifty + +go 1.21.5 + +require ( + github.com/gorilla/websocket v1.5.1 + golang.org/x/crypto v0.16.0 + golang.org/x/net v0.19.0 +) + +require golang.org/x/sys v0.15.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c203db9 --- /dev/null +++ b/go.sum @@ -0,0 +1,170 @@ +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2 h1:XdAboW3BNMv9ocSCOk/u1MFioZGzCNkiJZ19v9Oe3Ig= +golang.org/x/crypto v0.0.0-20220210151621-f4118a5b28e2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA= +golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0= +golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY= +golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220924013350-4ba4fb4dd9e7 h1:WJywXQVIb56P2kAvXeMGTIgQ1ZHQxR60+F9dLsodECc= +golang.org/x/crypto v0.0.0-20220924013350-4ba4fb4dd9e7/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE= +golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 h1:NWy5+hlRbC7HK+PmcXVUmW1IMyFce7to56IUvhUFm7Y= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 h1:Yqz/iviulwKwAREEeUd3nbBFn0XuyJqkoft2IlrvOhc= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220708220712-1185a9018129 h1:vucSRfWwTsoXro7P+3Cjlr6flUMtzCwzlvkxEQtHHB0= +golang.org/x/net v0.0.0-20220708220712-1185a9018129/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220811182439-13a9a731de15 h1:cik0bxZUSJVDyaHf1hZPSDsU8SZHGQZQMeueXCE7yBQ= +golang.org/x/net v0.0.0-20220811182439-13a9a731de15/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220919232410-f2f64ebce3c1 h1:TWZxd/th7FbRSMret2MVQdlI8uT49QEtwZdvJrxjEHU= +golang.org/x/net v0.0.0-20220919232410-f2f64ebce3c1/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220923203811-8be639271d50 h1:vKyz8L3zkd+xrMeIaBsQ/MNVPVFSffdaU3ZyYlBGFnI= +golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220519141025-dcacdad47464 h1:MpIuURY70f0iKp/oooEFtB2oENcHITo/z1b6u41pKCw= +golang.org/x/sys v0.0.0-20220519141025-dcacdad47464/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2 h1:wM1k/lXfpc5HdkJJyW9GELpd8ERGdnh8sMGL6Gzq3Ho= +golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 h1:h+EGohizhe9XlX18rfpa8k8RAc5XyaeamM+0VHRd4lc= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..096f0ff --- /dev/null +++ b/package-lock.json @@ -0,0 +1,12230 @@ +{ + "name": "sshwifty-ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sshwifty-ui", + "version": "0.0.0", + "license": "AGPL-3.0-only", + "devDependencies": { + "@azurity/pure-nerd-font": "3.0.0", + "@babel/core": "^7.23.5", + "@babel/eslint-parser": "^7.23.3", + "@babel/plugin-transform-runtime": "^7.23.4", + "@babel/preset-env": "^7.23.5", + "@babel/register": "^7.22.15", + "@babel/runtime": "^7.23.5", + "@xterm/addon-fit": "^0.9.0-beta.1", + "@xterm/addon-unicode11": "^0.7.0-beta.1", + "@xterm/addon-web-links": "^0.10.0-beta.1", + "@xterm/addon-webgl": "^0.17.0-beta.1", + "@xterm/xterm": "^5.4.0-beta.1", + "babel-loader": "^9.1.3", + "buffer": "^6.0.3", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.8.1", + "css-minimizer-webpack-plugin": "^5.0.1", + "cwebp-bin": "^8.0.0", + "eslint": "^8.55.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-vue": "^9.19.2", + "eslint-webpack-plugin": "^4.0.1", + "favicons": "^7.1.4", + "fontfaceobserver": "^2.3.0", + "hack-font": "^3.3.0", + "html-loader": "^4.2.0", + "html-webpack-plugin": "^5.5.3", + "iconv-lite": "^0.6.3", + "image-minimizer-webpack-plugin": "^3.8.3", + "imagemin": "^8.0.1", + "imagemin-gifsicle": "^7.0.0", + "imagemin-mozjpeg": "^10.0.0", + "imagemin-pngquant": "^9.0.2", + "imagemin-svgo": "^10.0.1", + "imagemin-webp": "^8.0.0", + "mini-css-extract-plugin": "^2.7.6", + "mocha": "^10.2.0", + "normalize.css": "^8.0.1", + "prettier": "^3.1.0", + "roboto-fontface": "^0.10.0", + "style-loader": "^3.3.3", + "terser-webpack-plugin": "^5.3.9", + "vue": "^2.6.14", + "vue-loader": "^15.9.8", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "webpack-favicons": "^1.3.8" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@azurity/pure-nerd-font": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@azurity/pure-nerd-font/-/pure-nerd-font-3.0.0.tgz", + "integrity": "sha512-69hcOoaZWsX77DYsaQ8qWHnvrP3JwfY+oyCq8h/T34yFYZTXH5ZlVQqLeXaDbg80etZR98qZPVrcxjClKZqTMA==", + "dev": true + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.5.tgz", + "integrity": "sha512-Cwc2XjUrG4ilcfOw4wBAK+enbdgwAcAJCfGUItPBKR7Mjw4aEfAFYrLxeRp4jWgtNIKn3n2AlBOfwwafl+42/g==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.5", + "@babel/parser": "^7.23.5", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/eslint-parser": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.23.3.tgz", + "integrity": "sha512-9bTuNlyx7oSstodm1cR1bECj4fkiknsDa1YniISkJemMY3DGhJNYBECbe6QD/q54mp2J8VO66jW3/7uP//iFCw==", + "dev": true, + "dependencies": { + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", + "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.15", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", + "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz", + "integrity": "sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.5.tgz", + "integrity": "sha512-oO7us8FzTEsG3U6ag9MfdF1iA/7Z6dz+MtFhifZk8C8o453rGJFFWUP1t+ULM9TUIAzC9uxXEiXjOiVMyd7QPg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.5", + "@babel/types": "^7.23.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", + "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz", + "integrity": "sha512-iRkKcCqb7iGnq9+3G6rZ+Ciz5VywC4XNRHe57lKM+jOeYAoR0lVqdeeDRfh0tQcTfw/+vBhHn926FmQhLtlFLQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz", + "integrity": "sha512-WwlxbfMNdVEpQjZmK5mhm7oSwD3dS6eU+Iwsi4Knl9wAletWem7kaRsGOG+8UEbRyqxY4SS5zvtfXwX+jMxUwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.23.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.3.tgz", + "integrity": "sha512-XaJak1qcityzrX0/IU5nKHb34VaibwP3saKqG6a/tppelgllOH13LUann4ZCIBcVOeE6H18K4Vx9QKkVww3z/w==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.23.3.tgz", + "integrity": "sha512-lPgDSU+SJLK3xmFDTV2ZRQAiM7UuUjGidwBywFavObCiZc1BeAAcMtHJKUya92hPHO+at63JJPLygilZard8jw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz", + "integrity": "sha512-pawnE0P9g10xgoP7yKr6CK63K2FMsTE+FZidZO/1PwRdzmAPVs+HS1mAURUsgaoxammTJvULUdIkEK0gOcU2tA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.23.3.tgz", + "integrity": "sha512-NzQcQrzaQPkaEwoTm4Mhyl8jI1huEL/WWIEvudjTCMJ9aBZNpsJbMASx7EQECtQQPS/DcnFpo0FIh3LvEO9cxQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.4.tgz", + "integrity": "sha512-efdkfPhHYTtn0G6n2ddrESE91fgXxjlqLsnUtPWnJs4a4mZIbUaK7ffqKIIUKXSHwcDvaCVX6GXkaJJFqtX7jw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz", + "integrity": "sha512-vI+0sIaPIO6CNuM9Kk5VmXcMVRiOpDh7w2zZt9GXzmE/9KD70CUEVhvPR/etAeNK/FAEkhxQtXOzVF3EuRL41A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz", + "integrity": "sha512-0QqbP6B6HOh7/8iNR4CQU2Th/bbRtBp4KS9vcaZd1fZ0wSh5Fyssg0UCIHwxh+ka+pNDREbVLQnHCMHKZfPwfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz", + "integrity": "sha512-uM+AN8yCIjDPccsKGlw271xjJtGii+xQIF/uMPS8H15L12jZTsLfF4o5vNO7d/oUguOyfdikHGc/yi9ge4SGIg==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz", + "integrity": "sha512-nsWu/1M+ggti1SOALj3hfx5FXzAY06fwPJsUZD4/A5e1bWi46VUIWtD+kOX6/IdhXGsXBWllLFDSnqSCdUNydQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.5.tgz", + "integrity": "sha512-jvOTR4nicqYC9yzOHIhXG5emiFEOpappSJAl73SDSEDcybD+Puuze8Tnpb9p9qEyYup24tq891gkaygIFvWDqg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20", + "@babel/helper-split-export-declaration": "^7.22.6", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz", + "integrity": "sha512-dTj83UVTLw/+nbiHqQSFdwO9CbTtwq1DsDqm3CUEtDrZNET5rT5E6bIdTlOftDTDLMYxvxHNEYO4B9SLl8SLZw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz", + "integrity": "sha512-n225npDqjDIr967cMScVKHXJs7rout1q+tt50inyBCPkyZ8KxeI6d+GIbSBTT/w/9WdlWDOej3V9HE5Lgk57gw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz", + "integrity": "sha512-vgnFYDHAKzFaTVp+mneDsIEbnJ2Np/9ng9iviHw3P/KVcgONxpNULEW/51Z/BaFojG2GI2GwwXck5uV1+1NOYQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz", + "integrity": "sha512-RrqQ+BQmU3Oyav3J+7/myfvRCq7Tbz+kKLLshUmMwNlDHExbGL7ARhajvoBJEvc+fCguPPu887N+3RRXBVKZUA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz", + "integrity": "sha512-V6jIbLhdJK86MaLh4Jpghi8ho5fGzt3imHOBu/x0jlBaPYqDoWz4RDXjmMOfnh+JWNaQleEAByZLV0QzBT4YQQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz", + "integrity": "sha512-5fhCsl1odX96u7ILKHBj4/Y8vipoqwsJMh4csSA8qFfxrZDEA4Ssku2DyNvMJSmZNOEBT750LfFPbtrnTP90BQ==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz", + "integrity": "sha512-GzuSBcKkx62dGzZI1WVgTWvkkz84FZO5TC5T8dl/Tht/rAla6Dg/Mz9Yhypg+ezVACf/rgDuQt3kbWEv7LdUDQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.3.tgz", + "integrity": "sha512-X8jSm8X1CMwxmK878qsUGJRmbysKNbdpTv/O1/v0LuY/ZkZrng5WYiekYSdg9m09OTmDDUWeEDsTE+17WYbAZw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz", + "integrity": "sha512-I1QXp1LxIvt8yLaib49dRW5Okt7Q4oaxao6tFVKS/anCdEOMtYwWVKoiOA1p34GOWIZjUK0E+zCp7+l1pfQyiw==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz", + "integrity": "sha512-81nTOqM1dMwZ/aRXQ59zVubN9wHGqk6UtqRK+/q+ciXmRy8fSolhGVvG09HHRGo4l6fr/c4ZhXUQH0uFW7PZbg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz", + "integrity": "sha512-wZ0PIXRxnwZvl9AYpqNUxpZ5BiTGrYt7kueGQ+N5FiQ7RCOD4cm8iShd6S6ggfVIWaJf2EMk8eRzAh52RfP4rQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz", + "integrity": "sha512-Mc/ALf1rmZTP4JKKEhUwiORU+vcfarFVLfcFiolKUo6sewoxSEgl36ak5t+4WamRsNr6nzjZXQjM35WsU+9vbg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz", + "integrity": "sha512-sC3LdDBDi5x96LA+Ytekz2ZPk8i/Ck+DEuDbRAll5rknJ5XRTSaPKEYwomLcs1AA8wg9b3KjIQRsnApj+q51Ag==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz", + "integrity": "sha512-vJYQGxeKM4t8hYCKVBlZX/gtIY2I7mRGFNcm85sgXGMTBcoV3QdVtdpbcWEbzbfUIUZKwvgFT82mRvaQIebZzw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", + "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz", + "integrity": "sha512-ZxyKGTkF9xT9YJuKQRo19ewf3pXpopuYQd8cDXqNzc3mUNbOME0RKMoZxviQk74hwzfQsEe66dE92MaZbdHKNQ==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz", + "integrity": "sha512-zHsy9iXX2nIsCBFPud3jKn1IRPWg3Ing1qOZgeKV39m1ZgIdpJqvlWVeiHBZC6ITRG0MfskhYe9cLgntfSFPIg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz", + "integrity": "sha512-YJ3xKqtJMAT5/TIZnpAR3I+K+WaDowYbN3xyxI8zxx/Gsypwf9B9h0VB+1Nh6ACAAPRS5NSRje0uVv5i79HYGQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz", + "integrity": "sha512-jHE9EVVqHKAQx+VePv5LLGHjmHSJR76vawFPTdlxR/LVJPfOEGxREQwQfjuZEOPTwG92X3LINSh3M40Rv4zpVA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz", + "integrity": "sha512-mps6auzgwjRrwKEZA05cOwuDc9FAzoyFS4ZsG/8F43bTLf/TgkJg7QXOrPO1JO599iA3qgK9MXdMGOEC8O1h6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz", + "integrity": "sha512-9x9K1YyeQVw0iOXJlIzwm8ltobIIv7j2iLyP2jIhEbqPRQ7ScNgwQufU2I0Gq11VjyG4gI4yMXt2VFags+1N3g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.3", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.23.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz", + "integrity": "sha512-BwQ8q0x2JG+3lxCVFohg+KbQM7plfpBwThdW9A6TMtWwLsbDA01Ek2Zb/AgDN39BiZsExm4qrXxjk+P1/fzGrA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz", + "integrity": "sha512-XIq8t0rJPHf6Wvmbn9nFxU6ao4c7WhghTR5WyV8SrJfUFzyxhCm4nhC+iAp3HFhbAKLfYpgzhJ6t4XCtVwqO5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz", + "integrity": "sha512-ZU8y5zWOfjM5vZ+asjgAPwDaBjJzgufjES89Rs4Lpq63O300R/kOz30WCLo6BxxX6QVEilwSlpClnG5cZaikTA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz", + "integrity": "sha512-09lMt6UsUb3/34BbECKVbVwrT9bO6lILWln237z7sLaWnMsTi7Yc9fhX5DLpkJzAGfaReXI22wP41SZmnAA3Vw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz", + "integrity": "sha512-UzqRcRtWsDMTLrRWFvUBDwmw06tCQH9Rl1uAjfh6ijMSmGYQ+fpdB+cnqRC8EMh5tuuxSv0/TejGL+7vyj+50g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz", + "integrity": "sha512-9G3K1YqTq3F4Vt88Djx1UZ79PDyj+yKRnUy7cZGSMe+a7jkwD259uKKuUzQlPkGam7R+8RJwh5z4xO27fA1o2A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz", + "integrity": "sha512-jR3Jn3y7cZp4oEWPFAlRsSWjxKe4PZILGBSd4nis1TsC5qeSpb+nrtihJuDhNI7QHiVbUaiXa0X2RZY3/TI6Nw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz", + "integrity": "sha512-KP+75h0KghBMcVpuKisx3XTu9Ncut8Q8TuvGO4IhY+9D5DFEckQefOuIsB/gQ2tG71lCke4NMrtIPS8pOj18BQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz", + "integrity": "sha512-QnNTazY54YqgGxwIexMZva9gqbPa15t/x9VS+0fsEFWplwVpXYZivtgl43Z1vMpc1bdPP2PP8siFeVcnFvA3Cg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.4.tgz", + "integrity": "sha512-ITwqpb6V4btwUG0YJR82o2QvmWrLgDnx/p2A3CTPYGaRgULkDiC0DRA2C4jlRB9uXGUEfaSS/IGHfVW+ohzYDw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.6", + "babel-plugin-polyfill-corejs3": "^0.8.5", + "babel-plugin-polyfill-regenerator": "^0.5.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz", + "integrity": "sha512-ED2fgqZLmexWiN+YNFX26fx4gh5qHDhn1O2gvEhreLW2iI63Sqm4llRLCXALKrCnbN4Jy0VcMQZl/SAzqug/jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz", + "integrity": "sha512-VvfVYlrlBVu+77xVTOAoxQ6mZbnIq5FM0aGBSFEcIh03qHf+zNqA4DC/3XMUozTg7bZV3e3mZQ0i13VB6v5yUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz", + "integrity": "sha512-HZOyN9g+rtvnOU3Yh7kSxXrKbzgrm5X4GncPY1QOquu7epga5MxKHVpYu2hvQnry/H+JjckSYRb93iNfsioAGg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz", + "integrity": "sha512-Flok06AYNp7GV2oJPZZcP9vZdszev6vPBkHLwxwSpaIqx75wn6mUd3UFWsSsA0l8nXAKkyCmL/sR02m8RYGeHg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz", + "integrity": "sha512-4t15ViVnaFdrPC74be1gXBSMzXk3B4Us9lP7uLRQHTFpV5Dvt33pn+2MyyNxmN3VTTm3oTrZVMUmuw3oBnQ2oQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz", + "integrity": "sha512-OMCUx/bU6ChE3r4+ZdylEqAjaQgHAgipgW8nsCfu5pGqDcFytVd91AwRvUJSBZDz0exPGgnjoqhgRYLRjFZc9Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz", + "integrity": "sha512-KcLIm+pDZkWZQAFJ9pdfmh89EwVfmNovFBcXko8szpBeF8z68kWIPeKlmSOkT9BXJxs2C0uk+5LxoxIv62MROA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz", + "integrity": "sha512-wMHpNA4x2cIA32b/ci3AfwNgheiva2W0WUKWTK7vBHBhDKfPsc5cFGNWm69WBqpwd86u1qwZ9PWevKqm1A3yAw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz", + "integrity": "sha512-W7lliA/v9bNR83Qc3q1ip9CQMZ09CcHDbHfbLRDNuAhn1Mvkr1ZNF7hPmztMQvtTGVLJ9m8IZqWsTkXOml8dbw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.23.5.tgz", + "integrity": "sha512-0d/uxVD6tFGWXGDSfyMD1p2otoaKmu6+GD+NfAx0tMaH+dxORnp7T9TaVQ6mKyya7iBtCIVxHjWT7MuzzM9z+A==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.3", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.4", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.5", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.3", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.3", + "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.23.4", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.6", + "babel-plugin-polyfill-corejs3": "^0.8.5", + "babel-plugin-polyfill-regenerator": "^0.5.3", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/register": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.22.15.tgz", + "integrity": "sha512-V3Q3EqoQdn65RCgTLwauZaTfd1ShhwPmbBv+1dkZV/HpCGMKVyn6oFcRlI7RaKqiDQjX2Qd3AuoEguBgdjIKlg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.5", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", + "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", + "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.5", + "@babel/types": "^7.23.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", + "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "dependencies": { + "eslint-scope": "5.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/eslint": { + "version": "8.44.7", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.7.tgz", + "integrity": "sha512-f5ORu2hcBbKei97U73mf+l9t4zTGl74IqZ0GQk4oVea/VS8tQZYkUveSYojk+frraAVYId0V2WC9O4PTNru2FQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", + "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vue/compiler-sfc": { + "version": "2.7.15", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.15.tgz", + "integrity": "sha512-FCvIEevPmgCgqFBH7wD+3B97y7u7oj/Wr69zADBf403Tui377bThTjBvekaZvlRr4IwUAu3M6hYZeULZFJbdYg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.18.4", + "postcss": "^8.4.14", + "source-map": "^0.6.1" + } + }, + "node_modules/@vue/component-compiler-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz", + "integrity": "sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==", + "dev": true, + "dependencies": { + "consolidate": "^0.15.1", + "hash-sum": "^1.0.2", + "lru-cache": "^4.1.2", + "merge-source-map": "^1.1.0", + "postcss": "^7.0.36", + "postcss-selector-parser": "^6.0.2", + "source-map": "~0.6.1", + "vue-template-es2015-compiler": "^1.9.0" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "node_modules/@vue/component-compiler-utils/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "optional": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.9.0-beta.1", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.9.0-beta.1.tgz", + "integrity": "sha512-HmGRUMMamUpQYuQBF2VP1LJ0xzqF85LMFfpaNu84t1Tsrl1lPKJWtqX9FDZ22Rf5q6bnKdbj44TRVAUHgDRbLA==", + "dev": true, + "peerDependencies": { + "xterm": "^5.0.0" + } + }, + "node_modules/@xterm/addon-unicode11": { + "version": "0.7.0-beta.1", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.7.0-beta.1.tgz", + "integrity": "sha512-4FUzF1hMCSK0WcpRZ1GxJlCAb+XEiJxUqv01/GQzEaGwbFUHd7Ekh2zxe8+2NvNXp/PpSaCny5kjKoxNxzrhRQ==", + "dev": true, + "peerDependencies": { + "xterm": "^5.0.0" + } + }, + "node_modules/@xterm/addon-web-links": { + "version": "0.10.0-beta.1", + "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.10.0-beta.1.tgz", + "integrity": "sha512-eVsIGY8CNk/mJfHWTkPCQ0E/buOxVWKd+awk9EI8GF5rcQJWNHrm1agnZhtGzCTCBXirhnl4lNqyY+z1uwBADQ==", + "dev": true, + "peerDependencies": { + "xterm": "^5.0.0" + } + }, + "node_modules/@xterm/addon-webgl": { + "version": "0.17.0-beta.1", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.17.0-beta.1.tgz", + "integrity": "sha512-SRUh8dFYmD8U37gGGl9+oV+D304wumkV7eoxvD53J5HQERSKAwk3YeIIVUGWu6q6pifjUgGqfiKCIQwbHZ6QIQ==", + "dev": true, + "peerDependencies": { + "xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.4.0-beta.1", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.4.0-beta.1.tgz", + "integrity": "sha512-e5dokYOdC0AiBRIRLKCxHqOmLyGsqAdYwalu2d+cTMXRX/gCiTWNmJoVISD6LPUUc6jYKHhfOH/WWa0LYsF8gw==", + "dev": true + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/archive-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", + "integrity": "sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==", + "dev": true, + "dependencies": { + "file-type": "^4.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/archive-type/node_modules/file-type": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", + "integrity": "sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/b4a": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", + "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==", + "dev": true + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-loader/node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/babel-loader/node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz", + "integrity": "sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.4.3", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz", + "integrity": "sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.3", + "core-js-compat": "^3.33.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz", + "integrity": "sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.3" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/bin-build": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bin-build/-/bin-build-3.0.0.tgz", + "integrity": "sha512-jcUOof71/TNAI2uM5uoUaDq2ePcVBQ3R/qhxAz1rX7UfvduAL/RXD3jXzvn8cVcDJdGVkiR1shal3OH0ImpuhA==", + "dev": true, + "dependencies": { + "decompress": "^4.0.0", + "download": "^6.2.2", + "execa": "^0.7.0", + "p-map-series": "^1.0.0", + "tempfile": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", + "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", + "dev": true, + "dependencies": { + "execa": "^0.7.0", + "executable": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-3.1.0.tgz", + "integrity": "sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ==", + "dev": true, + "dependencies": { + "execa": "^1.0.0", + "find-versions": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-version-check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-4.0.0.tgz", + "integrity": "sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ==", + "dev": true, + "dependencies": { + "bin-version": "^3.0.0", + "semver": "^5.6.0", + "semver-truncate": "^1.1.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-version-check/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/bin-version/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/bin-version/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-version/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-version/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-version/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/bin-version/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-version/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-version/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/bin-wrapper": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-wrapper/-/bin-wrapper-4.1.0.tgz", + "integrity": "sha512-hfRmo7hWIXPkbpi0ZltboCMVrU+0ClXR/JgbCKKjlDjQf6igXa7OwdqNcFWQZPZTgiY7ZpzE3+LjjkLiTN2T7Q==", + "dev": true, + "dependencies": { + "bin-check": "^4.1.0", + "bin-version-check": "^4.0.0", + "download": "^7.1.0", + "import-lazy": "^3.1.0", + "os-filter-obj": "^2.0.0", + "pify": "^4.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-wrapper/node_modules/download": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/download/-/download-7.1.0.tgz", + "integrity": "sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==", + "dev": true, + "dependencies": { + "archive-type": "^4.0.0", + "caw": "^2.0.1", + "content-disposition": "^0.5.2", + "decompress": "^4.2.0", + "ext-name": "^5.0.0", + "file-type": "^8.1.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^8.3.1", + "make-dir": "^1.2.0", + "p-event": "^2.1.0", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-wrapper/node_modules/download/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/file-type": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz", + "integrity": "sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-wrapper/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/got": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", + "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", + "dev": true, + "dependencies": { + "@sindresorhus/is": "^0.7.0", + "cacheable-request": "^2.1.1", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "into-stream": "^3.1.0", + "is-retry-allowed": "^1.1.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "mimic-response": "^1.0.0", + "p-cancelable": "^0.4.0", + "p-timeout": "^2.0.1", + "pify": "^3.0.0", + "safe-buffer": "^5.1.1", + "timed-out": "^4.0.1", + "url-parse-lax": "^3.0.0", + "url-to-options": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/got/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/p-cancelable": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", + "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/p-event": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", + "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", + "dev": true, + "dependencies": { + "p-timeout": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-wrapper/node_modules/p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "dev": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", + "dev": true, + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.44" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==", + "dev": true, + "dependencies": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/cacheable-request/node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", + "dev": true + }, + "node_modules/cacheable-request/node_modules/keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001564", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001564.tgz", + "integrity": "sha512-DqAOf+rhof+6GVx1y+xzbFPeOumfQnhYzVnZD6LAXijR77yPtm9mfOcqOnT3mpnJiZVT+kwLAFnRlZcIz+c6bg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/caw": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", + "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", + "dev": true, + "dependencies": { + "get-proxy": "^2.0.0", + "isurl": "^1.0.0-alpha5", + "tunnel-agent": "^0.6.0", + "url-to-options": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-css": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-webpack-plugin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz", + "integrity": "sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==", + "dev": true, + "dependencies": { + "del": "^4.1.1" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "webpack": ">=4.0.0 <6.0.0" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "dev": true + }, + "node_modules/cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/consolidate": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", + "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "deprecated": "Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog", + "dev": true, + "dependencies": { + "bluebird": "^3.1.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.33.3", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.3.tgz", + "integrity": "sha512-cNzGqFsh3Ot+529GIXacjTJ7kegdt5fPXxCBVS1G0iaZpuo/tBz399ymceLJveQhFFZ8qThHiP3fzuoQjKN2ow==", + "dev": true, + "dependencies": { + "browserslist": "^4.22.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-declaration-sorter": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", + "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-loader": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", + "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.21", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.3", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-loader/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-loader/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz", + "integrity": "sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "cssnano": "^6.0.1", + "jest-worker": "^29.4.3", + "postcss": "^8.4.24", + "schema-utils": "^4.0.1", + "serialize-javascript": "^6.0.1" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "lightningcss": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.1.tgz", + "integrity": "sha512-fVO1JdJ0LSdIGJq68eIxOqFpIJrZqXUsBt8fkrBcztCQqAjQD51OhZp7tc0ImcbwXD4k7ny84QTV90nZhmqbkg==", + "dev": true, + "dependencies": { + "cssnano-preset-default": "^6.0.1", + "lilconfig": "^2.1.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.0.1.tgz", + "integrity": "sha512-7VzyFZ5zEB1+l1nToKyrRkuaJIx0zi/1npjvZfbBwbtNTzhLtlvYraK/7/uqmX2Wb2aQtd983uuGw79jAjLSuQ==", + "dev": true, + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^4.0.0", + "postcss-calc": "^9.0.0", + "postcss-colormin": "^6.0.0", + "postcss-convert-values": "^6.0.0", + "postcss-discard-comments": "^6.0.0", + "postcss-discard-duplicates": "^6.0.0", + "postcss-discard-empty": "^6.0.0", + "postcss-discard-overridden": "^6.0.0", + "postcss-merge-longhand": "^6.0.0", + "postcss-merge-rules": "^6.0.1", + "postcss-minify-font-values": "^6.0.0", + "postcss-minify-gradients": "^6.0.0", + "postcss-minify-params": "^6.0.0", + "postcss-minify-selectors": "^6.0.0", + "postcss-normalize-charset": "^6.0.0", + "postcss-normalize-display-values": "^6.0.0", + "postcss-normalize-positions": "^6.0.0", + "postcss-normalize-repeat-style": "^6.0.0", + "postcss-normalize-string": "^6.0.0", + "postcss-normalize-timing-functions": "^6.0.0", + "postcss-normalize-unicode": "^6.0.0", + "postcss-normalize-url": "^6.0.0", + "postcss-normalize-whitespace": "^6.0.0", + "postcss-ordered-values": "^6.0.0", + "postcss-reduce-initial": "^6.0.0", + "postcss-reduce-transforms": "^6.0.0", + "postcss-svgo": "^6.0.0", + "postcss-unique-selectors": "^6.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.0.tgz", + "integrity": "sha512-Z39TLP+1E0KUcd7LGyF4qMfu8ZufI0rDzhdyAMsa/8UyNUU8wpS0fhdBxbQbv32r64ea00h4878gommRVg2BHw==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, + "node_modules/cwebp-bin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cwebp-bin/-/cwebp-bin-8.0.0.tgz", + "integrity": "sha512-j2s6jA84aG20lB0i/FBwqZGc8nHx4VASUK8OTDxy3xoUHoX/+pP6T15/TnWwhMcD0pZ05y5GgRPkurufOC8tnQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "bin-build": "^3.0.0", + "bin-wrapper": "^4.0.1" + }, + "bin": { + "cwebp": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/imagemin/cwebp-bin?sponsor=1" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "dev": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dev": true, + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "dependencies": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "dependencies": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-browser/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "dependencies": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/del/node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/globby/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/download": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/download/-/download-6.2.5.tgz", + "integrity": "sha512-DpO9K1sXAST8Cpzb7kmEhogJxymyVUd5qz/vCOSyvwtp2Klj2XcDt5YUuasgxka44SxF0q5RriKIwJmQHG2AuA==", + "dev": true, + "dependencies": { + "caw": "^2.0.0", + "content-disposition": "^0.5.2", + "decompress": "^4.0.0", + "ext-name": "^5.0.0", + "file-type": "5.2.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^7.0.0", + "make-dir": "^1.0.0", + "p-event": "^1.0.0", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/duplexer3": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.594", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.594.tgz", + "integrity": "sha512-xT1HVAu5xFn7bDfkjGQi9dNpMqGchUkebwf1GL7cZN32NSwwlHRPMSDJ1KN6HkS0bWUtndbSQZqvpQftKG2uFQ==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", + "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", + "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", + "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.19.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.19.2.tgz", + "integrity": "sha512-CPDqTOG2K4Ni2o4J5wixkLVNwgctKXFu6oBpVJlpNq7f38lh9I80pRTouZSJ2MAebPJlINU/KTFSXyQfBUlymA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.13", + "semver": "^7.5.4", + "vue-eslint-parser": "^9.3.1", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-vue/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-vue/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-webpack-plugin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-4.0.1.tgz", + "integrity": "sha512-fUFcXpui/FftGx3NzvWgLZXlLbu+m74sUxGEgxgoxYcUtkIQbS6SdNNZkS99m5ycb23TfoNYrDpp1k/CK5j6Hw==", + "dev": true, + "dependencies": { + "@types/eslint": "^8.37.0", + "jest-worker": "^29.5.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "eslint": "^8.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exec-buffer": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/exec-buffer/-/exec-buffer-3.2.0.tgz", + "integrity": "sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA==", + "dev": true, + "dependencies": { + "execa": "^0.7.0", + "p-finally": "^1.0.0", + "pify": "^3.0.0", + "rimraf": "^2.5.4", + "tempfile": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/exec-buffer/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", + "dev": true, + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dev": true, + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/execa/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/executable/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-xml-parser": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz", + "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/favicons": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/favicons/-/favicons-7.1.4.tgz", + "integrity": "sha512-lnZpVgT7Fzz+DUjioKF1dMwLYlpqWCaB4gIksIfIKwtlhHO1Q7w23hERwHQjEsec+43iENwbTAPRDW3XvpLhbg==", + "dev": true, + "dependencies": { + "escape-html": "^1.0.3", + "sharp": "^0.32.4", + "xml2js": "^0.6.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", + "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", + "dev": true, + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-versions": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", + "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "dev": true, + "dependencies": { + "semver-regex": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/fontfaceobserver": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz", + "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==", + "dev": true + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-proxy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", + "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", + "dev": true, + "dependencies": { + "npm-conf": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gifsicle": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gifsicle/-/gifsicle-5.3.0.tgz", + "integrity": "sha512-FJTpgdj1Ow/FITB7SVza5HlzXa+/lqEY0tHQazAJbuAdvyJtkH4wIdsR2K414oaTwRXHFLLF+tYbipj+OpYg+Q==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "bin-build": "^3.0.0", + "bin-wrapper": "^4.0.0", + "execa": "^5.0.0" + }, + "bin": { + "gifsicle": "cli.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/imagemin/gisicle-bin?sponsor=1" + } + }, + "node_modules/gifsicle/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/gifsicle/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gifsicle/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/gifsicle/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gifsicle/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/gifsicle/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gifsicle/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gifsicle/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz", + "integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==", + "dev": true, + "dependencies": { + "decompress-response": "^3.2.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-plain-obj": "^1.1.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "p-cancelable": "^0.3.0", + "p-timeout": "^1.1.1", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "url-parse-lax": "^1.0.0", + "url-to-options": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/got/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/hack-font": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/hack-font/-/hack-font-3.3.0.tgz", + "integrity": "sha512-RohrcAr3UaKiIoxDlOytCjObcUAucfFc6V5fKu6gBrvmvTfIXeBqZwR0Q5kb9qpbluThJWt326LClLKIGiFyug==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbol-support-x": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", + "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "dev": true, + "dependencies": { + "has-symbol-support-x": "^1.4.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-loader": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-4.2.0.tgz", + "integrity": "sha512-OxCHD3yt+qwqng2vvcaPApCEvbx+nXWu+v69TYHx1FO8bffHn/JjHtE3TTQZmHjwvnJe4xxzuecetDVBrQR1Zg==", + "dev": true, + "dependencies": { + "html-minifier-terser": "^7.0.0", + "parse5": "^7.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz", + "integrity": "sha512-6YrDKTuqaP/TquFH7h4srYWsZx+x6k6+FbsTm0ziCwGHDP78Unr1r9F/H4+sGmMbX08GQcJ+K64x55b+7VM/jg==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "webpack": "^5.20.0" + } + }, + "node_modules/html-webpack-plugin/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin/node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "dev": true + }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-minimizer-webpack-plugin": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/image-minimizer-webpack-plugin/-/image-minimizer-webpack-plugin-3.8.3.tgz", + "integrity": "sha512-Ex0cjNJc2FUSuwN7WHNyxkIZINP0M9lrN+uWJznMcsehiM5Z7ELwk+SEkSGEookK1GUd2wf+09jy1PEH5a5XmQ==", + "dev": true, + "dependencies": { + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@squoosh/lib": { + "optional": true + }, + "imagemin": { + "optional": true + }, + "sharp": { + "optional": true + }, + "svgo": { + "optional": true + } + } + }, + "node_modules/imagemin": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/imagemin/-/imagemin-8.0.1.tgz", + "integrity": "sha512-Q/QaPi+5HuwbZNtQRqUVk6hKacI6z9iWiCSQBisAv7uBynZwO7t1svkryKl7+iSQbkU/6t9DWnHz04cFs2WY7w==", + "dev": true, + "dependencies": { + "file-type": "^16.5.3", + "globby": "^12.0.0", + "graceful-fs": "^4.2.8", + "junk": "^3.1.0", + "p-pipe": "^4.0.0", + "replace-ext": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/imagemin-gifsicle": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/imagemin-gifsicle/-/imagemin-gifsicle-7.0.0.tgz", + "integrity": "sha512-LaP38xhxAwS3W8PFh4y5iQ6feoTSF+dTAXFRUEYQWYst6Xd+9L/iPk34QGgK/VO/objmIlmq9TStGfVY2IcHIA==", + "dev": true, + "dependencies": { + "execa": "^1.0.0", + "gifsicle": "^5.0.0", + "is-gif": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/imagemin/imagemin-gifsicle?sponsor=1" + } + }, + "node_modules/imagemin-gifsicle/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/imagemin-gifsicle/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/imagemin-gifsicle/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/imagemin-gifsicle/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imagemin-gifsicle/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/imagemin-gifsicle/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imagemin-gifsicle/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imagemin-gifsicle/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/imagemin-mozjpeg": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/imagemin-mozjpeg/-/imagemin-mozjpeg-10.0.0.tgz", + "integrity": "sha512-DK85QNOjS3/GzWYfNB3CACMZD10sIQgFDv1+WTOnZljgltQTEyATjdyUVyjKu5q4sCESQdwvwq7WEZzJ5fFjlg==", + "dev": true, + "dependencies": { + "execa": "^6.0.0", + "is-jpg": "^3.0.0", + "mozjpeg": "^8.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/imagemin-mozjpeg/node_modules/execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/imagemin-mozjpeg/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imagemin-mozjpeg/node_modules/human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/imagemin-mozjpeg/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imagemin-mozjpeg/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imagemin-mozjpeg/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imagemin-pngquant": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/imagemin-pngquant/-/imagemin-pngquant-9.0.2.tgz", + "integrity": "sha512-cj//bKo8+Frd/DM8l6Pg9pws1pnDUjgb7ae++sUX1kUVdv2nrngPykhiUOgFeE0LGY/LmUbCf4egCHC4YUcZSg==", + "dev": true, + "dependencies": { + "execa": "^4.0.0", + "is-png": "^2.0.0", + "is-stream": "^2.0.0", + "ow": "^0.17.0", + "pngquant-bin": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/imagemin-pngquant/node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/imagemin-pngquant/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imagemin-pngquant/node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/imagemin-pngquant/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imagemin-pngquant/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/imagemin-pngquant/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imagemin-pngquant/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imagemin-pngquant/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/imagemin-svgo": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/imagemin-svgo/-/imagemin-svgo-10.0.1.tgz", + "integrity": "sha512-v27/UTGkb3vrm5jvjsMGQ2oxaDfSOTBfJOgmFO2fYepx05bY1IqWCK13aDytVR+l9w9eOlq0NMCLbxJlghYb2g==", + "dev": true, + "dependencies": { + "is-svg": "^4.3.1", + "svgo": "^2.5.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sindresorhus/imagemin-svgo?sponsor=1" + } + }, + "node_modules/imagemin-webp": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/imagemin-webp/-/imagemin-webp-8.0.0.tgz", + "integrity": "sha512-yN6kNKir6T/U3AtP3uLHrLn9XYafk2m49EbUqLCQ3GPRRLRs+4pUQxxaHz+lnTDM+LQpkSjGQaFVcSgYqvW3dQ==", + "dev": true, + "dependencies": { + "cwebp-bin": "^8.0.0", + "exec-buffer": "^3.2.0", + "is-cwebp-readable": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/imagemin/node_modules/array-union": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imagemin/node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "dev": true, + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/imagemin/node_modules/globby": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz", + "integrity": "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==", + "dev": true, + "dependencies": { + "array-union": "^3.0.1", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.7", + "ignore": "^5.1.9", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imagemin/node_modules/globby/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imagemin/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz", + "integrity": "sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/into-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==", + "dev": true, + "dependencies": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-cwebp-readable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-cwebp-readable/-/is-cwebp-readable-3.0.0.tgz", + "integrity": "sha512-bpELc7/Q1/U5MWHn4NdHI44R3jxk0h9ew9ljzabiRl70/UIjL/ZAqRMb52F5+eke/VC8yTiv4Ewryo1fPWidvA==", + "dev": true, + "dependencies": { + "file-type": "^10.5.0" + } + }, + "node_modules/is-cwebp-readable/node_modules/file-type": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", + "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-gif": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-gif/-/is-gif-3.0.0.tgz", + "integrity": "sha512-IqJ/jlbw5WJSNfwQ/lHEDXF8rxhRgF6ythk2oiEvhpG29F704eX9NO6TvPfMiq9DrbwgcEDnETYNcZDPewQoVw==", + "dev": true, + "dependencies": { + "file-type": "^10.4.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-gif/node_modules/file-type": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", + "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-jpg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-jpg/-/is-jpg-3.0.0.tgz", + "integrity": "sha512-Vcd67KWHZblEKEBrtP25qLZ8wN9ICoAhl1pKUqD7SM7hf2qtuRl7loDgP5Zigh2oN/+7uj+KVyC0eRJvgOEFeQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "dependencies": { + "is-path-inside": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-in-cwd/node_modules/is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "dependencies": { + "path-is-inside": "^1.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-png": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-png/-/is-png-2.0.0.tgz", + "integrity": "sha512-4KPGizaVGj2LK7xwJIz8o5B2ubu1D/vcQsgOGFEDlpcvgZHto4gBnyd0ig7Ws+67ixmwKoNmu0hYnpo6AaKb5g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-svg": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-4.4.0.tgz", + "integrity": "sha512-v+AgVwiK5DsGtT9ng+m4mClp6zDAmwrW8nZi6Gg15qzvBnRWWdfWA1TGaXyCDnWq5g5asofIgMVl3PjKxvk1ug==", + "dev": true, + "dependencies": { + "fast-xml-parser": "^4.1.3" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "dev": true, + "dependencies": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/junk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", + "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/loader-utils/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, + "node_modules/merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", + "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "node_modules/mocha": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", + "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "nanoid": "3.3.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mocha/node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mozjpeg": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/mozjpeg/-/mozjpeg-8.0.0.tgz", + "integrity": "sha512-Ca2Yhah9hG0Iutgsn8MOrAl37P9ThnKsJatjXoWdUO+8X8GeG/6ahvHZrTyqvbs6leMww1SauWUCao/L9qBuFQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "bin-build": "^3.0.0", + "bin-wrapper": "^4.0.0" + }, + "bin": { + "mozjpeg": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-abi": { + "version": "3.51.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz", + "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "dev": true, + "dependencies": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-url/node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-url/node_modules/sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", + "dev": true, + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize.css": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", + "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==", + "dev": true + }, + "node_modules/npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "dev": true, + "dependencies": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-conf/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-filter-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", + "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", + "dev": true, + "dependencies": { + "arch": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ow": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/ow/-/ow-0.17.0.tgz", + "integrity": "sha512-i3keDzDQP5lWIe4oODyDFey1qVrq2hXKTuTH2VpqwpYtzPiKZt2ziRI4NBQmgW40AnV5Euz17OyWweCb+bNEQA==", + "dev": true, + "dependencies": { + "type-fest": "^0.11.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", + "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-event": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-1.3.0.tgz", + "integrity": "sha512-hV1zbA7gwqPVFcapfeATaNjQ3J0NuzorHPyG8GPL9g/Y/TplWVBVoCKCXL6Ej2zscrCEv195QNWJXuBH6XZuzA==", + "dev": true, + "dependencies": { + "p-timeout": "^1.1.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-map-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", + "integrity": "sha512-4k9LlvY6Bo/1FcIdV33wqZQES0Py+iKISU9Uc8p8AjWoZPnFKMpVIVD3s0EYn4jzLh1I+WeUZkJ0Yoa4Qfw3Kg==", + "dev": true, + "dependencies": { + "p-reduce": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-pipe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-4.0.0.tgz", + "integrity": "sha512-HkPfFklpZQPUKBFXzKFB6ihLriIHxnmuQdK9WmLDwe4hf2PdhhfWT/FJa+pc3bA1ywvKXtedxIRmd4Y7BTXE4w==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha512-3Tx1T3oM1xO/Y8Gj0sWyE78EIJZ+t+aEmXUdvQgvGmSMri7aPTHoovbXEreWKkL5j21Er60XAWLTzKbAKYOujQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-timeout": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", + "integrity": "sha512-gb0ryzr+K2qFqFv6qi3khoeqMZF/+ajxQipEF6NteZVnvz9tzdsfAVj3lYtn1gAXvH5lfLwfxEII799gt/mRIA==", + "dev": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pngquant-bin": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/pngquant-bin/-/pngquant-bin-6.0.1.tgz", + "integrity": "sha512-Q3PUyolfktf+hYio6wsg3SanQzEU/v8aICg/WpzxXcuCMRb7H2Q81okfpcEztbMvw25ILjd3a87doj2N9kvbpQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "bin-build": "^3.0.0", + "bin-wrapper": "^4.0.1", + "execa": "^4.0.0" + }, + "bin": { + "pngquant": "cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pngquant-bin/node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/pngquant-bin/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pngquant-bin/node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/pngquant-bin/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pngquant-bin/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pngquant-bin/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pngquant-bin/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pngquant-bin/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-colormin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.0.0.tgz", + "integrity": "sha512-EuO+bAUmutWoZYgHn2T1dG1pPqHU6L4TjzPlu4t1wZGXQ/fxV16xg2EJmYi0z+6r+MGV1yvpx1BHkUaRrPa2bw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-convert-values": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.0.0.tgz", + "integrity": "sha512-U5D8QhVwqT++ecmy8rnTb+RL9n/B806UVaS3m60lqle4YDFcpbS3ae5bTQIh3wOGUSDHSEtMYLs/38dNG7EYFw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-comments": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.0.tgz", + "integrity": "sha512-p2skSGqzPMZkEQvJsgnkBhCn8gI7NzRH2683EEjrIkoMiwRELx68yoUJ3q3DGSGuQ8Ug9Gsn+OuDr46yfO+eFw==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.0.tgz", + "integrity": "sha512-bU1SXIizMLtDW4oSsi5C/xHKbhLlhek/0/yCnoMQany9k3nPBq+Ctsv/9oMmyqbR96HYHxZcHyK2HR5P/mqoGA==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.0.tgz", + "integrity": "sha512-b+h1S1VT6dNhpcg+LpyiUrdnEZfICF0my7HAKgJixJLW7BnNmpRH34+uw/etf5AhOlIhIAuXApSzzDzMI9K/gQ==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.0.tgz", + "integrity": "sha512-4VELwssYXDFigPYAZ8vL4yX4mUepF/oCBeeIT4OXsJPYOtvJumyz9WflmJWTfDwCUcpDR+z0zvCWBXgTx35SVw==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.0.tgz", + "integrity": "sha512-4VSfd1lvGkLTLYcxFuISDtWUfFS4zXe0FpF149AyziftPFQIWxjvFSKhA4MIxMe4XM3yTDgQMbSNgzIVxChbIg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.0.1.tgz", + "integrity": "sha512-a4tlmJIQo9SCjcfiCcCMg/ZCEe0XTkl/xK0XHBs955GWg9xDX3NwP9pwZ78QUOWB8/0XCjZeJn98Dae0zg6AAw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.0.0.tgz", + "integrity": "sha512-zNRAVtyh5E8ndZEYXA4WS8ZYsAp798HiIQ1V2UF/C/munLp2r1UGHwf1+6JFu7hdEhJFN+W1WJQKBrtjhFgEnA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.0.tgz", + "integrity": "sha512-wO0F6YfVAR+K1xVxF53ueZJza3L+R3E6cp0VwuXJQejnNUH0DjcAFe3JEBeTY1dLwGa0NlDWueCA1VlEfiKgAA==", + "dev": true, + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^4.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.0.0.tgz", + "integrity": "sha512-Fz/wMQDveiS0n5JPcvsMeyNXOIMrwF88n7196puSuQSWSa+/Ofc1gDOSY2xi8+A4PqB5dlYCKk/WfqKqsI+ReQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^4.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.0.tgz", + "integrity": "sha512-ec/q9JNCOC2CRDNnypipGfOhbYPuUkewGwLnbv6omue/PSASbHSU7s6uSQ0tcFRVv731oMIx8k0SP4ZX6be/0g==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.0.tgz", + "integrity": "sha512-cqundwChbu8yO/gSWkuFDmKrCZ2vJzDAocheT2JTd0sFNA4HMGoKMfbk2B+J0OmO0t5GUkiAkSM5yF2rSLUjgQ==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.0.tgz", + "integrity": "sha512-Qyt5kMrvy7dJRO3OjF7zkotGfuYALETZE+4lk66sziWSPzlBEt7FrUshV6VLECkI4EN8Z863O6Nci4NXQGNzYw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.0.tgz", + "integrity": "sha512-mPCzhSV8+30FZyWhxi6UoVRYd3ZBJgTRly4hOkaSifo0H+pjDYcii/aVT4YE6QpOil15a5uiv6ftnY3rm0igPg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.0.tgz", + "integrity": "sha512-50W5JWEBiOOAez2AKBh4kRFm2uhrT3O1Uwdxz7k24aKtbD83vqmcVG7zoIwo6xI2FZ/HDlbrCopXhLeTpQib1A==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.0.tgz", + "integrity": "sha512-KWkIB7TrPOiqb8ZZz6homet2KWKJwIlysF5ICPZrXAylGe2hzX/HSf4NTX2rRPJMAtlRsj/yfkrWGavFuB+c0w==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.0.tgz", + "integrity": "sha512-tpIXWciXBp5CiFs8sem90IWlw76FV4oi6QEWfQwyeREVwUy39VSeSqjAT7X0Qw650yAimYW5gkl2Gd871N5SQg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.0.0.tgz", + "integrity": "sha512-ui5crYkb5ubEUDugDc786L/Me+DXp2dLg3fVJbqyAl0VPkAeALyAijF2zOsnZyaS1HyfPuMH0DwyY18VMFVNkg==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.0.tgz", + "integrity": "sha512-98mvh2QzIPbb02YDIrYvAg4OUzGH7s1ZgHlD3fIdTHLgPLRpv1ZTKJDnSAKr4Rt21ZQFzwhGMXxpXlfrUBKFHw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.0.tgz", + "integrity": "sha512-7cfE1AyLiK0+ZBG6FmLziJzqQCpTQY+8XjMhMAz8WSBSCsCNNUKujgIgjCAmDT3cJ+3zjTXFkoD15ZPsckArVw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.0.tgz", + "integrity": "sha512-K36XzUDpvfG/nWkjs6d1hRBydeIxGpKS2+n+ywlKPzx1nMYDYpoGbcjhj5AwVYJK1qV2/SDoDEnHzlPD6s3nMg==", + "dev": true, + "dependencies": { + "cssnano-utils": "^4.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", + "integrity": "sha512-s2UOnidpVuXu6JiiI5U+fV2jamAw5YNA9Fdi/GRK0zLDLCfXmSGqQtzpUPtfN66RtCbb9fFHoyZdQaxOB3WxVA==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.0.tgz", + "integrity": "sha512-FQ9f6xM1homnuy1wLe9lP1wujzxnwt1EwiigtWwuyf8FsqqXUDUp2Ulxf9A5yjlUOTdCJO6lonYjg1mgqIIi2w==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.0.tgz", + "integrity": "sha512-r9zvj/wGAoAIodn84dR/kFqwhINp5YsJkLoujybWG59grR/IHx+uQ2Zo+IcOwM0jskfYX3R0mo+1Kip1VSNcvw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.0.2" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/postcss-svgo/node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/postcss-svgo/node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/postcss-svgo/node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/postcss-svgo/node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/postcss-svgo/node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true + }, + "node_modules/postcss-svgo/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/postcss-svgo/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/postcss-svgo/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/postcss-svgo/node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/postcss-svgo/node_modules/svgo": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.0.4.tgz", + "integrity": "sha512-T+Xul3JwuJ6VGXKo/p2ndqx1ibxNKnLTvRc1ZTWKCfyKS/GgNjRZcYsK84fxTsy/izr91g/Rwx6fGnVgaFSI5g==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.2.1", + "css-what": "^6.1.0", + "csso": "5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.0.tgz", + "integrity": "sha512-EPQzpZNxOxP7777t73RQpZE5e9TrnCrkvp7AH7a0l89JmZiPnS82y216JowHXwpBCQitfyxrof9TK3rYbi7/Yw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/prebuild-install/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/prebuild-install/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prettier": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "dev": true, + "dependencies": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz", + "integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==", + "dev": true, + "dependencies": { + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "dev": true, + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roboto-fontface": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.10.0.tgz", + "integrity": "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g==", + "dev": true + }, + "node_modules/run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/run-applescript/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/run-applescript/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/run-applescript/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-applescript/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "dev": true + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", + "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver-truncate": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-1.1.2.tgz", + "integrity": "sha512-V1fGg9i4CL3qesB6U0L6XAm4xOJiHmt4QAacazumuasc03BvtFGIMCduv01JWQ69Nv+JST9TqhSCiJoxoY031w==", + "dev": true, + "dependencies": { + "semver": "^5.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver-truncate/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sharp": { + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-get/node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/simple-get/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dev": true, + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", + "dev": true + }, + "node_modules/streamx": { + "version": "2.15.5", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.5.tgz", + "integrity": "sha512-9thPGMkKC2GctCzyCUjME3yR03x2xNo0GPKGkRw2UMYN+gqWa9uqpyNWhmsNCutU5zHmkUum0LsCRQTXUgUCAg==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, + "node_modules/strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "dependencies": { + "is-natural-number": "^4.0.1" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true + }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "dev": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/style-loader": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.3.tgz", + "integrity": "sha512-53BiGLXAcll9maCYtZi2RCQZKa8NQQai5C4horqKyRmHj9H7QmcUyucrH+4KW/gBQbXM2AsB0axoEcFZPlfPcw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/stylehacks": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.0.0.tgz", + "integrity": "sha512-+UT589qhHPwz6mTlCLSt/vMNTJx8dopeJlZAlBMJPWA3ORqu6wmQY7FBXf+qD+FsqoBJODyqNxOUP3jdntFRdw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "dependencies": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dev": true, + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "node_modules/tar-fs/node_modules/tar-stream": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", + "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", + "integrity": "sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tempfile": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", + "integrity": "sha512-ZOn6nJUgvgC09+doCEF3oB+r3ag7kUvlsXEGX069QRD60p+P3uP7XG9N2/at+EyIRGSN//ZY3LyEotA1YpmjuA==", + "dev": true, + "dependencies": { + "temp-dir": "^1.0.0", + "uuid": "^3.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/terser": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", + "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dev": true, + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "dev": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unbzip2-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==", + "dev": true, + "dependencies": { + "prepend-http": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vinyl/node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vue": { + "version": "2.7.15", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.15.tgz", + "integrity": "sha512-a29fsXd2G0KMRqIFTpRgpSbWaNBK3lpCTOLuGLEDnlHWdjB8fwl6zyYZ8xCrqkJdatwZb4mGHiEfJjnw0Q6AwQ==", + "dev": true, + "dependencies": { + "@vue/compiler-sfc": "2.7.15", + "csstype": "^3.1.0" + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.3.2.tgz", + "integrity": "sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-eslint-parser/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-eslint-parser/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/vue-hot-reload-api": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", + "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", + "dev": true + }, + "node_modules/vue-loader": { + "version": "15.11.1", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.11.1.tgz", + "integrity": "sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==", + "dev": true, + "dependencies": { + "@vue/component-compiler-utils": "^3.1.0", + "hash-sum": "^1.0.2", + "loader-utils": "^1.1.0", + "vue-hot-reload-api": "^2.3.0", + "vue-style-loader": "^4.1.0" + }, + "peerDependencies": { + "css-loader": "*", + "webpack": "^3.0.0 || ^4.1.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "cache-loader": { + "optional": true + }, + "prettier": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/vue-style-loader": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", + "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==", + "dev": true, + "dependencies": { + "hash-sum": "^1.0.2", + "loader-utils": "^1.0.2" + } + }, + "node_modules/vue-template-es2015-compiler": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", + "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", + "dev": true + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.89.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", + "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-favicons": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/webpack-favicons/-/webpack-favicons-1.3.8.tgz", + "integrity": "sha512-c40JTAUOorQSle2X1p5eRBpNl/zWw4rqR5EvCvLGZqMaXHUtJM/waCLgz0H4Pg1nXvvqV/LoYoqSvoAI0WiD0g==", + "dev": true, + "dependencies": { + "favicons": "7.0.0-beta.1" + }, + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/webpack-favicons/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/webpack-favicons/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/webpack-favicons/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/webpack-favicons/node_modules/favicons": { + "version": "7.0.0-beta.1", + "resolved": "https://registry.npmjs.org/favicons/-/favicons-7.0.0-beta.1.tgz", + "integrity": "sha512-68NaOyryvZpQlQw1w1K40AH6bl8KfhpL2Nb/EBedeBseYS7Rpz1d3t4eBaLIdOBRM2/353EKkZeKzBvQnp6LvA==", + "dev": true, + "dependencies": { + "colors": "^1.4.0", + "escape-html": "^1.0.3", + "lodash.defaultsdeep": "^4.6.1", + "sharp": "^0.29.1", + "through2": "^4.0.2", + "vinyl": "^2.2.1", + "xml2js": "^0.4.23" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-favicons/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/webpack-favicons/node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true + }, + "node_modules/webpack-favicons/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-favicons/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/webpack-favicons/node_modules/sharp": { + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.29.3.tgz", + "integrity": "sha512-fKWUuOw77E4nhpyzCCJR1ayrttHoFHBT2U/kR/qEMRhvPEcluG4BKj324+SCO1e84+knXHwhJ1HHJGnUt4ElGA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "color": "^4.0.1", + "detect-libc": "^1.0.3", + "node-addon-api": "^4.2.0", + "prebuild-install": "^7.0.0", + "semver": "^7.3.5", + "simple-get": "^4.0.0", + "tar-fs": "^2.1.1", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=12.13.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/webpack-favicons/node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/webpack-favicons/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-favicons/node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/webpack-favicons/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "dev": true, + "peer": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e901442 --- /dev/null +++ b/package.json @@ -0,0 +1,68 @@ +{ + "name": "sshwifty-ui", + "version": "0.0.0", + "description": "Sshwifty Web Front-end Project", + "main": "", + "devDependencies": { + "@azurity/pure-nerd-font": "^3.0.1", + "@babel/core": "^7.23.5", + "@babel/eslint-parser": "^7.23.3", + "@babel/plugin-transform-runtime": "^7.23.4", + "@babel/preset-env": "^7.23.5", + "@babel/register": "^7.22.15", + "@babel/runtime": "^7.23.5", + "@xterm/addon-fit": "^0.9.0-beta.1", + "@xterm/addon-unicode11": "^0.7.0-beta.1", + "@xterm/addon-web-links": "^0.10.0-beta.1", + "@xterm/addon-webgl": "^0.17.0-beta.1", + "@xterm/xterm": "^5.4.0-beta.1", + "babel-loader": "^9.1.3", + "buffer": "^6.0.3", + "clean-webpack-plugin": "^4.0.0", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.8.1", + "css-minimizer-webpack-plugin": "^5.0.1", + "cwebp-bin": "^8.0.0", + "eslint": "^8.55.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-vue": "^9.19.2", + "eslint-webpack-plugin": "^4.0.1", + "favicons": "^7.1.4", + "fontfaceobserver": "^2.3.0", + "hack-font": "^3.3.0", + "html-loader": "^4.2.0", + "html-webpack-plugin": "^5.5.3", + "iconv-lite": "^0.6.3", + "image-minimizer-webpack-plugin": "^3.8.3", + "imagemin": "^8.0.1", + "imagemin-gifsicle": "^7.0.0", + "imagemin-mozjpeg": "^10.0.0", + "imagemin-pngquant": "^9.0.2", + "imagemin-svgo": "^10.0.1", + "imagemin-webp": "^8.0.0", + "mini-css-extract-plugin": "^2.7.6", + "mocha": "^10.2.0", + "normalize.css": "^8.0.1", + "prettier": "^3.1.0", + "roboto-fontface": "^0.10.0", + "style-loader": "^3.3.3", + "terser-webpack-plugin": "^5.3.9", + "vue": "^2.6.14", + "vue-loader": "^15.9.8", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "webpack-favicons": "^1.3.8" + }, + "scripts": { + "dev": "NODE_ENV=development webpack --mode=development --config=webpack.config.js --watch", + "clean": "rm .tmp/ -rf || true", + "generate": "npm run clean && NODE_ENV=production webpack --mode=production --config=webpack.config.js", + "build": "npm run generate && CGO_ENABLED=0 go build -ldflags \"-s -w -X github.com/nirui/sshwifty/application.version=$(git describe --always --dirty='*' --tag)\"", + "lint": "eslint --ext .js,.vue ui", + "testonly": "mocha --require @babel/register --recursive --timeout 3s ./ui/**/*_test.js && CGO_ENABLED=1 go test ./... -race -timeout 30s", + "test": "npm run generate && npm run testonly" + }, + "author": "", + "license": "AGPL-3.0-only" +} diff --git a/sshwifty.conf.example.json b/sshwifty.conf.example.json new file mode 100644 index 0000000..5e8aa2d --- /dev/null +++ b/sshwifty.conf.example.json @@ -0,0 +1,55 @@ +{ + "HostName": "", + "SharedKey": "WEB_ACCESS_PASSWORD", + "DialTimeout": 5, + "Socks5": "", + "Socks5User": "", + "Socks5Password": "", + "Servers": [ + { + "ListenInterface": "127.0.0.1", + "ListenPort": 8182, + "InitialTimeout": 3, + "ReadTimeout": 60, + "WriteTimeout": 60, + "HeartbeatTimeout": 20, + "ReadDelay": 10, + "WriteDelay": 10, + "TLSCertificateFile": "", + "TLSCertificateKeyFile": "", + "ServerMessage": "Programmers in China launched an online campaign against [implicitly forced overtime work](https://en.wikipedia.org/wiki/996_working_hour_system) in pursuit of balanced work-life relationship. Sshwifty wouldn't exist if its author must work such extreme hours. If you're benefiting from hobbyist projects like this one, please consider to support the action." + } + ], + "Presets": [ + { + "Title": "SDF.org Unix Shell", + "Type": "SSH", + "Host": "sdf.org:22", + "Meta": { + "Encoding": "utf-8", + "Authentication": "Password" + } + }, + { + "Title": "My own super secure server", + "Type": "SSH", + "Host": "localhost", + "Meta": { + "User": "root", + "Encoding": "utf-8", + "Private Key": "-----BEGIN RSA Will be sent to client-END RSA PRI...\n", + "Authentication": "Private Key", + "Fingerprint": "SHA256:bgO...." + } + }, + { + "Title": "My own super expensive router", + "Type": "Telnet", + "Host": "10.0.0.1", + "Meta": { + "Encoding": "ibm866" + } + } + ], + "OnlyAllowPresetRemotes": false +} diff --git a/sshwifty.go b/sshwifty.go new file mode 100644 index 0000000..0c2b1c7 --- /dev/null +++ b/sshwifty.go @@ -0,0 +1,54 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + "os" + + "github.com/nirui/sshwifty/application" + "github.com/nirui/sshwifty/application/commands" + "github.com/nirui/sshwifty/application/configuration" + "github.com/nirui/sshwifty/application/controller" + "github.com/nirui/sshwifty/application/log" +) + +func main() { + configLoaders := make([]configuration.Loader, 0, 2) + + if len(os.Getenv("SSHWIFTY_CONFIG")) > 0 { + configLoaders = append(configLoaders, + configuration.File(os.Getenv("SSHWIFTY_CONFIG"))) + } else { + configLoaders = append(configLoaders, configuration.File("")) + configLoaders = append(configLoaders, configuration.Enviro()) + } + + e := application. + New(os.Stderr, log.NewDebugOrNonDebugWriter( + len(os.Getenv("SSHWIFTY_DEBUG")) > 0, application.Name, os.Stderr)). + Run(configuration.Redundant(configLoaders...), + application.DefaultProccessSignallerBuilder, + commands.New(), + controller.Builder) + + if e == nil { + return + } + + os.Exit(1) +} diff --git a/traefik-forward-auth.env b/traefik-forward-auth.env new file mode 100644 index 0000000..a9b42d3 --- /dev/null +++ b/traefik-forward-auth.env @@ -0,0 +1,8 @@ +## KEYCLOAK ENV + +CLIENT_ID=sshwifty +CLIENT_SECRET= +OIDC_ISSUER=https:///auth/realms/garagenum +SECRET= +AUTH_HOST=https://id.legaragenumerique.fr +COOKIE_DOMAIN= \ No newline at end of file diff --git a/ui/app.css b/ui/app.css new file mode 100644 index 0000000..b2a8ce3 --- /dev/null +++ b/ui/app.css @@ -0,0 +1,73 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "utf-8"; + +#app { + padding: 0; + margin: 0; + width: 100%; + height: 100%; + position: relative; + min-width: 320px; +} + +body.app-error { + background: #b44; + color: #fff; +} + +#app-loading { + width: 100%; + min-height: 100%; + display: flex; + flex-direction: column; + justify-items: center; + justify-content: center; +} + +#app-loading-frame { + flex: 0 0; + padding: 30px; + text-align: center; +} + +#app-loading-icon { + background: url("./widgets/busy.svg") center center no-repeat; + background-size: contain; + width: 100%; + height: 200px; +} + +#app-loading-error { + font-size: 5em; + color: #fff; +} + +#app-loading-title { + color: #fab; + font-size: 1.2em; + font-weight: lighter; + max-width: 500px; + margin: 0 auto; +} + +#app-loading-title.error { + color: #fff; +} diff --git a/ui/app.js b/ui/app.js new file mode 100644 index 0000000..8857dcf --- /dev/null +++ b/ui/app.js @@ -0,0 +1,459 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import Vue from "vue"; +import "./app.css"; +import Auth from "./auth.vue"; +import { Color as ControlColor } from "./commands/color.js"; +import { Commands } from "./commands/commands.js"; +import { Controls } from "./commands/controls.js"; +import { Presets } from "./commands/presets.js"; +import * as ssh from "./commands/ssh.js"; +import * as telnet from "./commands/telnet.js"; +import "./common.css"; +import * as sshctl from "./control/ssh.js"; +import * as telnetctl from "./control/telnet.js"; +import * as cipher from "./crypto.js"; +import Home from "./home.vue"; +import "./landing.css"; +import Loading from "./loading.vue"; +import { Socket } from "./socket.js"; +import * as stream from "./stream/common"; +import * as xhr from "./xhr.js"; + +const backendQueryRetryDelay = 2000; + +const maxTimeDiff = 30000; + +const updateIndicatorMaxDisplayTime = 3000; + +const mainTemplate = ` + + + +`.trim(); + +const socksInterface = "/sshwifty/socket"; +const socksVerificationInterface = socksInterface + "/verify"; +const socksKeyTimeTruncater = 100 * 1000; + +function startApp(rootEl) { + const pageTitle = document.title; + + let uiControlColor = new ControlColor(); + + function getCurrentKeyMixer() { + return Number( + Math.trunc(new Date().getTime() / socksKeyTimeTruncater), + ).toString(); + } + + async function buildSocketKey(privateKey) { + return new Uint8Array( + await cipher.hmac512( + stream.buildBufferFromString(privateKey), + stream.buildBufferFromString(getCurrentKeyMixer()), + ), + ).slice(0, 16); + } + + new Vue({ + el: rootEl, + components: { + loading: Loading, + auth: Auth, + home: Home, + }, + data() { + return { + hostPath: + window.location.protocol + + "//" + + window.location.host + + window.location.pathname, + query: + window.location.hash.length > 0 && + window.location.hash.indexOf("#") === 0 + ? window.location.hash.slice(1, window.location.hash.length) + : "", + page: "loading", + key: "", + serverMessage: "", + presetData: { + presets: new Presets([]), + restricted: false, + }, + authErr: "", + loadErr: "", + socket: null, + controls: new Controls([ + new telnetctl.Telnet(uiControlColor), + new sshctl.SSH(uiControlColor), + ]), + commands: new Commands([new telnet.Command(), new ssh.Command()]), + tabUpdateIndicator: null, + viewPort: { + dim: { + width: 0, + height: 0, + renew(width, height) { + this.width = width; + this.height = height; + }, + }, + }, + viewPortUpdaters: { + width: 0, + height: 0, + dimResizer: null, + }, + }; + }, + watch: { + loadErr() { + this.isErrored() + ? document.body.classList.add("app-error") + : document.body.classList.remove("app-error"); + }, + authErr() { + this.isErrored() + ? document.body.classList.add("app-error") + : document.body.classList.remove("app-error"); + }, + }, + mounted() { + const self = this; + + self.tryInitialAuth(); + + self.viewPortUpdaters.dimResizer = () => { + self.viewPortUpdaters.height = window.innerHeight; + self.viewPortUpdaters.width = window.innerWidth; + + self.$nextTick(() => { + self.viewPort.dim.renew( + self.viewPortUpdaters.width, + self.viewPortUpdaters.height, + ); + }); + }; + + window.addEventListener("resize", self.viewPortUpdaters.dimResizer); + }, + beforeDestroy() { + window.removeEventListener("resize", self.viewPortUpdaters.dimResizer); + }, + methods: { + changeTitleInfo(newTitleInfo) { + document.title = newTitleInfo + " " + pageTitle; + }, + resetTitleInfo() { + document.title = pageTitle; + }, + changeURLHash(newHash) { + window.location.hash = newHash; + }, + isErrored() { + return this.authErr.length > 0 || this.loadErr.length > 0; + }, + async getSocketAuthKey(privateKey) { + const enc = new TextEncoder(), + rTime = Number(Math.trunc(new Date().getTime() / 100000)); + + var finalKey = ""; + + if (privateKey.length <= 0) { + finalKey = "DEFAULT VERIFY KEY"; + } else { + finalKey = privateKey; + } + + return new Uint8Array( + await cipher.hmac512(enc.encode(finalKey), enc.encode(rTime)), + ).slice(0, 32); + }, + buildBackendSocketURLs() { + let r = { + webSocket: "", + keepAlive: "", + }; + + switch (location.protocol) { + case "https:": + r.webSocket = "wss://"; + break; + + default: + r.webSocket = "ws://"; + } + + r.webSocket += location.host + socksInterface; + r.keepAlive = location.protocol + "//" + location.host + socksInterface; + + return r; + }, + buildSocket(key, dialTimeout, heartbeatInterval) { + return new Socket( + this.buildBackendSocketURLs(), + key, + dialTimeout * 1000, + heartbeatInterval * 1000, + ); + }, + executeHomeApp(authResult, key) { + let authData = JSON.parse(authResult.data); + this.serverMessage = authData.server_message + ? authData.server_message + : ""; + this.presetData = { + presets: new Presets(authData.presets ? authData.presets : []), + restricted: authResult.onlyAllowPresetRemotes, + }; + this.socket = this.buildSocket( + key, + authResult.timeout, + authResult.heartbeat, + ); + this.page = "app"; + }, + async doAuth(privateKey) { + let result = await this.requestAuth(privateKey); + + if (result.key) { + this.key = result.key; + } + + return result; + }, + async requestAuth(privateKey) { + let authKey = + !privateKey || !this.key + ? null + : await this.getSocketAuthKey(privateKey); + + let h = await xhr.get(socksVerificationInterface, { + "X-Key": authKey + ? btoa(String.fromCharCode.apply(null, authKey)) + : "", + }); + + let serverDate = h.getResponseHeader("Date"); + + return { + result: h.status, + key: h.getResponseHeader("X-Key"), + timeout: h.getResponseHeader("X-Timeout"), + heartbeat: h.getResponseHeader("X-Heartbeat"), + date: serverDate ? new Date(serverDate) : null, + data: h.responseText, + onlyAllowPresetRemotes: + h.getResponseHeader("X-OnlyAllowPresetRemotes") === "yes", + }; + }, + async tryInitialAuth() { + try { + let result = await this.doAuth(""); + + if (result.date) { + let serverTime = result.date.getTime(), + clientTime = new Date().getTime(), + timeDiff = Math.abs(serverTime - clientTime); + + if (timeDiff > maxTimeDiff) { + this.loadErr = + "The time difference between this client " + + "and the backend server is beyond operational limit.\r\n\r\n" + + "Please try reload the page, and if the problem persisted, " + + "consider to adjust your local time so both the client and " + + "the server are running at same date time"; + + return; + } + } + + let self = this; + switch (result.result) { + case 200: + this.executeHomeApp(result, { + async fetch() { + let result = await self.doAuth(""); + + if (result.result !== 200) { + throw new Error( + "Unable to fetch key from remote, unexpected " + + "error code: " + + result.result, + ); + } + + return await buildSocketKey(atob(result.key) + "+"); + }, + }); + break; + + case 403: + this.page = "auth"; + break; + + case 0: + setTimeout(() => { + this.tryInitialAuth(); + }, backendQueryRetryDelay); + break; + + default: + alert("Unexpected backend query status: " + result.result); + } + } catch (e) { + this.loadErr = "Unable to initialize client application: " + e; + } + }, + async submitAuth(passphrase) { + this.authErr = ""; + + try { + let result = await this.doAuth(passphrase); + + let self = this; + switch (result.result) { + case 200: + this.executeHomeApp(result, { + async fetch() { + let result = await self.doAuth(passphrase); + + if (result.result !== 200) { + throw new Error( + "Unable to fetch key from remote, unexpected " + + "error code: " + + result.result, + ); + } + + return await buildSocketKey( + atob(result.key) + "+" + passphrase, + ); + }, + }); + break; + + case 403: + this.authErr = "Authentication échoué. Mauvais mot de passe?"; + break; + + default: + this.authErr = + "Unexpected backend query status: " + result.result; + } + } catch (e) { + this.authErr = "Authentification impossible: " + e; + } + }, + updateTabTitleInfo(tabs, updated) { + if (tabs.length <= 0) { + this.resetTitleInfo(); + + return; + } + + this.changeTitleInfo("(" + tabs.length + (updated ? "*" : "") + ")"); + }, + tabOpened(tabs) { + this.tabUpdated(tabs); + }, + tabClosed(tabs) { + if (tabs.length > 0) { + this.updateTabTitleInfo(tabs, this.tabUpdateIndicator !== null); + + return; + } + + if (this.tabUpdateIndicator) { + clearTimeout(this.tabUpdateIndicator); + this.tabUpdateIndicator = null; + } + + this.updateTabTitleInfo(tabs, false); + }, + tabUpdated(tabs) { + if (this.tabUpdateIndicator) { + clearTimeout(this.tabUpdateIndicator); + this.tabUpdateIndicator = null; + } + + this.updateTabTitleInfo(tabs, true); + + this.tabUpdateIndicator = setTimeout(() => { + this.tabUpdateIndicator = null; + this.updateTabTitleInfo(tabs, false); + }, updateIndicatorMaxDisplayTime); + }, + }, + }); +} + +function initializeClient() { + let landingRoot = document.getElementById("landing"); + + if (!landingRoot) { + return; + } + + if (process.env.NODE_ENV === "development") { + console.log("Currently in Development environment"); + } + + window.addEventListener("unhandledrejection", function (e) { + console.error("Error:", e); + }); + + window.addEventListener("error", function (e) { + console.error("Error:", e); + }); + + landingRoot.parentNode.removeChild(landingRoot); + + let normalRoot = document.createElement("div"); + normalRoot.setAttribute("id", "app"); + normalRoot.innerHTML = mainTemplate; + + document.body.insertBefore(normalRoot, document.body.firstChild); + + startApp(normalRoot); +} + +window.addEventListener("load", initializeClient); +document.addEventListener("load", initializeClient); +document.addEventListener("DOMContentLoaded", initializeClient); diff --git a/ui/auth.vue b/ui/auth.vue new file mode 100644 index 0000000..aa6e6dc --- /dev/null +++ b/ui/auth.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/ui/commands/address.js b/ui/commands/address.js new file mode 100644 index 0000000..6647b81 --- /dev/null +++ b/ui/commands/address.js @@ -0,0 +1,230 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as reader from "../stream/reader.js"; +import * as common from "./common.js"; +import Exception from "./exception.js"; + +export const LOOPBACK = 0x00; +export const IPV4 = 0x01; +export const IPV6 = 0x02; +export const HOSTNAME = 0x03; + +export const MAX_ADDR_LEN = 0x3f; + +export class Address { + /** + * Read builds an Address from data readed from the reader + * + * @param {reader.Reader} rd The reader + * + * @returns {Address} The Address + * + * @throws {Exception} when address type is invalid + */ + static async read(rd) { + let readed = await reader.readN(rd, 3), + portNum = 0, + addrType = LOOPBACK, + addrData = null; + + portNum |= readed[0]; + portNum <<= 8; + portNum |= readed[1]; + + addrType = readed[2] >> 6; + + switch (addrType) { + case LOOPBACK: + break; + + case IPV4: + addrData = await reader.readN(rd, 4); + break; + + case IPV6: + addrData = await reader.readN(rd, 16); + break; + + case HOSTNAME: + addrData = await reader.readN(rd, 0x3f & readed[2]); + break; + + default: + throw new Exception("Unknown address type"); + } + + return new Address(addrType, addrData, portNum); + } + + /** + * constructor + * + * @param {number} type Type of the address + * @param {Uint8Array} address Address data + * @param {number} port port number of the address + * + */ + constructor(type, address, port) { + this.addrType = type; + this.addrData = address; + this.addrPort = port; + } + + /** + * Return the address type + * + */ + type() { + return this.addrType; + } + + /** + * Return the address data + * + */ + address() { + return this.addrData; + } + + /** + * Return the port data + * + */ + port() { + return this.addrPort; + } + + /** + * Buffer returns the marshalled address + * + * @returns {Uint8Array} Marshalled address + * + * @throws {Exception} When address data is invalid + * + */ + buffer() { + switch (this.type()) { + case LOOPBACK: + return new Uint8Array([ + this.addrPort >> 8, + this.addrPort & 0xff, + LOOPBACK << 6, + ]); + + case IPV4: + if (this.addrData.length != 4) { + throw new Exception("Invalid address length"); + } + + return new Uint8Array([ + this.addrPort >> 8, + this.addrPort & 0xff, + IPV4 << 6, + this.addrData[0], + this.addrData[1], + this.addrData[2], + this.addrData[3], + ]); + + case IPV6: + if (this.addrData.length != 16) { + throw new Exception("Invalid address length"); + } + + return new Uint8Array([ + this.addrPort >> 8, + this.addrPort & 0xff, + IPV6 << 6, + this.addrData[0], + this.addrData[1], + this.addrData[2], + this.addrData[3], + this.addrData[4], + this.addrData[5], + this.addrData[6], + this.addrData[7], + this.addrData[8], + this.addrData[9], + this.addrData[10], + this.addrData[11], + this.addrData[12], + this.addrData[13], + this.addrData[14], + this.addrData[15], + ]); + + case HOSTNAME: + if (this.addrData.length > MAX_ADDR_LEN) { + throw new Exception("Host name cannot longer than " + MAX_ADDR_LEN); + } + + { + let dataBuf = new Uint8Array(this.addrData.length + 3); + + dataBuf[0] = (this.addrPort >> 8) & 0xff; + dataBuf[1] = this.addrPort & 0xff; + dataBuf[2] = HOSTNAME << 6; + dataBuf[2] |= this.addrData.length; + + dataBuf.set(this.addrData, 3); + + return dataBuf; + } + + default: + throw new Exception("Unknown address type"); + } + } +} + +/** + * Get address data + * + * @param {string} s Address string + * @param {number} defaultPort Default port number + * + * @returns {object} result + * + * @throws {Exception} when the address is invalid + */ +export function parseHostPort(s, defaultPort) { + let d = common.splitHostPort(s, defaultPort), + t = HOSTNAME; + + switch (d.type) { + case "IPv4": + t = IPV4; + break; + + case "IPv6": + t = IPV6; + break; + + case "Hostname": + break; + + default: + throw new Exception("Invalid address type"); + } + + return { + type: t, + address: d.addr, + port: d.port, + }; +} diff --git a/ui/commands/address_test.js b/ui/commands/address_test.js new file mode 100644 index 0000000..ca1028a --- /dev/null +++ b/ui/commands/address_test.js @@ -0,0 +1,102 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import assert from "assert"; +import * as reader from "../stream/reader.js"; +import * as address from "./address.js"; + +describe("Address", () => { + it("Address Loopback", async () => { + let addr = new address.Address(address.LOOPBACK, null, 8080), + buf = addr.buffer(); + + let r = new reader.Reader(new reader.Multiple(), (data) => { + return data; + }); + + r.feed(buf); + + let addr2 = await address.Address.read(r); + + assert.strictEqual(addr2.type(), addr.type()); + assert.deepStrictEqual(addr2.address(), addr.address()); + assert.strictEqual(addr2.port(), addr.port()); + }); + + it("Address IPv4", async () => { + let addr = new address.Address( + address.IPV4, + new Uint8Array([127, 0, 0, 1]), + 8080, + ), + buf = addr.buffer(); + + let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return data; + }); + + r.feed(buf); + + let addr2 = await address.Address.read(r); + + assert.strictEqual(addr2.type(), addr.type()); + assert.deepStrictEqual(addr2.address(), addr.address()); + assert.strictEqual(addr2.port(), addr.port()); + }); + + it("Address IPv6", async () => { + let addr = new address.Address( + address.IPV6, + new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]), + 8080, + ), + buf = addr.buffer(); + + let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return data; + }); + + r.feed(buf); + + let addr2 = await address.Address.read(r); + + assert.strictEqual(addr2.type(), addr.type()); + assert.deepStrictEqual(addr2.address(), addr.address()); + assert.strictEqual(addr2.port(), addr.port()); + }); + + it("Address HostName", async () => { + let addr = new address.Address( + address.HOSTNAME, + new Uint8Array(["v", "a", "g", "u", "l", "1", "2", "3"]), + 8080, + ), + buf = addr.buffer(); + + let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return data; + }); + + r.feed(buf); + + let addr2 = await address.Address.read(r); + + assert.strictEqual(addr2.type(), addr.type()); + assert.deepStrictEqual(addr2.address(), addr.address()); + assert.strictEqual(addr2.port(), addr.port()); + }); +}); diff --git a/ui/commands/color.js b/ui/commands/color.js new file mode 100644 index 0000000..897e461 --- /dev/null +++ b/ui/commands/color.js @@ -0,0 +1,107 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/** + * Get one color hex byte + * + * @param {number} from Min color number + * @param {number} to Max color number + * + * @returns {string} color byte in string + * + */ +function getRandHex(from, to) { + let color = Math.random() * (to - from) + from, + colorDark = color - color / 20; + + let r = Math.round(color).toString(16), + rDark = Math.round(colorDark).toString(16); + + if (r.length % 2 !== 0) { + r = "0" + r; + } + + if (rDark.length % 2 !== 0) { + rDark = "0" + rDark; + } + + return [r, rDark]; +} + +/** + * Get rand color + * + * @param {number} from Min color number + * @param {number} to Max color number + * + * @returns {string} Color bytes in string + */ +function getRandColor(from, to) { + let r = getRandHex(from, to), + g = getRandHex(from, to), + b = getRandHex(from, to); + + return ["#" + r[0] + g[0] + b[0], "#" + r[1] + g[1] + b[1]]; +} + +export class Color { + /** + * constructor + */ + constructor() { + this.assignedColors = {}; + } + + /** + * Get one color + * + * @returns {string} Color code + * + */ + get() { + const maxTries = 10; + let tried = 0; + + for (;;) { + let color = getRandColor(0x22, 0x33); + + if (this.assignedColors[color[0]]) { + tried++; + + if (tried < maxTries) { + continue; + } + } + + this.assignedColors[color[0]] = true; + + return { + color: color[0], + dark: color[1], + }; + } + } + + /** + * forget already assigned color + * + * @param {string} color Color code + */ + forget(color) { + delete this.assignedColors[color]; + } +} diff --git a/ui/commands/commands.js b/ui/commands/commands.js new file mode 100644 index 0000000..4c0db55 --- /dev/null +++ b/ui/commands/commands.js @@ -0,0 +1,880 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as subscribe from "../stream/subscribe.js"; +import Exception from "./exception.js"; +import * as presets from "./presets.js"; + +export const NEXT_PROMPT = 1; +export const NEXT_WAIT = 2; +export const NEXT_DONE = 3; + +export class Result { + /** + * constructor + * + * @param {string} name Result type + * @param {Info} info Result info + * @param {object} control Result controller + * @param {string} ui User interfact this command will use + */ + constructor(name, info, control, ui) { + this.name = name; + this.info = info; + this.control = control; + this.ui = ui; + } +} + +class Done { + /** + * constructor + * + * @param {object} data Step data + * + */ + constructor(data) { + this.s = !!data.success; + this.d = data.successData; + this.errorTitle = data.errorTitle; + this.errorMessage = data.errorMessage; + } + + /** + * Return the error of current Done + * + * @returns {string} title + * + */ + error() { + return this.errorTitle; + } + + /** + * Return the error message of current Done + * + * @returns {string} message + * + */ + message() { + return this.errorMessage; + } + + /** + * Returns whether or not current Done is representing a success + * + * @returns {boolean} True when success, false otherwise + */ + success() { + return this.s; + } + + /** + * Returns final data + * + * @returns {Result} Successful result + */ + data() { + return this.d; + } +} + +class Wait { + /** + * constructor + * + * @param {object} data Step data + * + */ + constructor(data) { + this.t = data.title; + this.m = data.message; + } + + /** + * Return the title of current Wait + * + * @returns {string} title + * + */ + title() { + return this.t; + } + + /** + * Return the message of current Wait + * + * @returns {string} message + * + */ + message() { + return this.m; + } +} + +const defField = { + name: "", + description: "", + type: "", + value: "", + example: "", + readonly: false, + suggestions(input) { + return []; + }, + verify(v) { + return ""; + }, +}; + +/** + * Create a Prompt field + * + * @param {object} def Field default value + * @param {object} f Field value + * + * @returns {object} Field data + * + * @throws {Exception} When input field is invalid + * + */ +export function field(def, f) { + let n = {}; + + for (let i in def) { + n[i] = def[i]; + } + + for (let i in f) { + if (typeof n[i] === typeof f[i]) { + n[i] = f[i]; + + continue; + } + + throw new Exception( + 'Field data type for "' + + i + + '" was unmatched. Expecting "' + + typeof n[i] + + '", got "' + + typeof f[i] + + '" instead', + ); + } + + if (!n["name"]) { + throw new Exception('Field "name" must be specified'); + } + + return n; +} + +/** + * Build a group of field value + * + * @param {object} definitions Definition of a group of fields + * @param {Array} fs Data of the field group + * + * @returns {Array} Result fields + * + * @throws {Exception} When input field is invalid + * + */ +export function fields(definitions, fs) { + let fss = []; + + for (let i in fs) { + if (!fs[i]["name"]) { + throw new Exception('Field "name" must be specified'); + } + + if (!definitions[fs[i].name]) { + throw new Exception('Undefined field "' + fs[i].name + '"'); + } + + fss.push(field(definitions[fs[i].name], fs[i])); + } + + return fss; +} + +/** + * Build command fields with preset data + * + * @param {object} definitions Definition of a group of fields + * @param {object} fieldsData field data object, formated like a `defField` + * @param {presets.Preset} presetData Preset data + * @param {function} presetApplied Called when a preset is used for a field + * + * @returns {object} + * + */ +export function fieldsWithPreset( + definitions, + fieldsData, + presetData, + presetApplied, +) { + let newFields = fields(definitions, fieldsData); + + for (let i in newFields) { + try { + newFields[i].value = presetData.meta(newFields[i].name); + newFields[i].readonly = true; + + presetApplied(newFields[i].name); + } catch (e) { + // Do nothing + } + } + + return newFields; +} + +class Prompt { + /** + * constructor + * + * @param {object} data Step data + * + * @throws {Exception} If the field verify is not a function while + * not null + */ + constructor(data) { + this.t = data.title; + this.m = data.message; + this.a = data.actionText; + this.r = data.respond; + this.c = data.cancel; + + this.i = []; + this.f = {}; + + for (let i in data.inputs) { + let f = field(defField, data.inputs[i]); + + this.i.push(f); + + this.f[data.inputs[i].name.toLowerCase()] = { + value: f.value, + verify: f.verify, + }; + } + } + + /** + * Return the title of current Prompt + * + * @returns {string} title + * + */ + title() { + return this.t; + } + + /** + * Return the message of current Prompt + * + * @returns {string} message + * + */ + message() { + return this.m; + } + + /** + * Return the input field of current prompt + * + * @returns {array} Input fields + * + */ + inputs() { + let inputs = []; + + for (let i in this.i) { + inputs.push(this.i[i]); + } + + return inputs; + } + + /** + * Returns the name of the action + * + * @returns {string} Action name + * + */ + actionText() { + return this.a; + } + + /** + * Receive the submit of current prompt + * + * @param {object} inputs Input value + * + * @returns {any} The result of the step responder + * + * @throws {Exception} When the field is undefined or invalid + * + */ + submit(inputs) { + let fields = {}; + + for (let i in this.f) { + fields[i] = this.f[i].value; + } + + for (let i in inputs) { + let k = i.toLowerCase(); + + if (typeof fields[k] === "undefined") { + throw new Exception('Field "' + k + '" is undefined'); + } + + try { + this.f[k].verify(inputs[i]); + } catch (e) { + throw new Exception('Field "' + k + '" is invalid: ' + e); + } + + fields[k] = inputs[i]; + } + + return this.r(fields); + } + + /** + * Cancel current wait operation + * + */ + cancel() { + return this.c(); + } +} + +/** + * Create a Wizard step + * + * @param {string} type Step type + * @param {object} data Step data + * + * @returns {object} Step data + * + */ +function next(type, data) { + return { + type() { + return type; + }, + data() { + return data; + }, + }; +} + +/** + * Create data for a Done step of the wizard + * + * @param {boolean} success + * @param {Success} successData + * @param {string} errorTitle + * @param {string} errorMessage + * + * @returns {object} Done step data + * + */ +export function done(success, successData, errorTitle, errorMessage) { + return next(NEXT_DONE, { + success: success, + successData: successData, + errorTitle: errorTitle, + errorMessage: errorMessage, + }); +} + +/** + * Create data for a Wait step of the wizard + * + * @param {string} title Waiter title + * @param {message} message Waiter message + * + * @returns {object} Done step data + * + */ +export function wait(title, message) { + return next(NEXT_WAIT, { + title: title, + message: message, + }); +} + +/** + * Create data for a Prompt step of the wizard + * + * @param {string} title Title of the prompt + * @param {string} message Message of the prompt + * @param {string} actionText Text of the action (button) + * @param {function} respond Respond callback + * @param {function} cancel cancel handler + * @param {object} inputs Input field objects + * + * @returns {object} Prompt step data + * + */ +export function prompt(title, message, actionText, respond, cancel, inputs) { + return next(NEXT_PROMPT, { + title: title, + message: message, + actionText: actionText, + inputs: inputs, + respond: respond, + cancel: cancel, + }); +} + +class Next { + /** + * constructor + * + * @param {object} data Step data + */ + constructor(data) { + this.t = data.type(); + this.d = data.data(); + } + + /** + * Return step type + * + * @returns {string} Step type + */ + type() { + return this.t; + } + + /** + * Return step data + * + * @returns {Done|Prompt} Step data + * + * @throws {Exception} When the step type is unknown + * + */ + data() { + switch (this.type()) { + case NEXT_PROMPT: + return new Prompt(this.d); + + case NEXT_WAIT: + return new Wait(this.d); + + case NEXT_DONE: + return new Done(this.d); + + default: + throw new Exception("Unknown data type"); + } + } +} + +class Wizard { + /** + * constructor + * + * @param {object} built Command executer + * @param {subscribe.Subscribe} subs Wizard step subscriber + * @param {function} done Callback which will be called when the wizard + * is done + * + */ + constructor(built, subs, done) { + this.built = built; + this.subs = subs; + this.done = done; + this.closed = false; + + this.built.run(); + } + + /** + * Return the Next step + * + * @returns {Next} Next step + * + * @throws {Exception} When wizard is closed + * + */ + async next() { + if (this.closed) { + throw new Exception("Wizard already closed, no next step is available"); + } + + let n = await this.subs.subscribe(); + + if (n.type() === NEXT_DONE) { + this.close(); + this.done(n); + } + + return new Next(n); + } + + /** + * Return whether or not the command is started + * + * @returns {boolean} True when the command already started, false otherwise + * + */ + started() { + return this.built.started(); + } + + /** + * Return the name of the control info of current wizard + * + * @returns {object} + * + */ + control() { + return this.built.control(); + } + + /** + * Close current wizard + * + * @returns {any} Close result + * + */ + close() { + if (this.closed) { + return; + } + + this.closed = true; + + return this.built.close(); + } +} + +export class Info { + /** + * constructor + * + * @param {Builder} info Builder info + * + */ + constructor(info) { + this.type = info.name(); + this.info = info.description(); + this.tcolor = info.color(); + } + + /** + * Return command name + * + * @returns {string} Command name + * + */ + name() { + return this.type; + } + + /** + * Return command description + * + * @returns {string} Command description + * + */ + description() { + return this.info; + } + + /** + * Return the theme color of the command + * + * @returns {string} Command name + * + */ + color() { + return this.tcolor; + } +} + +class Builder { + /** + * constructor + * + * @param {object} command Command builder + * + */ + constructor(command) { + this.cid = command.id(); + this.represeter = (n) => { + return command.represet(n); + }; + this.wizarder = (n, i, r, u, y, x, l, p) => { + return command.wizard(n, i, r, u, y, x, l, p); + }; + this.executer = (n, i, r, u, y, x, l, p) => { + return command.execute(n, i, r, u, y, x, l, p); + }; + this.launchCmd = (n, i, r, u, y, x) => { + return command.launch(n, i, r, u, y, x); + }; + this.launcherCmd = (c) => { + return command.launcher(c); + }; + this.type = command.name(); + this.info = command.description(); + this.tcolor = command.color(); + } + + /** + * Return the command ID + * + * @returns {number} Command ID + * + */ + id() { + return this.cid; + } + + /** + * Return command name + * + * @returns {string} Command name + * + */ + name() { + return this.type; + } + + /** + * Return command description + * + * @returns {string} Command description + * + */ + description() { + return this.info; + } + + /** + * Return the theme color of the command + * + * @returns {string} Command name + * + */ + color() { + return this.tcolor; + } + + /** + * Execute an automatic command wizard + * + * @param {stream.Streams} streams + * @param {controls.Controls} controls + * @param {history.History} history + * @param {presets.Preset} preset + * @param {object} session + * @param {Array} keptSessions + * @param {function} done Callback which will be called when wizard is done + * + * @returns {Wizard} Command wizard + * + */ + wizard(streams, controls, history, preset, session, keptSessions, done) { + let subs = new subscribe.Subscribe(); + + return new Wizard( + this.wizarder( + new Info(this), + preset, + session, + keptSessions, + streams, + subs, + controls, + history, + ), + subs, + done, + ); + } + + /** + * Execute an automatic command wizard + * + * @param {stream.Streams} streams + * @param {controls.Controls} controls + * @param {history.History} history + * @param {object} config + * @param {object} session + * @param {Array} keptSessions + * @param {function} done Callback which will be called when wizard is done + * + * @returns {Wizard} Command wizard + * + */ + execute(streams, controls, history, config, session, keptSessions, done) { + let subs = new subscribe.Subscribe(); + + return new Wizard( + this.executer( + new Info(this), + config, + session, + keptSessions, + streams, + subs, + controls, + history, + ), + subs, + done, + ); + } + + /** + * Launch command wizard out of given launcher string + * + * @param {stream.Streams} streams + * @param {controls.Controls} controls + * @param {history.History} history + * @param {string} launcher Launcher format + * @param {function} done Callback which will be called when launching is done + * + * @returns {Wizard} Command wizard + * + */ + launch(streams, controls, history, launcher, done) { + let subs = new subscribe.Subscribe(); + + return new Wizard( + this.launchCmd( + new Info(this), + decodeURI(launcher), + streams, + subs, + controls, + history, + ), + subs, + done, + ); + } + + /** + * Build launcher string out of given config + * + * @param {object} config Configuration object + * + * @return {string} Launcher string + */ + launcher(config) { + return this.name() + ":" + encodeURI(this.launcherCmd(config)); + } + + /** + * Reconfigure the preset data for the command wizard + * + * @param {presets.Preset} n preset + * + * @return {presets.Preset} modified new preset + */ + represet(n) { + return this.represeter(n); + } +} + +export class Preset { + /** + * constructor + * + * @param {presets.Preset} preset preset + * @param {Builder} command executor + * + */ + constructor(preset, command) { + this.preset = preset; + this.command = command; + } +} + +export class Commands { + /** + * constructor + * + * @param {Array} commands Command array + * + */ + constructor(commands) { + this.commands = []; + + for (let i = 0; i < commands.length; i++) { + this.commands.push(new Builder(commands[i])); + } + } + + /** + * Return all commands + * + * @returns {Array} A group of command + * + */ + all() { + return this.commands; + } + + /** + * Select one command + * + * @param {number} id Command ID + * + * @returns {Builder} Command builder + * + */ + select(id) { + return this.commands[id]; + } + + /** + * Returns presets with merged command + * + * @param {presets.Presets} ps + * + * @returns {Array} + * + */ + mergePresets(ps) { + let pp = []; + + for (let i = 0; i < this.commands.length; i++) { + const fetched = ps.fetch(this.commands[i].name()); + + for (let j = 0; j < fetched.length; j++) { + pp.push( + new Preset(this.commands[i].represet(fetched[j]), this.commands[i]), + ); + } + } + + return pp; + } +} diff --git a/ui/commands/common.js b/ui/commands/common.js new file mode 100644 index 0000000..598d403 --- /dev/null +++ b/ui/commands/common.js @@ -0,0 +1,409 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as buffer from "buffer/"; +import * as iconv from "iconv-lite"; +import Exception from "./exception.js"; + +const availableEncodings = [ + "utf-8", + "ibm866", + "iso-8859-2", + "iso-8859-3", + "iso-8859-4", + "iso-8859-5", + "iso-8859-6", + "iso-8859-7", + "iso-8859-8", + "iso-8859-10", + "iso-8859-13", + "iso-8859-14", + "iso-8859-15", + "iso-8859-16", + "koi8-r", + "koi8-u", + "macintosh", + "windows-874", + "windows-1250", + "windows-1251", + "windows-1252", + "windows-1253", + "windows-1254", + "windows-1255", + "windows-1256", + "windows-1257", + "windows-1258", + "gbk", + "gb18030", + "big5", + "euc-jp", + "shift-jis", + "euc-kr", + "utf-16be", + "utf-16le", +]; + +export const charsetPresets = (() => { + let r = []; + + for (let i in availableEncodings) { + try { + if (!iconv.encodingExists(availableEncodings[i])) { + continue; + } + + new TextDecoder(availableEncodings[i]); + + r.push(availableEncodings[i]); + } catch (e) { + // Do nothing + } + } + + return r; +})(); + +const numCharators = { + 0: true, + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, +}; + +const hexCharators = { + 0: true, + 1: true, + 2: true, + 3: true, + 4: true, + 5: true, + 6: true, + 7: true, + 8: true, + 9: true, + a: true, + b: true, + c: true, + d: true, + e: true, + f: true, +}; + +/** + * Test whether or not given string is all number + * + * @param {string} d Input data + * + * @returns {boolean} Return true if given string is all number, false otherwise + * + */ +export function isNumber(d) { + for (let i = 0; i < d.length; i++) { + if (!numCharators[d[i]]) { + return false; + } + } + + return true; +} + +/** + * Test whether or not given string is all hex + * + * @param {string} d Input data + * + * @returns {boolean} Return true if given string is all hex, false otherwise + * + */ +export function isHex(d) { + let dd = d.toLowerCase(); + + for (let i = 0; i < dd.length; i++) { + if (!hexCharators[dd[i]]) { + return false; + } + } + + return true; +} + +/** + * Test whether or not given string is a valid hostname as far as the Sshwifty + * client consider. This function will return true if the string contains only + * printable charactors + * + * @param {string} d Input data + * + * @returns {boolean} Return true if given string is all hex, false otherwise + * + */ +function isHostname(d) { + for (let i = 0; i < d.length; i++) { + const dChar = d.charCodeAt(i); + + if (dChar >= 32 && dChar <= 126) { + continue; + } + + if (dChar === 128) { + continue; + } + + if (dChar >= 130 && dChar <= 140) { + continue; + } + + if (dChar === 142) { + continue; + } + + if (dChar >= 145 && dChar <= 156) { + continue; + } + + if (dChar >= 158 && dChar <= 159) { + continue; + } + + if (dChar >= 161 && dChar <= 255) { + continue; + } + + return false; + } + + return true; +} + +/** + * Parse IPv4 address + * + * @param {string} d IP address + * + * @returns {Uint8Array} Parsed IPv4 Address + * + * @throws {Exception} When the given ip address was not an IPv4 addr + * + */ +export function parseIPv4(d) { + const addrSeg = 4; + + let s = d.split("."); + + if (s.length != addrSeg) { + throw new Exception("Invalid address"); + } + + let r = new Uint8Array(addrSeg); + + for (let i in s) { + if (!isNumber(s[i])) { + throw new Exception("Invalid address"); + } + + let ii = parseInt(s[i], 10); // Only support dec + + if (isNaN(ii)) { + throw new Exception("Invalid address"); + } + + if (ii > 0xff) { + throw new Exception("Invalid address"); + } + + r[i] = ii; + } + + return r; +} + +/** + * Parse IPv6 address. ::ffff: notation is NOT supported + * + * @param {string} d IP address + * + * @returns {Uint8Array} Parsed IPv6 Address + * + * @throws {Exception} When the given ip address was not an IPv6 addr + * + */ +export function parseIPv6(d) { + const addrSeg = 8; + let s = d.split(":"); + + if (s.length > addrSeg || s.length <= 1) { + throw new Exception("Invalid address"); + } + + if (s[0].charAt(0) === "[") { + s[0] = s[0].substring(1, s[0].length); + let end = s.length - 1; + if (s[end].charAt(s[end].length - 1) !== "]") { + throw new Exception("Invalid address"); + } + s[end] = s[end].substring(0, s[end].length - 1); + } + + let r = new Uint8Array(addrSeg * 2), + rIndexShift = 0; + for (let i = 0; i < s.length; i++) { + if (s[i].length <= 0) { + rIndexShift = addrSeg - s.length; + continue; + } + if (!isHex(s[i])) { + throw new Exception("Invalid address"); + } + let ii = parseInt(s[i], 16); // Only support hex + if (isNaN(ii)) { + throw new Exception("Invalid address"); + } + if (ii > 0xffff) { + throw new Exception("Invalid address"); + } + let j = (rIndexShift + i) * 2; + r[j] = ii >> 8; + r[j + 1] = ii & 0xff; + } + + return r; +} + +/** + * Convert string into a {Uint8Array} + * + * @param {string} d Input + * + * @returns {Uint8Array} Output + * + */ +export function strToUint8Array(d) { + let r = new Uint8Array(d.length); + + for (let i = 0, j = d.length; i < j; i++) { + r[i] = d.charCodeAt(i); + } + + return r; +} + +/** + * Convert string into a binary {Uint8Array} + * + * @param {string} d Input + * + * @returns {Uint8Array} Output + * + */ +export function strToBinary(d) { + return new Uint8Array(buffer.Buffer.from(d, "binary").buffer); +} + +/** + * Parse IPv6 address. ::ffff: notation is NOT supported + * + * @param {string} d IP address + * + * @returns {Uint8Array} Parsed IPv6 Address + * + * @throws {Exception} When the given ip address was not an IPv6 addr + * + */ +export function parseHostname(d) { + if (d.length <= 0) { + throw new Exception("Invalid address"); + } + + if (!isHostname(d)) { + throw new Exception("Invalid address"); + } + + return strToUint8Array(d); +} + +function parseIP(d) { + try { + return { + type: "IPv4", + data: parseIPv4(d), + }; + } catch (e) { + // Do nothing + } + + try { + return { + type: "IPv6", + data: new Uint8Array(parseIPv6(d).buffer), + }; + } catch (e) { + // Do nothing + } + + return { + type: "Hostname", + data: parseHostname(d), + }; +} + +export function splitHostPort(d, defPort) { + let hps = d.lastIndexOf(":"), + fhps = d.indexOf(":"), + ipv6hps = d.indexOf("["); + + if ((hps < 0 || hps != fhps) && ipv6hps < 0) { + let a = parseIP(d); + + return { + type: a.type, + addr: a.data, + port: defPort, + }; + } + + if (ipv6hps > 0) { + throw new Exception("Invalid address"); + } else if (ipv6hps === 0) { + let ipv6hpse = d.lastIndexOf("]"); + + if (ipv6hpse <= ipv6hps || ipv6hpse + 1 != hps) { + throw new Exception("Invalid address"); + } + } + + let addr = d.slice(0, hps), + port = d.slice(hps + 1, d.length); + + if (!isNumber(port)) { + throw new Exception("Invalid address"); + } + + let portNum = parseInt(port, 10), + a = parseIP(addr); + + return { + type: a.type, + addr: a.data, + port: portNum, + }; +} diff --git a/ui/commands/common_test.js b/ui/commands/common_test.js new file mode 100644 index 0000000..4f5fef4 --- /dev/null +++ b/ui/commands/common_test.js @@ -0,0 +1,246 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import assert from "assert"; +import * as common from "./common.js"; + +describe("Common", () => { + it("parseIPv4", () => { + let tests = [ + { + sample: "127.0.0.1", + expectingFailure: false, + expected: new Uint8Array([127, 0, 0, 1]), + }, + { + sample: "255.255.255.255", + expectingFailure: false, + expected: new Uint8Array([255, 255, 255, 255]), + }, + { + sample: "255.255.a.255", + expectingFailure: true, + expected: null, + }, + { + sample: "255.255.255", + expectingFailure: true, + expected: null, + }, + { + sample: "2001:db8:1f70::999:de8:7648:6e8", + expectingFailure: true, + expected: null, + }, + { + sample: "a.ssh.vaguly.com", + expectingFailure: true, + expected: null, + }, + ]; + + for (let i in tests) { + if (tests[i].expectingFailure) { + let ee = null; + + try { + common.parseIPv4(tests[i].sample); + } catch (e) { + ee = e; + } + + assert.notStrictEqual(ee, null, "Test " + tests[i].sample); + } else { + let data = common.parseIPv4(tests[i].sample); + + assert.deepStrictEqual(data, tests[i].expected); + } + } + }); + + it("parseIPv6", () => { + let tests = [ + { + sample: "2001:db8:1f70:0:999:de8:7648:6e8", + expectingFailure: false, + expected: new Uint8Array([ + 0x20, 0x01, 0xd, 0xb8, 0x1f, 0x70, 0x0, 0x0, 0x9, 0x99, 0xd, 0xe8, + 0x76, 0x48, 0x6, 0xe8, + ]), + }, + { + sample: "2001:db8:85a3::8a2e:370:7334", + expectingFailure: false, + expected: new Uint8Array([ + 0x20, 0x01, 0xd, 0xb8, 0x85, 0xa3, 0x0, 0x0, 0x0, 0x0, 0x8a, 0x2e, + 0x3, 0x70, 0x73, 0x34, + ]), + }, + { + sample: "fdef:90fb:4138::8ca", + expectingFailure: false, + expected: new Uint8Array([ + 0xfd, 0xef, 0x90, 0xfb, 0x41, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x8, 0xca, + ]), + }, + { + sample: "::1", + expectingFailure: false, + expected: new Uint8Array([ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, + ]), + }, + { + sample: "::", + expectingFailure: false, + expected: new Uint8Array([ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, + ]), + }, + { + sample: "2001:db8:1f70::999:de8:7648:6e8", + expectingFailure: false, + expected: new Uint8Array([ + 0x20, 0x01, 0xd, 0xb8, 0x1f, 0x70, 0x0, 0x0, 0x9, 0x99, 0xd, 0xe8, + 0x76, 0x48, 0x6, 0xe8, + ]), + }, + { + sample: "2001:0db8:ac10:fe01::", + expectingFailure: false, + expected: new Uint8Array([ + 0x20, 0x01, 0x0d, 0xb8, 0xac, 0x10, 0xfe, 0x01, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, + ]), + }, + { + sample: "::7f00:1", + expectingFailure: false, + expected: new Uint8Array([ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7f, + 0x00, 0x00, 0x01, + ]), + }, + { + sample: "127.0.0.1", + expectingFailure: true, + expected: null, + }, + { + sample: "255.255.255.255", + expectingFailure: true, + expected: null, + }, + { + sample: "255.255.a.255", + expectingFailure: true, + expected: null, + }, + { + sample: "255.255.255", + expectingFailure: true, + expected: null, + }, + { + sample: "a.ssh.vaguly.com", + expectingFailure: true, + expected: null, + }, + ]; + + for (let i in tests) { + if (tests[i].expectingFailure) { + let ee = null; + + try { + common.parseIPv6(tests[i].sample); + } catch (e) { + ee = e; + } + + assert.notStrictEqual(ee, null, "Test " + tests[i].sample); + } else { + let data = common.parseIPv6(tests[i].sample); + + assert.deepStrictEqual(data, tests[i].expected); + } + } + }); + + it("splitHostPort", () => { + let tests = [ + // Host name + { + sample: "ssh.vaguly.com", + expectedType: "Hostname", + expectedAddr: common.strToUint8Array("ssh.vaguly.com"), + expectedPort: 22, + }, + { + sample: "ssh.vaguly.com:22", + expectedType: "Hostname", + expectedAddr: common.strToUint8Array("ssh.vaguly.com"), + expectedPort: 22, + }, + + // IPv4 + { + sample: "10.220.179.110", + expectedType: "IPv4", + expectedAddr: new Uint8Array([10, 220, 179, 110]), + expectedPort: 22, + }, + { + sample: "10.220.179.110:3333", + expectedType: "IPv4", + expectedAddr: new Uint8Array([10, 220, 179, 110]), + expectedPort: 3333, + }, + + // IPv6 + { + sample: "2001:db8:1f70::999:de8:7648:6e8", + expectedType: "IPv6", + expectedAddr: new Uint8Array([ + 0x20, 0x01, 0xd, 0xb8, 0x1f, 0x70, 0x0, 0x0, 0x9, 0x99, 0xd, 0xe8, + 0x76, 0x48, 0x6, 0xe8, + ]), + expectedPort: 22, + }, + { + sample: "[2001:db8:1f70::999:de8:7648:6e8]:100", + expectedType: "IPv6", + expectedAddr: new Uint8Array([ + 0x20, 0x01, 0xd, 0xb8, 0x1f, 0x70, 0x0, 0x0, 0x9, 0x99, 0xd, 0xe8, + 0x76, 0x48, 0x6, 0xe8, + ]), + expectedPort: 100, + }, + ]; + + for (let i in tests) { + let hostport = common.splitHostPort(tests[i].sample, 22); + + assert.deepStrictEqual(hostport.type, tests[i].expectedType); + assert.deepStrictEqual(hostport.addr, tests[i].expectedAddr); + assert.strictEqual(hostport.port, tests[i].expectedPort); + } + }); +}); diff --git a/ui/commands/controls.js b/ui/commands/controls.js new file mode 100644 index 0000000..7de1b3d --- /dev/null +++ b/ui/commands/controls.js @@ -0,0 +1,60 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import Exception from "./exception.js"; + +export class Controls { + /** + * constructor + * + * @param {[]object} controls + * + * @throws {Exception} When control type already been defined + * + */ + constructor(controls) { + this.controls = {}; + + for (let i in controls) { + let cType = controls[i].type(); + + if (typeof this.controls[cType] === "object") { + throw new Exception('Control "' + cType + '" already been defined'); + } + + this.controls[cType] = controls[i]; + } + } + + /** + * Get a control + * + * @param {string} type Type of the control + * + * @returns {object} Control object + * + * @throws {Exception} When given control type is undefined + * + */ + get(type) { + if (typeof this.controls[type] !== "object") { + throw new Exception('Control "' + type + '" was undefined'); + } + + return this.controls[type]; + } +} diff --git a/ui/commands/events.js b/ui/commands/events.js new file mode 100644 index 0000000..67f72f1 --- /dev/null +++ b/ui/commands/events.js @@ -0,0 +1,106 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import Exception from "./exception.js"; + +export class Events { + /** + * constructor + * + * @param {[]string} events required events + * @param {object} callbacks Callbacks + * + * @throws {Exception} When event handler is not registered + * + */ + constructor(events, callbacks) { + this.events = {}; + this.placeHolders = {}; + + for (let i in events) { + if (typeof callbacks[events[i]] !== "function") { + throw new Exception( + 'Unknown event type for "' + + events[i] + + '". Expecting "function" got "' + + typeof callbacks[events[i]] + + '" instead.', + ); + } + + let name = events[i]; + + if (name.indexOf("@") === 0) { + name = name.substring(1); + + this.placeHolders[name] = null; + } + + this.events[name] = callbacks[events[i]]; + } + } + + /** + * Place callbacks to pending placeholder events + * + * @param {string} type Event Type + * @param {function} callback Callback function + */ + place(type, callback) { + if (this.placeHolders[type] !== null) { + throw new Exception( + 'Event type "' + + type + + '" cannot be appended. It maybe ' + + "unregistered or already been acquired", + ); + } + + if (typeof callback !== "function") { + throw new Exception( + 'Unknown event type for "' + + type + + '". Expecting "function" got "' + + typeof callback + + '" instead.', + ); + } + + delete this.placeHolders[type]; + + this.events[type] = callback; + } + + /** + * Fire an event + * + * @param {string} type Event type + * @param {...any} data Event data + * + * @returns {any} The result of the event handler + * + * @throws {Exception} When event type is not registered + * + */ + fire(type, ...data) { + if (!this.events[type] && this.placeHolders[type] !== null) { + throw new Exception("Unknown event type: " + type); + } + + return this.events[type](...data); + } +} diff --git a/ui/commands/exception.js b/ui/commands/exception.js new file mode 100644 index 0000000..a894b8e --- /dev/null +++ b/ui/commands/exception.js @@ -0,0 +1,28 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +export default class Exception extends Error { + /** + * constructor + * + * @param {string} message error message + * + */ + constructor(message) { + super(message); + } +} diff --git a/ui/commands/history.js b/ui/commands/history.js new file mode 100644 index 0000000..0c80bc0 --- /dev/null +++ b/ui/commands/history.js @@ -0,0 +1,311 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as command from "./commands.js"; + +/** + * Extract needed data + * + * @param {Array} kept The keys of of the data to be kept + * @param {object} input Input data + * + * @return {object} Extracted data + */ +function extractSelectedData(kept, input) { + if (!kept || typeof kept !== "object" || kept.length < 0) { + return null; + } + + let data = {}, + length = 0; + + for (let k in kept) { + if (!input[kept[k]]) { + continue; + } + + data[kept[k]] = input[kept[k]]; + length++; + } + + if (length <= 0) { + return null; + } + + return data; +} + +/** + * constructor + * + * @param {object} data to be searched + * @param {string} metaName name of the meta + * @param {string} valContains target string to search + * + */ +function metaContains(data, metaName, valContains) { + switch (typeof data[metaName]) { + case "string": + return data[metaName].indexOf(valContains) >= 0; + + default: + return false; + } +} + +export class History { + /** + * constructor + * + * @param {Array} records + * @param {function} saver + * @param {number} maxItems + * + */ + constructor(records, saver, maxItems) { + this.records = records; + this.maxItems = maxItems; + this.saver = saver; + } + + /** + * Return the index of given uname, or -1 when not found + * + * @param {string} uname the unique name + * + * @returns {integer} The index of given uname + * + */ + indexOf(uname) { + for (let i in this.records) { + if (this.records[i].uname !== uname) { + continue; + } + + return i; + } + + return -1; + } + + /** + * Save record to history + * + * @param {string} uname unique name + * @param {string} title Title + * @param {command.Info} info Command info + * @param {Date} lastUsed Last used + * @param {object} data Data + * @param {object} sessionData Data which only available for current session + * @param {Array} keptSessions Keys of the session data that should + * be saved + * + */ + save(uname, title, lastUsed, info, data, sessionData, keptSessions) { + const unameIdx = this.indexOf(uname); + + if (unameIdx >= 0) { + this.records.splice(unameIdx, 1); + } + + this.records.push({ + uname: uname, + title: title, + type: info.name(), + color: info.color(), + last: lastUsed.getTime(), + data: data, + session: sessionData, + keptSessions: keptSessions, + }); + + if (this.records.length > this.maxItems) { + this.records = this.records.slice( + this.records.length - this.maxItems, + this.records.length, + ); + } + + this.store(); + } + + /** + * Save current records to storage + * + */ + store() { + this.saver(this, this.export()); + } + + /** + * Delete record from history + * + * @param {string} uid unique name + * + */ + del(uid) { + for (let i in this.records) { + if (this.records[i].uname !== uid) { + continue; + } + + this.records.splice(i, 1); + break; + } + + this.saver(this, this.records); + } + + /** + * Clear session data + * + * @param {string} uid unique name + * + */ + clearSession(uid) { + for (let i in this.records) { + if (this.records[i].uname !== uid) { + continue; + } + + this.records[i].session = null; + this.records[i].keptSessions = []; + break; + } + + this.store(); + } + + /** + * Return all history records. The exported data is differ than the + * internal ones, it cannot be directly import back + * + * @returns {Array} Records + * + */ + all() { + let r = []; + + for (let i in this.records) { + r.push({ + uid: this.records[i].uname, + title: this.records[i].title, + type: this.records[i].type, + color: this.records[i].color, + last: new Date(this.records[i].last), + data: this.records[i].data, + session: this.records[i].session, + keptSessions: this.records[i].keptSessions, + }); + } + + return r; + } + + /** + * Export current history records + * + * @returns {Array} Records + * + */ + export() { + let r = []; + + for (let i in this.records) { + r.push({ + uname: this.records[i].uname, + title: this.records[i].title, + type: this.records[i].type, + color: this.records[i].color, + last: this.records[i].last, + data: this.records[i].data, + session: extractSelectedData( + this.records[i].keptSessions, + this.records[i].session, + ), + keptSessions: this.records[i].keptSessions, + }); + } + + return r; + } + + /** + * Import data into current history records + * + * @param {Array} records Records + * + */ + import(records) { + for (let i in records) { + if (this.indexOf(records[i].uname) >= 0) { + continue; + } + + this.records.push({ + uname: records[i].uname, + title: records[i].title, + type: records[i].type, + color: records[i].color, + last: records[i].last, + data: records[i].data, + session: extractSelectedData( + records[i].keptSessions, + records[i].session, + ), + keptSessions: records[i].keptSessions, + }); + } + + this.store(); + } + + /** + * Search for partly matched results + * + * @param {string} type of the history record + * @param {string} metaName name of the meta data + * @param {string} keyword keyword to search + * @param {number} max max results + */ + search(type, metaName, keyword, max) { + let maxResults = max > this.records.length ? this.records.length : max; + let s = []; + + if (maxResults < 0) { + maxResults = this.records.length; + } + + for (let i = 0; i < this.records.length && s.length < maxResults; i++) { + if (this.records[i].type !== type) { + continue; + } + + if (!this.records[i].data) { + continue; + } + + if (!metaContains(this.records[i].data, metaName, keyword)) { + continue; + } + + s.push(this.records[i]); + } + + return s; + } +} diff --git a/ui/commands/integer.js b/ui/commands/integer.js new file mode 100644 index 0000000..77ac4e7 --- /dev/null +++ b/ui/commands/integer.js @@ -0,0 +1,90 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as reader from "../stream/reader.js"; +import Exception from "./exception.js"; + +export const MAX = 0x3fff; +export const MAX_BYTES = 2; + +const integerHasNextBit = 0x80; +const integerValueCutter = 0x7f; + +export class Integer { + /** + * constructor + * + * @param {number} num Integer number + * + */ + constructor(num) { + this.num = num; + } + + /** + * Marshal integer to buffer + * + * @returns {Uint8Array} Integer buffer + * + * @throws {Exception} When number is too large + * + */ + marshal() { + if (this.num > MAX) { + throw new Exception("Integer number cannot be greater than 0x3fff"); + } + + if (this.num <= integerValueCutter) { + return new Uint8Array([this.num & integerValueCutter]); + } + + return new Uint8Array([ + (this.num >> 7) | integerHasNextBit, + this.num & integerValueCutter, + ]); + } + + /** + * Parse the reader to build an Integer + * + * @param {reader.Reader} rd Data reader + * + */ + async unmarshal(rd) { + for (let i = 0; i < MAX_BYTES; i++) { + let r = await reader.readOne(rd); + + this.num |= r[0] & integerValueCutter; + + if ((integerHasNextBit & r[0]) == 0) { + return; + } + + this.num <<= 7; + } + } + + /** + * Return the value of the number + * + * @returns {number} The integer value + * + */ + value() { + return this.num; + } +} diff --git a/ui/commands/integer_test.js b/ui/commands/integer_test.js new file mode 100644 index 0000000..0f6dfcd --- /dev/null +++ b/ui/commands/integer_test.js @@ -0,0 +1,60 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import assert from "assert"; +import * as reader from "../stream/reader.js"; +import * as integer from "./integer.js"; + +describe("Integer", () => { + it("Integer 127", async () => { + let i = new integer.Integer(127), + marshalled = i.marshal(); + + let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return data; + }); + + assert.strictEqual(marshalled.length, 1); + + r.feed(marshalled); + + let i2 = new integer.Integer(0); + + await i2.unmarshal(r); + + assert.strictEqual(i.value(), i2.value()); + }); + + it("Integer MAX", async () => { + let i = new integer.Integer(integer.MAX), + marshalled = i.marshal(); + + let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return data; + }); + + assert.strictEqual(marshalled.length, 2); + + r.feed(marshalled); + + let i2 = new integer.Integer(0); + + await i2.unmarshal(r); + + assert.strictEqual(i.value(), i2.value()); + }); +}); diff --git a/ui/commands/presets.js b/ui/commands/presets.js new file mode 100644 index 0000000..1dc1ae2 --- /dev/null +++ b/ui/commands/presets.js @@ -0,0 +1,327 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import Exception from "./exception.js"; + +/** + * Default preset item, contains data of a default preset + * + */ +const presetItem = { + title: "", + type: "", + host: "", + meta: {}, +}; + +/** + * Verify Preset Item Meta + * + * @param {object} preset + * + */ +function verifyPresetItemMeta(preset) { + for (let i in preset.meta) { + if (typeof preset.meta[i] === "string") { + continue; + } + + throw new Exception( + 'The data type of meta field "' + + i + + '" was "' + + typeof preset.meta[i] + + '" instead of expected "string"', + ); + } +} + +/** + * Parse and verify the given preset, return a valid preset + * + * @param {object} item + * + * @throws {Exception} when invalid data is given + * + * @return {object} + * + */ +function parsePresetItem(item) { + let preset = {}; + + for (let i in presetItem) { + preset[i] = presetItem[i]; + } + + for (let i in presetItem) { + if (typeof presetItem[i] === typeof item[i]) { + preset[i] = item[i]; + + continue; + } + + throw new Exception( + 'Expecting the data type of "' + + i + + '" is "' + + typeof presetItem[i] + + '", given "' + + typeof item[i] + + '" instead', + ); + } + + verifyPresetItemMeta(preset.meta); + + return preset; +} + +/** + * Preset data + * + */ +export class Preset { + /** + * constructor + * + * @param {object} preset preset data + * + */ + constructor(preset) { + this.preset = parsePresetItem(preset); + } + + /** + * Return the title of the preset + * + * @returns {string} + * + */ + title() { + return this.preset.title; + } + + /** + * Return the type of the preset + * + * @returns {string} + * + */ + type() { + return this.preset.type; + } + + /** + * Return the host of the preset + * + * @returns {string} + * + */ + host() { + return this.preset.host; + } + + /** + * Return the given meta of current preset + * + * @param {string} name name of the meta data + * + * @throws {Exception} when invalid data is given + * + * @returns {string} + * + */ + meta(name) { + if (typeof this.preset.meta[name] !== "string") { + throw new Exception('Meta "' + name + '" was undefined'); + } + + return this.preset.meta[name]; + } + + /** + * Return the given meta of current preset, and if failed, return the given + * default value + * + * @param {string} name name of the meta data + * @param {string} defaultValue default value to be returned when the meta was + * not found + * + * @returns {string} + * + */ + metaDefault(name, defaultValue) { + try { + return this.meta(name); + } catch (e) { + return defaultValue; + } + } + + /** + * Insert new meta item + * + * @param {string} name name of the meta data + * @param {string} data data of the meta data + * + * @throws {Exception} when invalid data is given + * + */ + insertMeta(name, data) { + if (typeof this.preset.meta[name] !== "undefined") { + throw new Exception('Meta "' + name + '" has already been defined'); + } + + this.preset.meta[name] = data; + } + + /** + * Export all meta keys + * + * @returns {Array} All meta keys + * + */ + metaKeys() { + let keys = []; + + for (let k in this.preset.meta) { + keys.push(k); + } + + return keys; + } +} + +/** + * Returns an empty preset + * + * @returns {Preset} + * + */ +export function emptyPreset() { + return new Preset({ + title: "Default", + type: "Default", + host: "", + meta: {}, + }); +} + +/** + * Command Preset manager + * + */ +export class Presets { + /** + * constructor + * + * @param {Array} presets Array of preset data + * + */ + constructor(presets) { + this.presets = []; + + for (let i = 0; i < presets.length; i++) { + this.presets.push(new Preset(presets[i])); + } + } + + /** + * Return all presets of a type + * + * @param {string} type type of the presets data + * + * @returns {Array} + * + */ + fetch(type) { + let presets = []; + + for (let i = 0; i < this.presets.length; i++) { + if (this.presets[i].type() !== type) { + continue; + } + + presets.push(this.presets[i]); + } + + return presets; + } + + /** + * Return presets with matched type and meta data + * + * @param {string} type type of the presets data + * @param {string} metaName name of the meta data + * @param {string} metaVal value of the meta data + * + * @returns {Array} + * + */ + meta(type, metaName, metaVal) { + let presets = []; + + for (let i = 0; i < this.presets.length; i++) { + if (this.presets[i].type() !== type) { + continue; + } + + try { + if (this.presets[i].meta(metaName) !== metaVal) { + continue; + } + } catch (e) { + if (!(e instanceof Exception)) { + throw e; + } + + continue; + } + + presets.push(this.presets[i]); + } + + return presets; + } + + /** + * Return presets with matched type and host + * + * @param {string} type type of the presets + * @param {string} host host of the presets + * + * @returns {Array} + * + */ + hosts(type, host) { + let presets = []; + + for (let i = 0; i < this.presets.length; i++) { + if (this.presets[i].type() !== type) { + continue; + } + + if (this.presets[i].host() !== host) { + continue; + } + + presets.push(this.presets[i]); + } + + return presets; + } +} diff --git a/ui/commands/ssh.js b/ui/commands/ssh.js new file mode 100644 index 0000000..1401cee --- /dev/null +++ b/ui/commands/ssh.js @@ -0,0 +1,1124 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as header from "../stream/header.js"; +import * as reader from "../stream/reader.js"; +import * as stream from "../stream/stream.js"; +import * as address from "./address.js"; +import * as command from "./commands.js"; +import * as common from "./common.js"; +import * as controls from "./controls.js"; +import * as event from "./events.js"; +import Exception from "./exception.js"; +import * as history from "./history.js"; +import * as presets from "./presets.js"; +import * as strings from "./string.js"; + +const AUTHMETHOD_NONE = 0x00; +const AUTHMETHOD_PASSPHRASE = 0x01; +const AUTHMETHOD_PRIVATE_KEY = 0x02; + +const COMMAND_ID = 0x01; + +const MAX_USERNAME_LEN = 64; +const MAX_PASSWORD_LEN = 4096; +const DEFAULT_PORT = 22; + +const SERVER_REMOTE_STDOUT = 0x00; +const SERVER_REMOTE_STDERR = 0x01; +const SERVER_CONNECT_FAILED = 0x02; +const SERVER_CONNECTED = 0x03; +const SERVER_CONNECT_REQUEST_FINGERPRINT = 0x04; +const SERVER_CONNECT_REQUEST_CREDENTIAL = 0x05; + +const CLIENT_DATA_STDIN = 0x00; +const CLIENT_DATA_RESIZE = 0x01; +const CLIENT_CONNECT_RESPOND_FINGERPRINT = 0x02; +const CLIENT_CONNECT_RESPOND_CREDENTIAL = 0x03; + +const SERVER_REQUEST_ERROR_BAD_USERNAME = 0x01; +const SERVER_REQUEST_ERROR_BAD_ADDRESS = 0x02; +const SERVER_REQUEST_ERROR_BAD_AUTHMETHOD = 0x03; + +const FingerprintPromptVerifyPassed = 0x00; +const FingerprintPromptVerifyNoRecord = 0x01; +const FingerprintPromptVerifyMismatch = 0x02; + +const HostMaxSearchResults = 3; + +class SSH { + /** + * constructor + * + * @param {stream.Sender} sd Stream sender + * @param {object} config configuration + * @param {object} callbacks Event callbacks + * + */ + constructor(sd, config, callbacks) { + this.sender = sd; + this.config = config; + this.connected = false; + this.events = new event.Events( + [ + "initialization.failed", + "initialized", + "connect.failed", + "connect.succeed", + "connect.fingerprint", + "connect.credential", + "@stdout", + "@stderr", + "close", + "@completed", + ], + callbacks, + ); + } + + /** + * Send intial request + * + * @param {stream.InitialSender} initialSender Initial stream request sender + * + */ + run(initialSender) { + let user = new strings.String(this.config.user), + userBuf = user.buffer(), + addr = new address.Address( + this.config.host.type, + this.config.host.address, + this.config.host.port, + ), + addrBuf = addr.buffer(), + authMethod = new Uint8Array([this.config.auth]); + + let data = new Uint8Array(userBuf.length + addrBuf.length + 1); + + data.set(userBuf, 0); + data.set(addrBuf, userBuf.length); + data.set(authMethod, userBuf.length + addrBuf.length); + + initialSender.send(data); + } + + /** + * Receive the initial stream request + * + * @param {header.InitialStream} streamInitialHeader Server respond on the + * initial stream request + * + */ + initialize(streamInitialHeader) { + if (!streamInitialHeader.success()) { + this.events.fire("initialization.failed", streamInitialHeader); + + return; + } + + this.events.fire("initialized", streamInitialHeader); + } + + /** + * Tick the command + * + * @param {header.Stream} streamHeader Stream data header + * @param {reader.Limited} rd Data reader + * + * @returns {any} The result of the ticking + * + * @throws {Exception} When the stream header type is unknown + * + */ + tick(streamHeader, rd) { + switch (streamHeader.marker()) { + case SERVER_CONNECTED: + if (!this.connected) { + this.connected = true; + + return this.events.fire("connect.succeed", rd, this); + } + break; + + case SERVER_CONNECT_FAILED: + if (!this.connected) { + return this.events.fire("connect.failed", rd); + } + break; + + case SERVER_CONNECT_REQUEST_FINGERPRINT: + if (!this.connected) { + return this.events.fire("connect.fingerprint", rd, this.sender); + } + break; + + case SERVER_CONNECT_REQUEST_CREDENTIAL: + if (!this.connected) { + return this.events.fire("connect.credential", rd, this.sender); + } + break; + + case SERVER_REMOTE_STDOUT: + if (this.connected) { + return this.events.fire("stdout", rd); + } + break; + + case SERVER_REMOTE_STDERR: + if (this.connected) { + return this.events.fire("stderr", rd); + } + break; + } + + throw new Exception("Unknown stream header marker"); + } + + /** + * Send close signal to remote + * + */ + async sendClose() { + return await this.sender.close(); + } + + /** + * Send data to remote + * + * @param {Uint8Array} data + * + */ + async sendData(data) { + return this.sender.sendData(CLIENT_DATA_STDIN, data); + } + + /** + * Send resize request + * + * @param {number} rows + * @param {number} cols + * + */ + async sendResize(rows, cols) { + let data = new DataView(new ArrayBuffer(4)); + + data.setUint16(0, rows); + data.setUint16(2, cols); + + return this.sender.send(CLIENT_DATA_RESIZE, new Uint8Array(data.buffer)); + } + + /** + * Close the command + * + */ + async close() { + await this.sendClose(); + + return this.events.fire("close"); + } + + /** + * Tear down the command completely + * + */ + completed() { + return this.events.fire("completed"); + } +} + +const initialFieldDef = { + Host: { + name: "Host", + description: "", + type: "text", + value: "", + example: "ssh.vaguly.com:22", + readonly: false, + suggestions(input) { + return []; + }, + verify(d) { + if (d.length <= 0) { + throw new Error("Hostname must be specified"); + } + + let addr = common.splitHostPort(d, DEFAULT_PORT); + + if (addr.addr.length <= 0) { + throw new Error("Cannot be empty"); + } + + if (addr.addr.length > address.MAX_ADDR_LEN) { + throw new Error( + "Can no longer than " + address.MAX_ADDR_LEN + " bytes", + ); + } + + if (addr.port <= 0) { + throw new Error("Port must be specified"); + } + + return "Look like " + addr.type + " address"; + }, + }, + User: { + name: "User", + description: "", + type: "text", + value: "", + example: "guest", + readonly: false, + suggestions(input) { + return []; + }, + verify(d) { + if (d.length <= 0) { + throw new Error("Username must be specified"); + } + + if (d.length > MAX_USERNAME_LEN) { + throw new Error( + "Username must not longer than " + MAX_USERNAME_LEN + " bytes", + ); + } + + return "We'll login as user \"" + d + '"'; + }, + }, + Encoding: { + name: "Encoding", + description: "The character encoding of the server", + type: "select", + value: "utf-8", + example: common.charsetPresets.join(","), + readonly: false, + suggestions(input) { + return []; + }, + verify(d) { + for (let i in common.charsetPresets) { + if (common.charsetPresets[i] !== d) { + continue; + } + + return ""; + } + + throw new Error('The character encoding "' + d + '" is not supported'); + }, + }, + Notice: { + name: "Notice", + description: "", + type: "textdata", + value: + "SSH session is handled by the backend. Traffic will be decrypted " + + "on the backend server and then transmit back to your client.", + example: "", + readonly: false, + suggestions(input) { + return []; + }, + verify(d) { + return ""; + }, + }, + Password: { + name: "Password", + description: "", + type: "password", + value: "", + example: "----------", + readonly: false, + suggestions(input) { + return []; + }, + verify(d) { + if (d.length <= 0) { + throw new Error("Password must be specified"); + } + + if (d.length > MAX_PASSWORD_LEN) { + throw new Error( + "It's too long, make it shorter than " + MAX_PASSWORD_LEN + " bytes", + ); + } + + return "We'll login with this password"; + }, + }, + "Private Key": { + name: "Private Key", + description: + 'Like the one inside ' + + "~/.ssh/id_rsa, can't be encrypted

" + + 'To decrypt the Private Key, use command: ssh-keygen -f /path/to/private_key -p
' + + "
" + + "It is strongly recommended to use one Private Key per SSH server if " + + "the Private Key will be submitted to Sshwifty. To generate a new SSH " + + 'key pair, use command ' + + "ssh-keygen -o -f /path/to/my_server_key and then deploy the " + + 'generated ' + + "/path/to/my_server_key.pub file onto the target SSH server", + type: "textfile", + value: "", + example: "", + readonly: false, + suggestions(input) { + return []; + }, + verify(d) { + if (d.length <= 0) { + throw new Error("Private Key must be specified"); + } + + if (d.length > MAX_PASSWORD_LEN) { + throw new Error( + "It's too long, make it shorter than " + MAX_PASSWORD_LEN + " bytes", + ); + } + + const lines = d.trim().split("\n"); + let firstLineReaded = false; + + for (let i in lines) { + if (!firstLineReaded) { + if (lines[i].indexOf("-") === 0) { + firstLineReaded = true; + + if (lines[i].indexOf("RSA") <= 0) { + break; + } + } + + continue; + } + + if (lines[i].indexOf("Proc-Type: 4,ENCRYPTED") === 0) { + throw new Error("Cannot use encrypted Private Key file"); + } + + if (lines[i].indexOf(":") > 0) { + continue; + } + + if (lines[i].indexOf("MII") < 0) { + throw new Error("Cannot use encrypted Private Key file"); + } + + break; + } + + return "We'll login with this Private Key"; + }, + }, + Authentication: { + name: "Authentication", + description: + "Please make sure the authentication method that you selected is " + + "supported by the server, otherwise it will be ignored and likely " + + "cause the login to fail", + type: "radio", + value: "", + example: "Password,Private Key,None", + readonly: false, + suggestions(input) { + return []; + }, + verify(d) { + switch (d) { + case "Password": + case "Private Key": + case "None": + return ""; + + default: + throw new Error("Authentication method must be specified"); + } + }, + }, + Fingerprint: { + name: "Fingerprint", + description: + "Please carefully verify the fingerprint. DO NOT continue " + + "if the fingerprint is unknown to you, otherwise you maybe " + + "giving your own secrets to an imposter", + type: "textdata", + value: "", + example: "", + readonly: false, + suggestions(input) { + return []; + }, + verify(d) { + return ""; + }, + }, +}; + +/** + * Return auth method from given string + * + * @param {string} d string data + * + * @returns {number} Auth method + * + * @throws {Exception} When auth method is invalid + * + */ +function getAuthMethodFromStr(d) { + switch (d) { + case "None": + return AUTHMETHOD_NONE; + + case "Password": + return AUTHMETHOD_PASSPHRASE; + + case "Private Key": + return AUTHMETHOD_PRIVATE_KEY; + + default: + throw new Exception("Unknown Auth method"); + } +} + +class Wizard { + /** + * constructor + * + * @param {command.Info} info + * @param {presets.Preset} preset + * @param {object} session + * @param {Array} keptSessions + * @param {streams.Streams} streams + * @param {subscribe.Subscribe} subs + * @param {controls.Controls} controls + * @param {history.History} history + * + */ + constructor( + info, + preset, + session, + keptSessions, + streams, + subs, + controls, + history, + ) { + this.info = info; + this.preset = preset; + this.hasStarted = false; + this.streams = streams; + this.session = session + ? session + : { + credential: "", + }; + this.keptSessions = keptSessions; + this.step = subs; + this.controls = controls.get("SSH"); + this.history = history; + } + + run() { + this.step.resolve(this.stepInitialPrompt()); + } + + started() { + return this.hasStarted; + } + + control() { + return this.controls; + } + + close() { + this.step.resolve( + this.stepErrorDone( + "Action cancelled", + "Action has been cancelled without reach any success", + ), + ); + } + + stepErrorDone(title, message) { + return command.done(false, null, title, message); + } + + stepSuccessfulDone(data) { + return command.done( + true, + data, + "Success!", + "We have connected to the remote", + ); + } + + stepWaitForAcceptWait() { + return command.wait( + "Requesting", + "Waiting for the request to be accepted by the backend", + ); + } + + stepWaitForEstablishWait(host) { + return command.wait( + "Connecting to " + host, + "Establishing connection with the remote host, may take a while", + ); + } + + stepContinueWaitForEstablishWait() { + return command.wait( + "Connecting", + "Establishing connection with the remote host, may take a while", + ); + } + + /** + * + * @param {stream.Sender} sender + * @param {object} configInput + * @param {object} sessionData + * + */ + buildCommand(sender, configInput, sessionData) { + let self = this; + + let config = { + user: common.strToUint8Array(configInput.user), + auth: getAuthMethodFromStr(configInput.authentication), + charset: configInput.charset, + credential: sessionData.credential, + host: address.parseHostPort(configInput.host, DEFAULT_PORT), + fingerprint: configInput.fingerprint, + }; + + // Copy the keptSessions from the record so it will not be overwritten here + let keptSessions = self.keptSessions ? [].concat(...self.keptSessions) : []; + + return new SSH(sender, config, { + "initialization.failed"(hd) { + switch (hd.data()) { + case SERVER_REQUEST_ERROR_BAD_USERNAME: + self.step.resolve( + self.stepErrorDone("Request failed", "Invalid username"), + ); + return; + + case SERVER_REQUEST_ERROR_BAD_ADDRESS: + self.step.resolve( + self.stepErrorDone("Request failed", "Invalid address"), + ); + return; + + case SERVER_REQUEST_ERROR_BAD_AUTHMETHOD: + self.step.resolve( + self.stepErrorDone( + "Request failed", + "Invalid authication method", + ), + ); + return; + } + + self.step.resolve( + self.stepErrorDone("Request failed", "Unknown error: " + hd.data()), + ); + }, + initialized(hd) { + self.step.resolve(self.stepWaitForEstablishWait(configInput.host)); + }, + async "connect.failed"(rd) { + let d = new TextDecoder("utf-8").decode( + await reader.readCompletely(rd), + ); + + self.step.resolve(self.stepErrorDone("Connection failed", d)); + }, + "connect.succeed"(rd, commandHandler) { + self.connectionSucceed = true; + + self.step.resolve( + self.stepSuccessfulDone( + new command.Result( + configInput.user + "@" + configInput.host, + self.info, + self.controls.build({ + charset: configInput.charset, + send(data) { + return commandHandler.sendData(data); + }, + close() { + return commandHandler.sendClose(); + }, + resize(rows, cols) { + return commandHandler.sendResize(rows, cols); + }, + events: commandHandler.events, + }), + self.controls.ui(), + ), + ), + ); + + self.history.save( + self.info.name() + ":" + configInput.user + "@" + configInput.host, + configInput.user + "@" + configInput.host, + new Date(), + self.info, + configInput, + sessionData, + keptSessions, + ); + }, + async "connect.fingerprint"(rd, sd) { + self.step.resolve( + await self.stepFingerprintPrompt( + rd, + sd, + (v) => { + if (!configInput.fingerprint) { + return FingerprintPromptVerifyNoRecord; + } + + if (configInput.fingerprint === v) { + return FingerprintPromptVerifyPassed; + } + + return FingerprintPromptVerifyMismatch; + }, + (newFingerprint) => { + configInput.fingerprint = newFingerprint; + }, + ), + ); + }, + async "connect.credential"(rd, sd) { + self.step.resolve( + self.stepCredentialPrompt(rd, sd, config, (newCred, fromPreset) => { + sessionData.credential = newCred; + + // Save the credential if the credential was from a preset + if (fromPreset && keptSessions.indexOf("credential") < 0) { + keptSessions.push("credential"); + } + }), + ); + }, + "@stdout"(rd) {}, + "@stderr"(rd) {}, + close() {}, + "@completed"() { + self.step.resolve( + self.stepErrorDone( + "Operation has failed", + "Connection has been cancelled", + ), + ); + }, + }); + } + + stepInitialPrompt() { + let self = this; + + return command.prompt( + "SSH", + "Secure Shell Host", + "Connect", + (r) => { + self.hasStarted = true; + + self.streams.request(COMMAND_ID, (sd) => { + return self.buildCommand( + sd, + { + user: r.user, + authentication: r.authentication, + host: r.host, + charset: r.encoding, + fingerprint: self.preset + ? self.preset.metaDefault("Fingerprint", "") + : "", + }, + self.session, + ); + }); + + self.step.resolve(self.stepWaitForAcceptWait()); + }, + () => {}, + command.fieldsWithPreset( + initialFieldDef, + [ + { + name: "Host", + suggestions(input) { + const hosts = self.history.search( + "SSH", + "host", + input, + HostMaxSearchResults, + ); + + let sugg = []; + + for (let i = 0; i < hosts.length; i++) { + sugg.push({ + title: hosts[i].title, + value: hosts[i].data.host, + meta: { + User: hosts[i].data.user, + Authentication: hosts[i].data.authentication, + Encoding: hosts[i].data.charset, + }, + }); + } + + return sugg; + }, + }, + { name: "User" }, + { name: "Authentication" }, + { name: "Encoding" }, + { name: "Notice" }, + ], + self.preset, + (r) => {}, + ), + ); + } + + async stepFingerprintPrompt(rd, sd, verify, newFingerprint) { + const self = this; + + let fingerprintData = new TextDecoder("utf-8").decode( + await reader.readCompletely(rd), + ), + fingerprintChanged = false; + + switch (verify(fingerprintData)) { + case FingerprintPromptVerifyPassed: + sd.send(CLIENT_CONNECT_RESPOND_FINGERPRINT, new Uint8Array([0])); + + return self.stepContinueWaitForEstablishWait(); + + case FingerprintPromptVerifyMismatch: + fingerprintChanged = true; + } + + return command.prompt( + !fingerprintChanged + ? "Do you recognize this server?" + : "Danger! Server fingerprint has changed!", + !fingerprintChanged + ? "Verify server fingerprint displayed below" + : "It's very unusual. Please verify the new server fingerprint below", + !fingerprintChanged ? "Yes, I do" : "I'm aware of the change", + (r) => { + newFingerprint(fingerprintData); + + sd.send(CLIENT_CONNECT_RESPOND_FINGERPRINT, new Uint8Array([0])); + + self.step.resolve(self.stepContinueWaitForEstablishWait()); + }, + () => { + sd.send(CLIENT_CONNECT_RESPOND_FINGERPRINT, new Uint8Array([1])); + + self.step.resolve( + command.wait("Rejecting", "Sending rejection to the backend"), + ); + }, + command.fields(initialFieldDef, [ + { + name: "Fingerprint", + value: fingerprintData, + }, + ]), + ); + } + + async stepCredentialPrompt(rd, sd, config, newCredential) { + const self = this; + + let fields = []; + + if (config.credential.length > 0) { + sd.send( + CLIENT_CONNECT_RESPOND_CREDENTIAL, + new TextEncoder().encode(config.credential), + ); + + return self.stepContinueWaitForEstablishWait(); + } + + switch (config.auth) { + case AUTHMETHOD_PASSPHRASE: + fields = [{ name: "Password" }]; + break; + + case AUTHMETHOD_PRIVATE_KEY: + fields = [{ name: "Private Key" }]; + break; + + default: + throw new Exception( + 'Auth method "' + config.auth + '" was unsupported', + ); + } + + let presetCredentialUsed = false; + const inputFields = command.fieldsWithPreset( + initialFieldDef, + fields, + self.preset, + (r) => { + if (r !== fields[0].name) { + return; + } + + presetCredentialUsed = true; + }, + ); + + return command.prompt( + "Provide credential", + "Please input your credential", + "Login", + (r) => { + let vv = r[fields[0].name.toLowerCase()]; + + sd.send( + CLIENT_CONNECT_RESPOND_CREDENTIAL, + new TextEncoder().encode(vv), + ); + + newCredential(vv, presetCredentialUsed); + + self.step.resolve(self.stepContinueWaitForEstablishWait()); + }, + () => { + sd.close(); + + self.step.resolve( + command.wait( + "Cancelling login", + "Cancelling login request, please wait", + ), + ); + }, + inputFields, + ); + } +} + +class Executer extends Wizard { + /** + * constructor + * + * @param {command.Info} info + * @param {config} config + * @param {object} session + * @param {Array} keptSessions + * @param {streams.Streams} streams + * @param {subscribe.Subscribe} subs + * @param {controls.Controls} controls + * @param {history.History} history + * + */ + constructor( + info, + config, + session, + keptSessions, + streams, + subs, + controls, + history, + ) { + super( + info, + presets.emptyPreset(), + session, + keptSessions, + streams, + subs, + controls, + history, + ); + + this.config = config; + } + + stepInitialPrompt() { + const self = this; + + self.hasStarted = true; + + self.streams.request(COMMAND_ID, (sd) => { + return self.buildCommand( + sd, + { + user: self.config.user, + authentication: self.config.authentication, + host: self.config.host, + charset: self.config.charset ? self.config.charset : "utf-8", + fingerprint: self.config.fingerprint, + }, + self.session, + ); + }); + + return self.stepWaitForAcceptWait(); + } +} + +export class Command { + constructor() {} + + id() { + return COMMAND_ID; + } + + name() { + return "SSH"; + } + + description() { + return "Secure Shell Host"; + } + + color() { + return "#3c8"; + } + + wizard( + info, + preset, + session, + keptSessions, + streams, + subs, + controls, + history, + ) { + return new Wizard( + info, + preset, + session, + keptSessions, + streams, + subs, + controls, + history, + ); + } + + execute( + info, + config, + session, + keptSessions, + streams, + subs, + controls, + history, + ) { + return new Executer( + info, + config, + session, + keptSessions, + streams, + subs, + controls, + history, + ); + } + + launch(info, launcher, streams, subs, controls, history) { + const d = launcher.split("|", 3); + + if (d.length < 2) { + throw new Exception('Given launcher "' + launcher + '" was invalid'); + } + + const userHostName = d[0].match(new RegExp("^(.*)\\@(.*)$")); + + if (!userHostName || userHostName.length !== 3) { + throw new Exception('Given launcher "' + launcher + '" was malformed'); + } + + let user = userHostName[1], + host = userHostName[2], + auth = d[1], + charset = d.length >= 3 && d[2] ? d[2] : "utf-8"; // RM after depreciation + + try { + initialFieldDef["User"].verify(user); + initialFieldDef["Host"].verify(host); + initialFieldDef["Authentication"].verify(auth); + initialFieldDef["Encoding"].verify(charset); + } catch (e) { + throw new Exception( + 'Given launcher "' + launcher + '" was malformed ' + e, + ); + } + + return this.execute( + info, + { + user: user, + host: host, + authentication: auth, + charset: charset, + }, + null, + null, + streams, + subs, + controls, + history, + ); + } + + launcher(config) { + return ( + config.user + + "@" + + config.host + + "|" + + config.authentication + + "|" + + (config.charset ? config.charset : "utf-8") + ); + } + + represet(preset) { + const host = preset.host(); + + if (host.length > 0) { + preset.insertMeta("Host", host); + } + + return preset; + } +} diff --git a/ui/commands/string.js b/ui/commands/string.js new file mode 100644 index 0000000..f2e4992 --- /dev/null +++ b/ui/commands/string.js @@ -0,0 +1,72 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as reader from "../stream/reader.js"; +import * as integer from "./integer.js"; + +export class String { + /** + * Read String from given reader + * + * @param {reader.Reader} rd Source reader + * + * @returns {String} readed string + * + */ + static async read(rd) { + let l = new integer.Integer(0); + + await l.unmarshal(rd); + + return new String(await reader.readN(rd, l.value())); + } + + /** + * constructor + * + * @param {Uint8Array} str String data + */ + constructor(str) { + this.str = str; + } + + /** + * Return the string + * + * @returns {Uint8Array} String data + * + */ + data() { + return this.str; + } + + /** + * Return serialized String as array + * + * @returns {Uint8Array} serialized String + * + */ + buffer() { + let lBytes = new integer.Integer(this.str.length).marshal(), + buf = new Uint8Array(lBytes.length + this.str.length); + + buf.set(lBytes, 0); + buf.set(this.str, lBytes.length); + + return buf; + } +} diff --git a/ui/commands/string_test.js b/ui/commands/string_test.js new file mode 100644 index 0000000..b2ac97e --- /dev/null +++ b/ui/commands/string_test.js @@ -0,0 +1,265 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import assert from "assert"; +import * as reader from "../stream/reader.js"; +import * as strings from "./string.js"; + +describe("String", () => { + it("String 1", async () => { + let s = new strings.String(new Uint8Array(["H", "E", "L", "L", "O"])), + sBuf = s.buffer(); + + let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return data; + }); + + r.feed(sBuf); + + let s2 = await strings.String.read(r); + + assert.deepStrictEqual(s2.data(), s.data()); + }); + + it("String 2", async () => { + let s = new strings.String( + new Uint8Array([ + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + "H", + "E", + "L", + "L", + "O", + "W", + "O", + "R", + "L", + "D", + ]), + ), + sBuf = s.buffer(); + + let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return data; + }); + + r.feed(sBuf); + + let s2 = await strings.String.read(r); + + assert.deepStrictEqual(s2.data(), s.data()); + }); +}); diff --git a/ui/commands/telnet.js b/ui/commands/telnet.js new file mode 100644 index 0000000..7980e03 --- /dev/null +++ b/ui/commands/telnet.js @@ -0,0 +1,648 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as header from "../stream/header.js"; +import * as reader from "../stream/reader.js"; +import * as stream from "../stream/stream.js"; +import * as address from "./address.js"; +import * as command from "./commands.js"; +import * as common from "./common.js"; +import * as controls from "./controls.js"; +import * as event from "./events.js"; +import Exception from "./exception.js"; +import * as history from "./history.js"; +import * as presets from "./presets.js"; + +const COMMAND_ID = 0x00; + +const SERVER_INITIAL_ERROR_BAD_ADDRESS = 0x01; + +const SERVER_REMOTE_BAND = 0x00; +const SERVER_DIAL_FAILED = 0x01; +const SERVER_DIAL_CONNECTED = 0x02; + +const DEFAULT_PORT = 23; + +const HostMaxSearchResults = 3; + +class Telnet { + /** + * constructor + * + * @param {stream.Sender} sd Stream sender + * @param {object} config configuration + * @param {object} callbacks Event callbacks + * + */ + constructor(sd, config, callbacks) { + this.sender = sd; + this.config = config; + this.connected = false; + this.events = new event.Events( + [ + "initialization.failed", + "initialized", + "connect.failed", + "connect.succeed", + "@inband", + "close", + "@completed", + ], + callbacks, + ); + } + + /** + * Send intial request + * + * @param {stream.InitialSender} initialSender Initial stream request sender + * + */ + run(initialSender) { + let addr = new address.Address( + this.config.host.type, + this.config.host.address, + this.config.host.port, + ), + addrBuf = addr.buffer(); + + let data = new Uint8Array(addrBuf.length); + + data.set(addrBuf, 0); + + initialSender.send(data); + } + + /** + * Receive the initial stream request + * + * @param {header.InitialStream} streamInitialHeader Server respond on the + * initial stream request + * + */ + initialize(streamInitialHeader) { + if (!streamInitialHeader.success()) { + this.events.fire("initialization.failed", streamInitialHeader); + + return; + } + + this.events.fire("initialized", streamInitialHeader); + } + + /** + * Tick the command + * + * @param {header.Stream} streamHeader Stream data header + * @param {reader.Limited} rd Data reader + * + * @returns {any} The result of the ticking + * + * @throws {Exception} When the stream header type is unknown + * + */ + tick(streamHeader, rd) { + switch (streamHeader.marker()) { + case SERVER_DIAL_CONNECTED: + if (!this.connected) { + this.connected = true; + + return this.events.fire("connect.succeed", rd, this); + } + break; + + case SERVER_DIAL_FAILED: + if (!this.connected) { + return this.events.fire("connect.failed", rd); + } + break; + + case SERVER_REMOTE_BAND: + if (this.connected) { + return this.events.fire("inband", rd); + } + break; + } + + throw new Exception("Unknown stream header marker"); + } + + /** + * Send close signal to remote + * + */ + sendClose() { + return this.sender.close(); + } + + /** + * Send data to remote + * + * @param {Uint8Array} data + * + */ + sendData(data) { + return this.sender.sendData(0x00, data); + } + + /** + * Close the command + * + */ + close() { + this.sendClose(); + + return this.events.fire("close"); + } + + /** + * Tear down the command completely + * + */ + completed() { + return this.events.fire("completed"); + } +} + +const initialFieldDef = { + Host: { + name: "Host", + description: + "Looking for server to connect? Checkout " + + '' + + "telnet.org for public servers.", + type: "text", + value: "", + example: "telnet.vaguly.com:23", + readonly: false, + suggestions(input) { + return []; + }, + verify(d) { + if (d.length <= 0) { + throw new Error("Hostname must be specified"); + } + + let addr = common.splitHostPort(d, DEFAULT_PORT); + + if (addr.addr.length <= 0) { + throw new Error("Cannot be empty"); + } + + if (addr.addr.length > address.MAX_ADDR_LEN) { + throw new Error( + "Can no longer than " + address.MAX_ADDR_LEN + " bytes", + ); + } + + if (addr.port <= 0) { + throw new Error("Port must be specified"); + } + + return "Look like " + addr.type + " address"; + }, + }, + Encoding: { + name: "Encoding", + description: "The character encoding of the server", + type: "select", + value: "utf-8", + example: common.charsetPresets.join(","), + readonly: false, + suggestions(input) { + return []; + }, + verify(d) { + for (let i in common.charsetPresets) { + if (common.charsetPresets[i] !== d) { + continue; + } + + return ""; + } + + throw new Error('The character encoding "' + d + '" is not supported'); + }, + }, +}; + +class Wizard { + /** + * constructor + * + * @param {command.Info} info + * @param {presets.Preset} preset + * @param {object} session + * @param {Array} keptSessions + * @param {streams.Streams} streams + * @param {subscribe.Subscribe} subs + * @param {controls.Controls} controls + * @param {history.History} history + * + */ + constructor( + info, + preset, + session, + keptSessions, + streams, + subs, + controls, + history, + ) { + this.info = info; + this.preset = preset; + this.hasStarted = false; + this.streams = streams; + this.session = session; + this.keptSessions = keptSessions; + this.step = subs; + this.controls = controls.get("Telnet"); + this.history = history; + } + + run() { + this.step.resolve(this.stepInitialPrompt()); + } + + started() { + return this.hasStarted; + } + + control() { + return this.controls; + } + + close() { + this.step.resolve( + this.stepErrorDone( + "Action cancelled", + "Action has been cancelled without reach any success", + ), + ); + } + + stepErrorDone(title, message) { + return command.done(false, null, title, message); + } + + stepSuccessfulDone(data) { + return command.done( + true, + data, + "Success!", + "We have connected to the remote", + ); + } + + stepWaitForAcceptWait() { + return command.wait( + "Requesting", + "Waiting for the request to be accepted by the backend", + ); + } + + stepWaitForEstablishWait(host) { + return command.wait( + "Connecting to " + host, + "Establishing connection with the remote host, may take a while", + ); + } + + /** + * + * @param {stream.Sender} sender + * @param {object} configInput + * @param {object} sessionData + * + */ + buildCommand(sender, configInput, sessionData) { + let self = this; + + let parsedConfig = { + host: address.parseHostPort(configInput.host, DEFAULT_PORT), + charset: configInput.charset, + }; + + // Copy the keptSessions from the record so it will not be overwritten here + let keptSessions = self.keptSessions ? [].concat(...self.keptSessions) : []; + + return new Telnet(sender, parsedConfig, { + "initialization.failed"(streamInitialHeader) { + switch (streamInitialHeader.data()) { + case SERVER_INITIAL_ERROR_BAD_ADDRESS: + self.step.resolve( + self.stepErrorDone("Request rejected", "Invalid address"), + ); + + return; + } + + self.step.resolve( + self.stepErrorDone( + "Request rejected", + "Unknown error code: " + streamInitialHeader.data(), + ), + ); + }, + initialized(streamInitialHeader) { + self.step.resolve(self.stepWaitForEstablishWait(configInput.host)); + }, + "connect.succeed"(rd, commandHandler) { + self.step.resolve( + self.stepSuccessfulDone( + new command.Result( + configInput.host, + self.info, + self.controls.build({ + charset: parsedConfig.charset, + send(data) { + return commandHandler.sendData(data); + }, + close() { + return commandHandler.sendClose(); + }, + events: commandHandler.events, + }), + self.controls.ui(), + ), + ), + ); + + self.history.save( + self.info.name() + ":" + configInput.host, + configInput.host, + new Date(), + self.info, + configInput, + sessionData, + keptSessions, + ); + }, + async "connect.failed"(rd) { + let readed = await reader.readCompletely(rd), + message = new TextDecoder("utf-8").decode(readed.buffer); + + self.step.resolve(self.stepErrorDone("Connection failed", message)); + }, + "@inband"(rd) {}, + close() {}, + "@completed"() {}, + }); + } + + stepInitialPrompt() { + const self = this; + + return command.prompt( + "Telnet", + "Teletype Network", + "Connect", + (r) => { + self.hasStarted = true; + + self.streams.request(COMMAND_ID, (sd) => { + return self.buildCommand( + sd, + { + host: r.host, + charset: r.encoding, + }, + self.session, + ); + }); + + self.step.resolve(self.stepWaitForAcceptWait()); + }, + () => {}, + command.fieldsWithPreset( + initialFieldDef, + [ + { + name: "Host", + suggestions(input) { + const hosts = self.history.search( + "Telnet", + "host", + input, + HostMaxSearchResults, + ); + + let sugg = []; + + for (let i = 0; i < hosts.length; i++) { + sugg.push({ + title: hosts[i].title, + value: hosts[i].data.host, + meta: { + Encoding: hosts[i].data.charset, + }, + }); + } + + return sugg; + }, + }, + { name: "Encoding" }, + ], + self.preset, + (r) => {}, + ), + ); + } +} + +class Executor extends Wizard { + /** + * constructor + * + * @param {command.Info} info + * @param {object} config + * @param {object} session + * @param {Array} keptSessions + * @param {streams.Streams} streams + * @param {subscribe.Subscribe} subs + * @param {controls.Controls} controls + * @param {history.History} history + * + */ + constructor( + info, + config, + session, + keptSessions, + streams, + subs, + controls, + history, + ) { + super( + info, + presets.emptyPreset(), + session, + keptSessions, + streams, + subs, + controls, + history, + ); + + this.config = config; + } + + stepInitialPrompt() { + const self = this; + + self.hasStarted = true; + + self.streams.request(COMMAND_ID, (sd) => { + return self.buildCommand( + sd, + { + host: self.config.host, + charset: self.config.charset ? self.config.charset : "utf-8", + }, + self.session, + ); + }); + + return self.stepWaitForAcceptWait(); + } +} + +export class Command { + constructor() {} + + id() { + return COMMAND_ID; + } + + name() { + return "Telnet"; + } + + description() { + return "Teletype Network"; + } + + color() { + return "#6ac"; + } + + wizard( + info, + preset, + session, + keptSessions, + streams, + subs, + controls, + history, + ) { + return new Wizard( + info, + preset, + session, + keptSessions, + streams, + subs, + controls, + history, + ); + } + + execute( + info, + config, + session, + keptSessions, + streams, + subs, + controls, + history, + ) { + return new Executor( + info, + config, + session, + keptSessions, + streams, + subs, + controls, + history, + ); + } + + launch(info, launcher, streams, subs, controls, history) { + const d = launcher.split("|", 2); + + if (d.length <= 0) { + throw new Exception('Given launcher "' + launcher + '" was invalid'); + } + + try { + initialFieldDef["Host"].verify(d[0]); + } catch (e) { + throw new Exception( + 'Given launcher "' + launcher + '" was invalid: ' + e, + ); + } + + let charset = "utf-8"; + + if (d.length > 1) { + // TODO: Remove this check after depreciation period. + try { + initialFieldDef["Encoding"].verify(d[1]); + + charset = d[1]; + } catch (e) { + throw new Exception( + 'Given launcher "' + launcher + '" was invalid: ' + e, + ); + } + } + + return this.execute( + info, + { + host: d[0], + charset: charset, + }, + null, + null, + streams, + subs, + controls, + history, + ); + } + + launcher(config) { + return config.host + "|" + (config.charset ? config.charset : "utf-8"); + } + + represet(preset) { + const host = preset.host(); + + if (host.length > 0) { + preset.insertMeta("Host", host); + } + + return preset; + } +} diff --git a/ui/common.css b/ui/common.css new file mode 100644 index 0000000..38fe36d --- /dev/null +++ b/ui/common.css @@ -0,0 +1,694 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "UTF-8"; + +@import "~normalize.css"; + +html, +body { + height: 100%; +} + +* { + margin: 0; + padding: 0; +} + +p { + overflow: hidden; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + padding: 0; +} + +a { + outline: 0; +} + +body { + line-height: 1.5; +} + +body { + background: #444; + font-family: Arial, Helvetica, sans-serif; + font-size: 1em; + position: relative; +} + +/* Tabs */ +.tab1 { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-items: center; + list-style: none; + list-style-position: inside; + margin: 0; + padding: 0; + height: 100%; +} + +.tab1 > li { + padding: 0 15px; + color: #999; + white-space: nowrap; + word-wrap: none; + text-overflow: ellipsis; + overflow: hidden; + flex: initial; + display: flex; + flex-direction: column; + justify-content: center; + cursor: pointer; +} + +.tab1 > li.active { + background: #444; +} + +.tab1.tab1-list { + flex-direction: column; + flex-wrap: wrap; +} + +.tab1.tab1-list > li { + flex: 0 0; + margin: 0; + padding: 0; + width: 100%; + box-sizing: border-box; +} + +.tab2 { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-items: center; + list-style: none; + list-style-position: inside; + margin: 0; + height: 100%; + color: #aaa; + border-bottom: 1px solid #a56; + background: #333; + padding: 0 10px; + position: relative; +} + +.tab2::before { + content: " "; + display: block; + position: absolute; + width: 100%; + height: 1px; + left: 0; + right: 0; + bottom: 0; + box-shadow: 0 -3px 3px #0003; +} + +.tab2 > li { + flex: auto; + cursor: pointer; + border-color: transparent; + border-width: 1px 1px 0 1px; + border-style: solid; + padding: 7px 10px; + text-align: center; + position: relative; + z-index: 1; +} + +.tab2 > li.active { + color: #fff; + background: #644; + margin-bottom: -1px; + border-color: #a56; + border-style: solid; + box-shadow: 0 -2px 2px #0002; +} + +/* List */ +.lst-nostyle { + list-style: none; + list-style-position: inside; +} + +.hlst { +} + +.hlst > li { + float: left; +} + +.lst1 { + margin: 0; + padding: 0; + width: 100%; +} + +.lst1 > li { + border-bottom: 1px solid #555; +} + +.lst1 > li:last-child { + border-bottom: none; +} + +.lst1 > li .lst-wrap { + padding: 10px; +} + +.hlst.lstcl1 { + list-style: none; + list-style-position: inside; + margin: 0; + padding: 0; + width: 100%; + overflow: auto; +} + +.hlst.lstcl1 > li { + width: 33%; + white-space: nowrap; +} + +.hlst.lstcl1 > li .lst-wrap { + padding: 10px; + margin: 10px; + background: #333; + text-overflow: ellipsis; + overflow: hidden; + box-shadow: 2px 2px 0 0 #222; +} + +.hlst.lstcl1 > li .lst-wrap:hover { + background: #3a3a3a; + box-shadow: 2px 2px 0 0 #222; +} + +.hlst.lstcl1 > li .lst-wrap:active { + background: #333; +} + +.hlst.lstcl2 { + list-style: none; + list-style-position: inside; + margin: 0; + padding: 0; + width: 100%; + overflow: auto; +} + +.hlst.lstcl2 > li { + width: 33%; + white-space: nowrap; +} + +.hlst.lstcl2 > li .lst-wrap { + padding: 10px; + margin: 5px; + background: #333; + text-overflow: ellipsis; + overflow: hidden; +} + +/* Icon */ +.icon { + line-height: 1; + overflow: visible; +} + +.icon.icon-close1 { + margin-top: -2.5px; + font-size: 26px; + line-height: 0; +} + +.icon.icon-close1::before { + content: "\00d7"; + font-weight: bold; + display: block; + margin: 0; + padding: 6px; +} + +.icon.icon-plus1 { + color: #fff; + background: #a56; +} + +.icon.icon-plus1::before { + content: "+"; + font-weight: bold; +} + +.icon.icon-more1 { + color: #fff; + background: #222; +} + +.icon.icon-more1::before { + content: "\2261"; + font-weight: bold; +} + +.icon.icon-warning1 { + font-size: 20px; +} + +.icon.icon-warning1::after { + content: "!"; + font-weight: bold; + background: #e11; + padding: 3px 13px; +} + +.icon.icon-point1 { + position: relative; + text-shadow: 0 0 3px #fff; +} + +.icon.icon-point1::after { + content: "\25CF"; +} + +.icon.icon-keyboardkey1 { + background: #fff; + color: #999; + padding: 4px 6px; + display: inline-block; + border-radius: 3px; + box-shadow: 1px 1px 0 2px #0003; +} + +.icon.icon-iconed-bottom1 { + padding: 4px 6px; + display: inline-block; + border-radius: 3px; + text-align: center; +} +.icon.icon-iconed-bottom1 > i { + font-style: normal; + display: block; + margin: 3px; + font-size: 2em; + font-weight: normal; +} + +/* Windows */ +.window { + position: absolute; +} + +.window.window1 { + background: #a56; + box-shadow: 0 0 5px #0006; + color: #fff; + font-size: 1em; + display: none; +} + +.window.window1.display { + display: block; +} + +.window.window1::before { + top: -5px; + position: absolute; + display: block; + content: " "; + width: 10px; + height: 10px; + background: #a56; + transform: rotate(45deg); +} + +.window.window1 .window-frame { + width: 100%; + overflow: auto; + position: relative; +} + +.window.window1 .window-title { + font-size: 0.9em; + font-weight: bold; + text-transform: uppercase; + color: #e9a; +} + +.window.window1 .window-close { + position: absolute; + top: 10px; + right: 10px; + border-color: #844; + cursor: pointer; +} +.window.window1 .window-close::after { + border-color: #844; +} + +/* Form1 */ +.form1 { +} + +.form1 > fieldset, +.form1 > fieldset * { + padding: 0; + margin: 0; + color: #fff; + outline: none; + border: 0; + font-size: 1em; +} + +.form1 > fieldset .field { + color: #ccc; + display: block; + width: 100%; + overflow: auto; +} + +.form1 > fieldset .field.horizontal { + width: auto; + float: left; + margin-right: 10px; +} + +.form1 > fieldset .items { + width: 100%; + overflow: auto; +} + +.form1 > fieldset .field.horizontal.item { + margin-top: 3px; + margin-bottom: 3px; +} + +.form1 > fieldset .field.horizontal:last-child { + margin-right: 0; +} + +.form1 > fieldset .field, +.form1 > fieldset .field input, +.form1 > fieldset .field select, +.form1 > fieldset .field textarea, +.form1 > fieldset .field button { + vertical-align: middle; + font-size: 1.05em; +} + +.form1 > fieldset .field { + font-size: 0.95em; + margin-bottom: 10px; +} + +.form1 > fieldset .field > .textinfo { + color: #fff; +} + +.form1 > fieldset .field > .textinfo > .info { + padding: 10px; + margin: 10px 0; + font-size: 1.1em; + background: #292929; + border: 1px solid #444; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; +} + +.form1 > fieldset .field:last-child { + margin-bottom: 0; +} + +.form1 > fieldset .field:last-child { + margin-bottom: 0; +} + +.form1 > fieldset .field > input, +.form1 > fieldset .field > select, +.form1 > fieldset .field > textarea, +.form1 > fieldset .field > button { + box-sizing: border-box; + resize: none; +} + +.form1 > fieldset .field > input::placeholder { + color: #666; +} + +.form1 > fieldset .field > input:focus::placeholder { + color: #444; +} + +.form1 > fieldset .field > input[type="text"], +.form1 > fieldset .field > input[type="file"], +.form1 > fieldset .field > input[type="email"], +.form1 > fieldset .field > input[type="number"], +.form1 > fieldset .field > input[type="search"], +.form1 > fieldset .field > input[type="tel"], +.form1 > fieldset .field > input[type="url"], +.form1 > fieldset .field > input[type="password"], +.form1 > fieldset .field > select, +.form1 > fieldset .field > textarea { + width: 100%; + padding: 10px; + border: 0; + background: #2e2e2e; + margin-top: 5px; + border-bottom: 2px solid #3e3e3e; +} + +.form1 > fieldset .field > textarea { + min-height: 120px; +} + +.form1 > fieldset .field.error > .error { + margin-top: 5px; + color: #f55; +} + +.form1 > fieldset .field > .message { + margin-top: 5px; + color: #999; +} + +.form1 > fieldset .field > .message * { + color: #999; +} + +.form1 > fieldset .field > .message > p { + margin-bottom: 5px; +} + +.form1 > fieldset .field > .message > a { + color: #e9a; +} + +.form1 > fieldset .field.highlight > input[type="text"], +.form1 > fieldset .field.highlight > input[type="file"], +.form1 > fieldset .field.highlight > input[type="email"], +.form1 > fieldset .field.highlight > input[type="number"], +.form1 > fieldset .field.highlight > input[type="search"], +.form1 > fieldset .field.highlight > input[type="tel"], +.form1 > fieldset .field.highlight > input[type="url"], +.form1 > fieldset .field.highlight > input[type="password"], +.form1 > fieldset .field.highlight > select, +.form1 > fieldset .field.highlight > textarea { + background: #666; + border-bottom: 2px solid #ccc; +} + +.form1 > fieldset .field.error > input[type="text"], +.form1 > fieldset .field.error > input[type="file"], +.form1 > fieldset .field.error > input[type="email"], +.form1 > fieldset .field.error > input[type="number"], +.form1 > fieldset .field.error > input[type="search"], +.form1 > fieldset .field.error > input[type="tel"], +.form1 > fieldset .field.error > input[type="url"], +.form1 > fieldset .field.error > input[type="password"], +.form1 > fieldset .field.error > select, +.form1 > fieldset .field.error > textarea { + background: #483535; + border-bottom: 2px solid #a83333; +} + +.form1 > fieldset .field > input:disabled, +.form1 > fieldset .field > select:disabled, +.form1 > fieldset .field > textarea:disabled, +.form1 > fieldset .field > button:disabled { + opacity: 0.35; +} + +.form1 > fieldset .field > input:disabled:active, +.form1 > fieldset .field > select:disabled:active, +.form1 > fieldset .field > textarea:disabled:active, +.form1 > fieldset .field > button:disabled:active { + opacity: 0.5; +} + +.form1 > fieldset .field > input[type="checkbox"], +.form1 > fieldset .field > input[type="radio"] { + background: #2e2e2e; + margin: 1px 3px 1px 1px; +} + +.form1 > fieldset .field > input[type="checkbox"]:active, +.form1 > fieldset .field > input[type="radio"]:active, +.form1 > fieldset .field > input[type="checkbox"]:focus, +.form1 > fieldset .field > input[type="radio"]:focus { + outline: 1px solid #e9a; +} + +.form1 > fieldset .field > input[type="text"]:focus, +.form1 > fieldset .field > input[type="email"]:focus, +.form1 > fieldset .field > input[type="number"]:focus, +.form1 > fieldset .field > input[type="search"]:focus, +.form1 > fieldset .field > input[type="tel"]:focus, +.form1 > fieldset .field > input[type="url"]:focus, +.form1 > fieldset .field > input[type="password"]:focus, +.form1 > fieldset .field > select:focus, +.form1 > fieldset .field > textarea:focus { + background: #222; + border-bottom: 2px solid #e9a; +} + +.form1 > fieldset .field > button { + padding: 8px 13px; + font-weight: normal; + background: #c78; + border-color: #c78; + border-width: 2px; + border-style: solid; +} + +.form1 > fieldset .field > button:focus, +.form1 > fieldset .field > button:hover { + border-color: #a56; + background: #c78; +} + +.form1 > fieldset .field > button:active { + background: #a56; + border-color: #a56; +} + +.form1 > fieldset .field > button.secondary { + float: right; + background: transparent; + color: #eee; + border-color: #eee; +} + +.form1 > fieldset .field > button.secondary:focus, +.form1 > fieldset .field > button.secondary:hover { + border-color: #ddd; + color: #ddd; +} + +.form1 > fieldset .field > button.secondary:active { + border-color: #666; + color: #666; +} + +.form1 > fieldset .field > ul.input-suggestions { + background: #262626; + box-shadow: 0 0 3px #0006; + border: 1px solid #666; + position: relative; + margin: 3px; +} + +.form1 > fieldset .field > ul.input-suggestions::before, +.form1 > fieldset .field > ul.input-suggestions::after { + top: -5px; + left: 5px; + position: absolute; + z-index: 1; + display: block; + content: " "; + width: 10px; + height: 10px; + background: #262626; + transform: rotate(45deg); +} + +.form1 > fieldset .field > ul.input-suggestions::after { + top: -6px; + z-index: 0; + background: #666; +} + +.form1 > fieldset .field > ul.input-suggestions > li { + position: relative; + z-index: 2; + padding: 10px; + cursor: pointer; + border-bottom: 1px solid #222; +} + +.form1 > fieldset .field > ul.input-suggestions > li:hover, +.form1 > fieldset .field > ul.input-suggestions > li.current { + background: #555; + border-bottom: 1px solid #555; +} + +.form1 > fieldset .field > ul.input-suggestions > li:first-child:hover::before, +.form1 + > fieldset + .field + > ul.input-suggestions + > li.current:first-child::before { + top: -6px; + position: absolute; + z-index: 0; + left: 5px; + display: block; + content: " "; + width: 10px; + height: 10px; + background: #555; + transform: rotate(45deg); +} + +.form1 > fieldset .field > ul.input-suggestions > li:last-child { + border-bottom: none; +} + +.form1 > fieldset .field > ul.input-suggestions > li > .sugt-title { + color: #fff; + font-weight: bold; + margin: 0 5px; +} + +.form1 > fieldset .field > ul.input-suggestions > li > .sugt-value { + color: #fdd; + font-size: 0.9em; + margin: 0 5px; +} diff --git a/ui/control/ssh.js b/ui/control/ssh.js new file mode 100644 index 0000000..4586387 --- /dev/null +++ b/ui/control/ssh.js @@ -0,0 +1,152 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as iconv from "iconv-lite"; +import * as color from "../commands/color.js"; +import * as common from "../commands/common.js"; +import * as reader from "../stream/reader.js"; +import * as subscribe from "../stream/subscribe.js"; + +class Control { + constructor(data, color) { + this.colorM = color; + this.colors = this.colorM.get(); + this.charset = data.charset; + + this.charsetDecoder = (d) => { + return iconv.decode(d, this.charset); + }; + this.charsetEncoder = (dStr) => { + return iconv.encode(dStr, this.charset); + }; + + this.enable = false; + this.sender = data.send; + this.closer = data.close; + this.resizer = data.resize; + this.subs = new subscribe.Subscribe(); + + let self = this; + + data.events.place("stdout", async (rd) => { + try { + self.subs.resolve(self.charsetDecoder(await reader.readCompletely(rd))); + } catch (e) { + // Do nothing + } + }); + + data.events.place("stderr", async (rd) => { + try { + self.subs.resolve(self.charsetDecoder(await reader.readCompletely(rd))); + } catch (e) { + // Do nothing + } + }); + + data.events.place("completed", () => { + self.closed = true; + self.colorM.forget(self.colors.color); + + self.subs.reject("Remote connection has been terminated"); + }); + } + + echo() { + return false; + } + + resize(dim) { + if (this.closed) { + return; + } + + this.resizer(dim.rows, dim.cols); + } + + enabled() { + this.enable = true; + } + + disabled() { + this.enable = false; + } + + retap(isOn) {} + + receive() { + return this.subs.subscribe(); + } + + send(data) { + if (this.closed) { + return; + } + + return this.sender(this.charsetEncoder(data)); + } + + sendBinary(data) { + if (this.closed) { + return; + } + + return this.sender(common.strToBinary(data)); + } + + color() { + return this.colors.dark; + } + + activeColor() { + return this.colors.color; + } + + close() { + if (this.closer === null) { + return; + } + + let cc = this.closer; + this.closer = null; + + return cc(); + } +} + +export class SSH { + /** + * constructor + * + * @param {color.Color} c + */ + constructor(c) { + this.color = c; + } + + type() { + return "SSH"; + } + + ui() { + return "Console"; + } + + build(data) { + return new Control(data, this.color); + } +} diff --git a/ui/control/telnet.js b/ui/control/telnet.js new file mode 100644 index 0000000..546cde5 --- /dev/null +++ b/ui/control/telnet.js @@ -0,0 +1,513 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as iconv from "iconv-lite"; +import * as color from "../commands/color.js"; +import * as common from "../commands/common.js"; +import Exception from "../commands/exception.js"; +import * as reader from "../stream/reader.js"; +import * as subscribe from "../stream/subscribe.js"; + +// const maxReadBufSize = 1024; + +const cmdSE = 240; +// const cmdNOP = 241; +// const cmdDataMark = 242; +// const cmdBreak = 243; +// const cmdInterrputProcess = 244; +// const cmdAbortOutput = 245; +// const cmdAreYouThere = 246; +// const cmdEraseCharacter = 247; +// const cmdEraseLine = 248; +const cmdGoAhead = 249; +const cmdSB = 250; +const cmdWill = 251; +const cmdWont = 252; +const cmdDo = 253; +const cmdDont = 254; +const cmdIAC = 255; + +const optEcho = 1; +const optSuppressGoAhead = 3; +const optTerminalType = 24; +const optNAWS = 31; + +const optTerminalTypeIs = 0; +const optTerminalTypeSend = 1; + +const unknownTermTypeSendData = new Uint8Array([ + optTerminalTypeIs, + 88, + 84, + 69, + 82, + 77, +]); + +// Most of code of this class is directly from +// https://github.com/ziutek/telnet/blob/master/conn.go#L122 +// Thank you! +class Parser { + constructor(sender, flusher, callbacks) { + this.sender = sender; + this.flusher = flusher; + this.callbacks = callbacks; + this.reader = new reader.Multiple(() => {}); + this.options = { + echoEnabled: false, + suppressGoAhead: false, + nawsAccpeted: false, + }; + this.current = 0; + } + + sendNego(cmd, option) { + return this.sender(new Uint8Array([cmdIAC, cmd, option])); + } + + sendDeny(cmd, o) { + switch (cmd) { + case cmdDo: + return this.sendNego(cmdWont, o); + + case (cmdWill, cmdWont): + return this.sendNego(cmdDont, o); + } + } + + sendWillSubNego(willCmd, data, option) { + let b = new Uint8Array(6 + data.length + 2); + + b.set([cmdIAC, willCmd, option, cmdIAC, cmdSB, option], 0); + b.set(data, 6); + b.set([cmdIAC, cmdSE], data.length + 6); + + return this.sender(b); + } + + sendSubNego(data, option) { + let b = new Uint8Array(3 + data.length + 2); + + b.set([cmdIAC, cmdSB, option], 0); + b.set(data, 3); + b.set([cmdIAC, cmdSE], data.length + 3); + + return this.sender(b); + } + + async handleTermTypeSubNego(rd) { + let action = await reader.readOne(rd); + + if (action[0] !== optTerminalTypeSend) { + return null; + } + + let self = this; + + return () => { + self.sendSubNego(unknownTermTypeSendData, optTerminalType); + }; + } + + async handleSubNego(rd) { + let endExec = null; + + for (;;) { + let d = await reader.readOne(rd); + + switch (d[0]) { + case optTerminalType: + endExec = await this.handleTermTypeSubNego(rd); + continue; + + case cmdIAC: + break; + + default: + continue; + } + + let e = await reader.readOne(rd); + + if (e[0] !== cmdSE) { + continue; + } + + if (endExec !== null) { + endExec(); + } + + return; + } + } + + handleOption(cmd, option, oldVal, newVal) { + switch (cmd) { + case cmdWill: + if (!oldVal) { + this.sendNego(cmdDo, option); + } + + newVal(true, cmdWill); + return; + + case cmdWont: + if (oldVal) { + this.sendNego(cmdDont, option); + } + + newVal(false, cmdWont); + return; + + case cmdDo: + if (!oldVal) { + this.sendNego(cmdWill, option); + } + + newVal(true, cmdDo); + return; + + case cmdDont: + if (oldVal) { + this.sendNego(cmdWont, option); + } + + newVal(false, cmdDont); + return; + } + } + + async handleCmd(rd) { + let d = await reader.readOne(rd); + + switch (d[0]) { + case cmdWill: + case cmdWont: + case cmdDo: + case cmdDont: + break; + + case cmdIAC: + this.flusher(d); + return; + + case cmdGoAhead: + return; + + case cmdSB: + await this.handleSubNego(rd); + return; + + default: + throw new Exception("Unknown command"); + } + + let o = await reader.readOne(rd); + + switch (o[0]) { + case optEcho: + return this.handleOption( + d[0], + o[0], + this.options.echoEnabled, + (d, action) => { + this.options.echoEnabled = d; + + switch (action) { + case cmdWill: + case cmdDont: + this.callbacks.setEcho(false); + break; + + case cmdWont: + case cmdDo: + this.callbacks.setEcho(true); + break; + } + }, + ); + + case optSuppressGoAhead: + return this.handleOption( + d[0], + o[0], + this.options.suppressGoAhead, + (d, _action) => { + this.options.suppressGoAhead = d; + }, + ); + + case optNAWS: + // Window resize allowed? + if (d[0] !== cmdDo) { + this.sendDeny(d[0], o[0]); + + return; + } + + { + let dim = this.callbacks.getWindowDim(), + dimData = new DataView(new ArrayBuffer(4)); + + dimData.setUint16(0, dim.cols); + dimData.setUint16(2, dim.rows); + + let dimBytes = new Uint8Array(dimData.buffer); + + if (this.options.nawsAccpeted) { + this.sendSubNego(dimBytes, optNAWS); + + return; + } + + this.options.nawsAccpeted = true; + this.sendWillSubNego(cmdWill, dimBytes, optNAWS); + } + return; + + case optTerminalType: + if (d[0] !== cmdDo) { + this.sendDeny(d[0], o[0]); + + return; + } + + this.sendNego(cmdWill, o[0]); + return; + } + + this.sendDeny(d[0], o[0]); + } + + requestWindowResize() { + this.options.nawsAccpeted = true; + + this.sendNego(cmdWill, optNAWS); + } + + async run() { + try { + for (;;) { + let d = await reader.readUntil(this.reader, cmdIAC); + + if (!d.found) { + this.flusher(d.data); + + continue; + } + + if (d.data.length > 1) { + this.flusher(d.data.slice(0, d.data.length - 1)); + } + + await this.handleCmd(this.reader); + } + } catch (e) { + // Do nothing + } + } + + feed(rd, cb) { + this.reader.feed(rd, cb); + } + + close() { + this.reader.close(); + } +} + +class Control { + constructor(data, color) { + this.colorM = color; + this.colors = this.colorM.get(); + this.charset = data.charset; + + this.charsetDecoder = (d) => { + return iconv.decode(d, this.charset); + }; + this.charsetEncoder = (dStr) => { + return iconv.encode(dStr, this.charset); + }; + + this.sender = data.send; + this.closer = data.close; + this.closed = false; + this.localEchoEnabled = true; + this.subs = new subscribe.Subscribe(); + this.enable = false; + this.windowDim = { + cols: 65535, + rows: 65535, + }; + + let self = this; + + this.parser = new Parser( + this.sender, + (d) => { + self.subs.resolve(this.charsetDecoder(d)); + }, + { + setEcho(newVal) { + self.localEchoEnabled = newVal; + }, + getWindowDim() { + return self.windowDim; + }, + }, + ); + + let runWait = this.parser.run(); + + data.events.place("inband", (rd) => { + return new Promise((resolve, _reject) => { + self.parser.feed(rd, () => { + resolve(true); + }); + }); + }); + + data.events.place("completed", async () => { + self.parser.close(); + self.closed = true; + + self.colorM.forget(self.colors.color); + + await runWait; + + self.subs.reject("Remote connection has been terminated"); + }); + } + + echo() { + return this.localEchoEnabled; + } + + resize(dim) { + if (this.closed) { + return; + } + + this.windowDim.cols = dim.cols; + this.windowDim.rows = dim.rows; + + this.parser.requestWindowResize(); + } + + enabled() { + this.enable = true; + } + + disabled() { + this.enable = false; + } + + retap(_isOn) {} + + receive() { + return this.subs.subscribe(); + } + + searchNextIAC(start, data) { + for (let i = start; i < data.length; i++) { + if (data[i] !== cmdIAC) { + continue; + } + + return i; + } + + return -1; + } + + sendSeg(enc) { + let currentLen = 0; + + while (currentLen < enc.length) { + const iacPos = this.searchNextIAC(currentLen, enc); + + if (iacPos < 0) { + this.sender(enc.slice(currentLen, enc.length)); + + return; + } + + this.sender(enc.slice(currentLen, iacPos + 1)); + this.sender(enc.slice(iacPos, iacPos + 1)); + + currentLen = iacPos + 1; + } + } + + send(data) { + if (this.closed) { + return; + } + + this.sendSeg(this.charsetEncoder(data)); + } + + sendBinary(data) { + if (this.closed) { + return; + } + + return this.sendSeg(common.strToBinary(data)); + } + + color() { + return this.colors.dark; + } + + activeColor() { + return this.colors.color; + } + + close() { + if (this.closer === null) { + return; + } + + let cc = this.closer; + this.closer = null; + + return cc(); + } +} + +export class Telnet { + /** + * constructor + * + * @param {color.Color} c + */ + constructor(c) { + this.color = c; + } + + type() { + return "Telnet"; + } + + ui() { + return "Console"; + } + + build(data) { + return new Control(data, this.color); + } +} diff --git a/ui/crypto.js b/ui/crypto.js new file mode 100644 index 0000000..33beed3 --- /dev/null +++ b/ui/crypto.js @@ -0,0 +1,118 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/** + * Generate HMAC 512 of given data + * + * @param {Uint8Array} secret Secret key + * @param {Uint8Array} data Data to be HMAC'ed + */ +export async function hmac512(secret, data) { + const key = await window.crypto.subtle.importKey( + "raw", + secret, + { + name: "HMAC", + hash: { name: "SHA-512" }, + }, + false, + ["sign", "verify"], + ); + + return window.crypto.subtle.sign(key.algorithm, key, data); +} + +export const GCMNonceSize = 12; +export const GCMKeyBitLen = 128; + +/** + * Build AES GCM Encryption/Decryption key + * + * @param {Uint8Array} keyData Key data + */ +export function buildGCMKey(keyData) { + return window.crypto.subtle.importKey( + "raw", + keyData, + { + name: "AES-GCM", + length: GCMKeyBitLen, + }, + false, + ["encrypt", "decrypt"], + ); +} + +/** + * Encrypt data + * + * @param {CryptoKey} key Key + * @param {Uint8Array} iv Nonce + * @param {Uint8Array} plaintext Data to be encrypted + */ +export function encryptGCM(key, iv, plaintext) { + return window.crypto.subtle.encrypt( + { name: "AES-GCM", iv: iv, tagLength: GCMKeyBitLen }, + key, + plaintext, + ); +} + +/** + * Decrypt data + * + * @param {CryptoKey} key Key + * @param {Uint8Array} iv Nonce + * @param {Uint8Array} cipherText Data to be decrypted + */ +export function decryptGCM(key, iv, cipherText) { + return window.crypto.subtle.decrypt( + { name: "AES-GCM", iv: iv, tagLength: GCMKeyBitLen }, + key, + cipherText, + ); +} + +/** + * generate Random nonce + * + */ +export function generateNonce() { + return window.crypto.getRandomValues(new Uint8Array(GCMNonceSize)); +} + +/** + * Increase nonce by one + * + * @param {Uint8Array} nonce Nonce data + * + * @returns {Uint8Array} New nonce + * + */ +export function increaseNonce(nonce) { + for (let i = nonce.length; i > 0; i--) { + nonce[i - 1]++; + + if (nonce[i - 1] <= 0) { + continue; + } + + break; + } + + return nonce; +} diff --git a/ui/error.html b/ui/error.html new file mode 100644 index 0000000..feb902e --- /dev/null +++ b/ui/error.html @@ -0,0 +1,37 @@ + + + + + + Error + + + +
+
+
×
+ +

+ Server was unable to complete the request +

+
+
+ + diff --git a/ui/history.js b/ui/history.js new file mode 100644 index 0000000..8e268d8 --- /dev/null +++ b/ui/history.js @@ -0,0 +1,54 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +export class Records { + /** + * constructor + * + * @param {array} data Data space + */ + constructor(data) { + this.data = data; + } + + /** + * Insert new item into the history records + * + * @param {number} newData New value + */ + update(newData) { + this.data.shift(); + this.data.push({ data: newData, class: "" }); + } + + /** + * Set all existing data as expired + */ + expire() { + for (let i = 0; i < this.data.length; i++) { + this.data[i].class = "expired"; + } + } + + /** + * Return data + * + */ + get() { + return this.data; + } +} diff --git a/ui/home.css b/ui/home.css new file mode 100644 index 0000000..b99cd47 --- /dev/null +++ b/ui/home.css @@ -0,0 +1,422 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "utf-8"; + +@import "~roboto-fontface/css/roboto/roboto-fontface.css"; + +@keyframes home-window-display-flash { + 0% { + top: -2px; + opacity: 0; + box-shadow: 0 0 0 transparent; + } + + 20% { + height: 20px; + box-shadow: 0 0 50px #fff; + } + + 50% { + opacity: 0.5; + box-shadow: 0 0 10px #fff; + } + + 100% { + top: 100%; + height: 0; + opacity: 0; + box-shadow: 0 0 0 transparent; + } +} + +.home-window-display { +} + +.home-window-display::after { + opacity: 0; + z-index: 2; + content: " "; + display: block; + position: absolute; + width: 100%; + height: 0; + top: -2px; + left: 0; + right: 0; + background: #fff; + animation-name: home-window-display-flash; + animation-duration: 0.3s; + animation-iteration-count: 1; + box-shadow: 0 0 10px #fff; +} + +#home { + height: 100%; + display: flex; + flex-direction: column; + font: 1em "Roboto", sans-serif; +} + +#home-header { + flex: 0 0 40px; + font-size: 0.9em; + color: #fff; + width: 100%; + overflow: auto; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +#home-hd-title { + font-size: 1.1em; + padding: 0 0 0 20px; + font-weight: bold; + flex: 0 0 65px; + text-align: center; + overflow: hidden; +} + +#home-hd-delay { + font-size: 0.95em; + display: flex; + flex: 0 0 70px; + flex-direction: row; + overflow: hidden; + margin: 0 10px; + color: #aaa; + text-decoration: none; + justify-items: center; + justify-content: center; + align-items: center; +} + +@media (max-width: 768px) { + #home-hd-title { + padding: 0 0 0 10px; + } + + #home-hd-delay { + flex: 0 0 60px; + } +} + +#home-hd-delay-icon { + color: #bbb; + text-shadow: 0 0 3px #999; + transition: linear 0.2s color, text-shadow; + margin: 5px; + font-size: 0.54em; +} + +#home-hd-delay-value { + display: inline-block; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + word-wrap: none; +} + +@keyframes home-hd-delay-icon-flash { + 0% { + opacity: 1; + } + + 10% { + opacity: 0; + } + + 20% { + opacity: 1; + } + + 30% { + opacity: 0; + } + + 90% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes home-hd-delay-icon-working { + 0% { + opacity: 1; + } + + 50% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +#home-hd-delay-icon.green { + color: #1e8; + text-shadow: 0 0 3px #1e8; +} + +#home-hd-delay-icon.yellow { + color: #ff4; + text-shadow: 0 0 3px #ff4; +} + +#home-hd-delay-icon.orange { + color: #f80; + text-shadow: 0 0 3px #f80; +} + +#home-hd-delay-icon.red { + color: #e11; + text-shadow: 0 0 3px #e11; +} + +#home-hd-delay-icon.flash { + animation-name: home-hd-delay-icon-flash; + animation-duration: 1s; + animation-iteration-count: infinite; +} + +#home-hd-delay-icon.working { + animation-name: home-hd-delay-icon-working; + animation-duration: 1.5s; + animation-iteration-count: infinite; +} + +#home-hd-plus { + flex: 0 0; + padding: 0 13px; + text-decoration: none; + font-size: 22px; + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; +} + +@keyframes home-hd-plus-icon-flash { + 0% { + background: #a56; + } + + 20% { + background: #5a7; + } + + 40% { + background: #96a; + } + + 60% { + background: #379; + } + + 80% { + background: #da0; + } + + 100% { + background: #a56; + } +} + +#home-hd-plus.working { + color: #fff; + background: #a56; + animation-name: home-hd-plus-icon-flash; + animation-duration: 10s; + animation-iteration-count: infinite; + animation-direction: normal; + transition: linear 2s background; +} + +#home-hd-plus.working.intensify { + animation-duration: 3s; +} + +#home-hd-tabs { + background: #333; + flex: auto; + overflow: hidden; + height: 100%; + display: flex; + flex-direction: row; + justify-content: left; + align-items: center; +} + +#home-hd-tabs-tabs { + flex: auto; + overflow: hidden; +} + +#home-hd-tabs-tabs > li { + flex: 0 0 180px; + display: flex; + position: relative; + padding: 0 15px; + opacity: 0.5; + color: #999; +} + +#home-hd-tabs-tabs > li::after { + content: " "; + display: block; + position: absolute; + bottom: 0; + right: 10px; + left: 10px; + height: 0; + transition: all 0.1s linear; + transition-property: height, right, left; +} + +#home-hd-tabs-tabs > li.active::after { + right: 0; + left: 0; +} + +#home-hd-tabs-tabs > li.updated::after { + background: #fff3; + height: 2px; +} + +#home-hd-tabs-tabs > li.error::after { + background: #d55; + height: 2px; +} + +#home-hd-tabs-tabs > li > span.title { + text-overflow: ellipsis; + overflow: hidden; + display: inline-block; +} + +#home-hd-tabs-tabs > li > span.title > span.type { + display: inline-block; + font-size: 0.85em; + font-weight: bold; + margin-right: 3px; + text-transform: uppercase; + color: #fff; + background: #222; + padding: 1px 4px; + border-radius: 2px; +} + +#home-hd-tabs-tabs > li > .icon-close { + display: none; +} + +#home-hd-tabs-tabs > li.active { + color: #fff; + opacity: 1; +} + +#home-hd-tabs-tabs > li.active > span.title { + padding-right: 15px; +} + +#home-hd-tabs-tabs > li.active > .icon-close { + display: block; + position: absolute; + top: 50%; + right: 10px; + margin-top: -5px; + color: #fff6; +} + +#home-hd-tabs-list { + display: flex; + font-size: 22px; + flex: 0 0; + padding: 0 13px; + flex-direction: column; + justify-content: center; + height: 100%; + text-decoration: none; + box-shadow: 0 0 3px #333; +} + +#home-content { + flex: auto; + display: flex; + justify-content: center; + flex-direction: column; + color: #fff; + font-size: 1.2em; +} + +@media (max-width: 768px) { + #home-content { + font-size: 1em; + } +} + +#home-content-wrap { + max-width: 520px; + margin: 50px auto; + padding: 0 30px; + text-align: center; +} + +#home-content h1 { + margin: 20px 0; +} + +#home-content p { + margin: 10px 0; + font-size: 0.9em; + color: #eee; + line-height: 1.6; +} + +#home-content p.secondary { + margin: 5px 0; + line-height: 1.5; + font-size: 0.7em; + color: #aaa; +} + +#home-content p a { + color: #e9a; +} + +#home-content hr { + height: 2px; + background: #3c3c3c; + border: none; + margin: 30px 0; +} + +#home-content-connect { + padding: 5px; + display: inline-block; + width: 20px; + height: 20px; + cursor: pointer; +} diff --git a/ui/home.vue b/ui/home.vue new file mode 100644 index 0000000..fad5751 --- /dev/null +++ b/ui/home.vue @@ -0,0 +1,616 @@ + + + + + diff --git a/ui/home_historyctl.js b/ui/home_historyctl.js new file mode 100644 index 0000000..f72aa42 --- /dev/null +++ b/ui/home_historyctl.js @@ -0,0 +1,58 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { History } from "./commands/history.js"; + +export function build(ctx) { + let rec = []; + + // This renames "knowns" to "sshwifty-knowns" + // TODO: Remove this after some few years + try { + let oldStore = localStorage.getItem("knowns"); + + if (oldStore) { + localStorage.setItem("sshwifty-knowns", oldStore); + localStorage.removeItem("knowns"); + } + } catch (e) { + // Do nothing + } + + try { + rec = JSON.parse(localStorage.getItem("sshwifty-knowns")); + + if (!rec) { + rec = []; + } + } catch (e) { + alert("Unable to load data of Known remotes: " + e); + } + + return new History( + rec, + (h, d) => { + try { + localStorage.setItem("sshwifty-knowns", JSON.stringify(d)); + ctx.connector.knowns = h.all(); + } catch (e) { + alert("Unable to save remote history due to error: " + e); + } + }, + 64, + ); +} diff --git a/ui/home_socketctl.js b/ui/home_socketctl.js new file mode 100644 index 0000000..a7cb883 --- /dev/null +++ b/ui/home_socketctl.js @@ -0,0 +1,223 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as history from "./history.js"; +import { ECHO_FAILED } from "./socket.js"; + +export function build(ctx) { + const connectionStatusNotConnected = "Sshwifty is ready to connect"; + const connectionStatusConnecting = + "Connecting to Sshwifty backend server. It should only take " + + "less than a second, or two"; + const connectionStatusDisconnected = + "Sshwifty is disconnected from it's backend server"; + const connectionStatusConnected = + "Sshwifty is connected to it's backend server, user interface operational"; + const connectionStatusUnmeasurable = + "Unable to measure connection delay. The connection maybe very " + + "busy or already lost"; + + const connectionDelayGood = + "Connection delay is low, operation should be very responsive"; + const connectionDelayFair = + "Experiencing minor connection delay, operation should be responded " + + "within a reasonable time"; + const connectionDelayMedian = + "Experiencing median connection delay, consider to slow down your input " + + "to avoid misoperation"; + const connectionDelayHeavy = + "Experiencing bad connection delay, operation may freeze at any moment. " + + "Consider to pause your input until remote is responsive"; + + const buildEmptyHistory = () => { + let r = []; + + for (let i = 0; i < 32; i++) { + r.push({ data: 0, class: "" }); + } + + return r; + }; + + let isClosed = false, + inboundPerSecond = 0, + outboundPerSecond = 0, + trafficPreSecondNextUpdate = new Date(), + inboundPre10Seconds = 0, + outboundPre10Seconds = 0, + trafficPre10sNextUpdate = new Date(), + inboundHistory = new history.Records(buildEmptyHistory()), + outboundHistory = new history.Records(buildEmptyHistory()), + trafficSamples = 0; + + let delayHistory = new history.Records(buildEmptyHistory()), + delaySamples = 0, + delayPerInterval = 0; + + return { + update(time) { + if (isClosed) { + return; + } + + if (time >= trafficPreSecondNextUpdate) { + trafficPreSecondNextUpdate = new Date(time.getTime() + 1000); + inboundPre10Seconds += inboundPerSecond; + outboundPre10Seconds += outboundPerSecond; + + this.status.inbound = inboundPerSecond; + this.status.outbound = outboundPerSecond; + + inboundPerSecond = 0; + outboundPerSecond = 0; + + trafficSamples++; + } + + if (time >= trafficPre10sNextUpdate) { + trafficPre10sNextUpdate = new Date(time.getTime() + 10000); + + if (trafficSamples > 0) { + inboundHistory.update(inboundPre10Seconds / trafficSamples); + outboundHistory.update(outboundPre10Seconds / trafficSamples); + + inboundPre10Seconds = 0; + outboundPre10Seconds = 0; + trafficSamples = 0; + } + + if (delaySamples > 0) { + delayHistory.update(delayPerInterval / delaySamples); + + delaySamples = 0; + delayPerInterval = 0; + } + } + }, + classStyle: "", + windowClass: "", + message: "", + status: { + description: connectionStatusNotConnected, + delay: 0, + delayHistory: delayHistory.get(), + inbound: 0, + inboundHistory: inboundHistory.get(), + outbound: 0, + outboundHistory: outboundHistory.get(), + }, + connecting() { + isClosed = false; + + this.message = "--"; + this.classStyle = "working"; + this.windowClass = ""; + this.status.description = connectionStatusConnecting; + }, + connected() { + isClosed = false; + + this.message = "??"; + this.classStyle = "working"; + this.windowClass = ""; + this.status.description = connectionStatusConnected; + }, + traffic(inb, outb) { + inboundPerSecond += inb; + outboundPerSecond += outb; + }, + echo(delay) { + delayPerInterval += delay > 0 ? delay : 0; + delaySamples++; + + if (delay == ECHO_FAILED) { + this.status.delay = -1; + this.message = ""; + this.classStyle = "red flash"; + this.windowClass = "red"; + this.status.description = connectionStatusUnmeasurable; + + return; + } + + let avgDelay = Math.round(delayPerInterval / delaySamples); + + this.message = Number(avgDelay).toLocaleString() + "ms"; + this.status.delay = avgDelay; + + if (avgDelay < 30) { + this.classStyle = "green"; + this.windowClass = "green"; + this.status.description = + connectionStatusConnected + ". " + connectionDelayGood; + } else if (avgDelay < 100) { + this.classStyle = "yellow"; + this.windowClass = "yellow"; + this.status.description = + connectionStatusConnected + ". " + connectionDelayFair; + } else if (avgDelay < 300) { + this.classStyle = "orange"; + this.windowClass = "orange"; + this.status.description = + connectionStatusConnected + ". " + connectionDelayMedian; + } else { + this.classStyle = "red"; + this.windowClass = "red"; + this.status.description = + connectionStatusConnected + ". " + connectionDelayHeavy; + } + }, + close(e) { + isClosed = true; + delayHistory.expire(); + inboundHistory.expire(); + outboundHistory.expire(); + + ctx.connector.inputting = false; + + if (e === null) { + this.message = ""; + this.classStyle = ""; + this.status.description = connectionStatusDisconnected; + + return; + } + + this.status.delay = -1; + this.message = "ERR"; + this.classStyle = "red flash"; + this.windowClass = "red"; + this.status.description = connectionStatusDisconnected + ": " + e; + }, + failed(e) { + isClosed = true; + + ctx.connector.inputting = false; + + if (e.code) { + this.message = "E" + e.code; + } else { + this.message = "E????"; + } + + this.status.delay = -1; + this.classStyle = "red flash"; + this.windowClass = "red"; + this.status.description = connectionStatusDisconnected + ". Error: " + e; + }, + }; +} diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..642a073 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,62 @@ + + + + + + Garage Web SSH Client + + + +
+
+ + +

Chargement de Sshwifty

+ +
+

+ Ca charge! +

+ + + +
+
+
+ + diff --git a/ui/landing.css b/ui/landing.css new file mode 100644 index 0000000..37da237 --- /dev/null +++ b/ui/landing.css @@ -0,0 +1,132 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "utf-8"; + +body.landing { + background: #945; +} + +#landing { + min-height: 100%; + font-family: Arial, Helvetica, sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + justify-items: center; + color: #fff; + text-align: center; +} + +#landing-message { + font-size: 0.9em; + max-width: 500px; + margin: 0 auto; + padding: 50px; + flex: 0 0; +} + +#landing-message a { + text-decoration: none; + color: #fab; +} + +#landing-message p { + margin: 10px 0; +} + +#landing-message p.copy { + margin: 20px 0 10px 0; + color: #fab; +} + +#landing-message p.copy.copy-first { + margin-top: 50px; +} + +#landing-message p.copy a { + border: 1px solid #fab; + display: inline-block; + padding: 3px 7px; + margin: 5px; + border-radius: 5px; + line-height: initial; +} + +#landing-message-logo { + background: url("./widgets/busy.svg") center center no-repeat; + background-size: contain; + width: 100%; + height: 200px; +} + +#landing-message-info { + margin-top: 50px; + font-size: 0.9em; +} + +#auth { + width: 100%; + min-height: 100%; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + justify-content: center; +} + +#auth-frame { + flex: 0 0; + padding: 10px; +} + +#auth-content { + background: #333; + box-shadow: 0 0 3px #111; + padding: 30px; + max-width: 380px; + margin: 0 auto; + font-size: 1em; +} + +#auth-content > h1 { + margin-bottom: 20px; + color: #fab; + font-size: 1.2em; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 2px; +} + +#auth-content > h1:after { + content: "\2731"; + margin-left: 5px; +} + +#auth-content > form { + font-size: 0.9em; +} + +#auth-content > form .field:last-child { + margin-top: 30px; +} + +#auth-content > form > fieldset { + margin-top: 30px; +} diff --git a/ui/loading.vue b/ui/loading.vue new file mode 100644 index 0000000..4438104 --- /dev/null +++ b/ui/loading.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/ui/robots.txt b/ui/robots.txt new file mode 100644 index 0000000..2ca25e7 --- /dev/null +++ b/ui/robots.txt @@ -0,0 +1,4 @@ +user-agent: * + +Disallow: / +Allow: /$ \ No newline at end of file diff --git a/ui/socket.js b/ui/socket.js new file mode 100644 index 0000000..cffa33d --- /dev/null +++ b/ui/socket.js @@ -0,0 +1,397 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as crypt from "./crypto.js"; +import * as reader from "./stream/reader.js"; +import * as sender from "./stream/sender.js"; +import * as streams from "./stream/streams.js"; +import * as xhr from "./xhr.js"; + +export const ECHO_FAILED = streams.ECHO_FAILED; + +const maxSenderDelay = 200; +const minSenderDelay = 30; + +class Dial { + /** + * constructor + * + * @param {string} address Address to the Websocket server + * @param {number} Dial timeout + * @param {object} privateKey String key that will be used to encrypt and + * decrypt socket traffic + * + */ + constructor(address, timeout, privateKey) { + this.address = address; + this.timeout = timeout; + this.privateKey = privateKey; + this.keepAliveTicker = null; + } + + /** + * Connect to the remote server + * + * @param {string} address Target URL address + * @param {number} timeout Connect timeout + * + * @returns {Promise} When connection is established + * + */ + connect(address, timeout) { + const self = this; + return new Promise((resolve, reject) => { + let ws = new WebSocket(address.webSocket), + promised = false, + timeoutTimer = setTimeout(() => { + ws.close(); + }, timeout), + myRes = (w) => { + if (promised) { + return; + } + + clearTimeout(timeoutTimer); + promised = true; + + return resolve(w); + }, + myRej = (e) => { + if (promised) { + return; + } + + clearTimeout(timeoutTimer); + promised = true; + + return reject(e); + }; + + if (!self.keepAliveTicker) { + self.keepAliveTicker = setInterval(() => { + xhr.options(address.keepAlive, {}); + }, self.timeout); + } + + ws.addEventListener("open", (_event) => { + myRes(ws); + }); + + ws.addEventListener("close", (event) => { + event.toString = () => { + return "WebSocket Error (" + event.code + ")"; + }; + + myRej(event); + clearInterval(self.keepAliveTicker); + self.keepAliveTicker = null; + }); + + ws.addEventListener("error", (_event) => { + ws.close(); + clearInterval(self.keepAliveTicker); + self.keepAliveTicker = null; + }); + }); + } + + /** + * Build an socket encrypt and decrypt key string + * + */ + async buildKeyString() { + return this.privateKey.fetch(); + } + + /** + * Build encrypt and decrypt key + * + */ + async buildKey() { + let kStr = await this.buildKeyString(); + + return await crypt.buildGCMKey(kStr); + } + + /** + * Connect to the server + * + * @param {object} callbacks Callbacks + * + * @returns {object} A pair of ReadWriter which can be used to read and + * send data to the underlaying websocket connection + * + */ + async dial(callbacks) { + let ws = await this.connect(this.address, this.timeout); + + try { + let rd = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return new Promise((resolve) => { + let bufferReader = new FileReader(); + + bufferReader.onload = (event) => { + let d = new Uint8Array(event.target.result); + + resolve(d); + + callbacks.inboundUnpacked(d); + }; + + bufferReader.readAsArrayBuffer(data); + }); + }); + + ws.addEventListener("message", (event) => { + callbacks.inbound(event.data); + + rd.feed(event.data); + }); + + ws.addEventListener("error", (event) => { + event.toString = () => { + return ( + "WebSocket Error (" + (event.code ? event.code : "Unknown") + ")" + ); + }; + + rd.closeWithReason(event); + }); + + ws.addEventListener("close", (_event) => { + rd.closeWithReason("Connection is closed"); + }); + + let sdDataConvert = (rawData) => { + return rawData; + }, + getSdDataConvert = () => { + return sdDataConvert; + }, + sd = new sender.Sender( + async (rawData) => { + try { + let data = await getSdDataConvert()(rawData); + + ws.send(data.buffer); + callbacks.outbound(data); + } catch (e) { + ws.close(); + rd.closeWithReason(e); + + if (process.env.NODE_ENV === "development") { + console.error(e); + } + + throw e; + } + }, + 4096 - 64, // Server has a 4096 bytes receive buffer, can be no greater, + minSenderDelay, // 30ms input delay + 10, // max 10 buffered requests + ); + + let senderNonce = crypt.generateNonce(); + sd.send(senderNonce); + + let receiverNonce = await reader.readN(rd, crypt.GCMNonceSize); + + let key = await this.buildKey(); + + sdDataConvert = async (rawData) => { + let encoded = await crypt.encryptGCM(key, senderNonce, rawData); + + crypt.increaseNonce(senderNonce); + + let dataToSend = new Uint8Array(encoded.byteLength + 2); + + dataToSend[0] = (encoded.byteLength >> 8) & 0xff; + dataToSend[1] = encoded.byteLength & 0xff; + + dataToSend.set(new Uint8Array(encoded), 2); + + return dataToSend; + }; + + let cgmReader = new reader.Multiple(async (r) => { + try { + let dSizeBytes = await reader.readN(rd, 2), + dSize = 0; + + dSize = dSizeBytes[0]; + dSize <<= 8; + dSize |= dSizeBytes[1]; + + let decoded = await crypt.decryptGCM( + key, + receiverNonce, + await reader.readN(rd, dSize), + ); + + crypt.increaseNonce(receiverNonce); + + r.feed( + new reader.Buffer(new Uint8Array(decoded), () => {}), + () => {}, + ); + } catch (e) { + r.closeWithReason(e); + } + }); + + return { + reader: cgmReader, + sender: sd, + ws: ws, + }; + } catch (e) { + ws.close(); + throw e; + } + } +} + +export class Socket { + /** + * constructor + * + * @param {string} address Address of the WebSocket server + * @param {object} privateKey String key that will be used to encrypt and + * decrypt socket traffic + * @param {number} timeout Dial timeout + * @param {number} echoInterval Echo interval + */ + constructor(address, privateKey, timeout, echoInterval) { + this.dial = new Dial(address, timeout, privateKey); + this.echoInterval = echoInterval; + this.streamHandler = null; + } + + /** + * Return a stream handler + * + * @param {object} callbacks A group of callbacks to call when needed + * + * @returns {Promise} The stream manager + * + */ + async get(callbacks) { + let self = this; + + if (this.streamHandler) { + return this.streamHandler; + } + + callbacks.connecting(); + + const receiveToPauseFactor = 6, + minReceivedToPause = 1024 * 16; + + let streamPaused = false, + currentReceived = 0, + currentUnpacked = 0; + + const shouldPause = () => { + return ( + currentReceived > minReceivedToPause && + currentReceived > currentUnpacked * receiveToPauseFactor + ); + }; + + try { + let conn = await this.dial.dial({ + inbound(data) { + currentReceived += data.size; + + callbacks.traffic(data.size, 0); + }, + inboundUnpacked(data) { + currentUnpacked += data.length; + + if (currentUnpacked >= currentReceived) { + currentUnpacked = 0; + currentReceived = 0; + } + + if (self.streamHandler !== null) { + if (streamPaused && !shouldPause()) { + streamPaused = false; + self.streamHandler.resume(); + + return; + } else if (!streamPaused && shouldPause()) { + streamPaused = true; + self.streamHandler.pause(); + + return; + } + } + }, + outbound(data) { + callbacks.traffic(0, data.length); + }, + }); + + let streamHandler = new streams.Streams(conn.reader, conn.sender, { + echoInterval: self.echoInterval, + echoUpdater(delay) { + const sendDelay = delay / 2; + + if (sendDelay > maxSenderDelay) { + conn.sender.setDelay(maxSenderDelay); + } else if (sendDelay < minSenderDelay) { + conn.sender.setDelay(minSenderDelay); + } else { + conn.sender.setDelay(sendDelay); + } + + return callbacks.echo(delay); + }, + cleared(e) { + if (self.streamHandler === null) { + return; + } + + self.streamHandler = null; + + // Close connection first otherwise we may + // risk sending things out + conn.ws.close(); + callbacks.close(e); + }, + }); + + callbacks.connected(); + + streamHandler.serve().catch((e) => { + if (process.env.NODE_ENV !== "development") { + return; + } + + console.trace(e); + }); + + this.streamHandler = streamHandler; + } catch (e) { + callbacks.failed(e); + + throw e; + } + + return this.streamHandler; + } +} diff --git a/ui/sshwifty.svg b/ui/sshwifty.svg new file mode 100644 index 0000000..d325963 --- /dev/null +++ b/ui/sshwifty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/stream/common.js b/ui/stream/common.js new file mode 100644 index 0000000..3a7037c --- /dev/null +++ b/ui/stream/common.js @@ -0,0 +1,109 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +/** + * Get one unsafe random number + * + * @param {number} min Min value (included) + * @param {number} max Max value (not included) + * + * @returns {number} Get random number + * + */ +export function getRand(min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +/** + * Get a group of random number + * + * @param {number} n How many number to get + * @param {number} min Min value (included) + * @param {number} max Max value (not included) + * + * @returns {Array} A group of random number + */ +export function getRands(n, min, max) { + let r = []; + + for (let i = 0; i < n; i++) { + r.push(getRand(min, max)); + } + + return r; +} + +/** + * Separate given buffer to multiple ones based on input max length + * + * @param {Uint8Array} buf Buffer to separate + * @param {number} max Max length of each buffer + * + * @returns {Array} Separated buffers + * + */ +export function separateBuffer(buf, max) { + let start = 0, + result = []; + + while (start < buf.length) { + let remain = buf.length - start; + + if (remain <= max) { + result.push(buf.slice(start, start + remain)); + + return result; + } + + remain = max; + + result.push(buf.slice(start, start + remain)); + start += remain; + } +} + +/** + * Create an Uint8Array out of given binary string + * + * @param {string} str binary string + * + * @returns {Uint8Array} Separated buffers + * + */ +export function buildBufferFromString(str) { + let r = [], + t = []; + + for (let i in str) { + let c = str.charCodeAt(i); + + while (c > 0xff) { + t.push(c & 0xff); + c >>= 8; + } + + r.push(c); + + for (let j = t.length; j > 0; j--) { + r.push(t[j]); + } + + t = []; + } + + return new Uint8Array(r); +} diff --git a/ui/stream/common_test.js b/ui/stream/common_test.js new file mode 100644 index 0000000..eda63ce --- /dev/null +++ b/ui/stream/common_test.js @@ -0,0 +1,41 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import assert from "assert"; +import * as common from "./common.js"; + +describe("Common", () => { + it("separateBuffer", async () => { + let resultArr = []; + const expected = new Uint8Array([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, + 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, + 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, + 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + ]), + sepSeg = common.separateBuffer(expected, 16); + + sepSeg.forEach((d) => { + resultArr.push(...d); + }); + + const result = new Uint8Array(resultArr); + + assert.deepStrictEqual(result, expected); + }); +}); diff --git a/ui/stream/exception.js b/ui/stream/exception.js new file mode 100644 index 0000000..67fe76b --- /dev/null +++ b/ui/stream/exception.js @@ -0,0 +1,31 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +export default class Exception extends Error { + /** + * constructor + * + * @param {string} message error message + * @param {boolean} temporary whether or not the error is temporary + * + */ + constructor(message, temporary) { + super(message); + + this.temporary = temporary; + } +} diff --git a/ui/stream/header.js b/ui/stream/header.js new file mode 100644 index 0000000..ccd8232 --- /dev/null +++ b/ui/stream/header.js @@ -0,0 +1,264 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import Exception from "./exception.js"; + +export const CONTROL = 0x00; +export const STREAM = 0x40; +export const CLOSE = 0x80; +export const COMPLETED = 0xc0; + +export const CONTROL_ECHO = 0x00; +export const CONTROL_PAUSESTREAM = 0x01; +export const CONTROL_RESUMESTREAM = 0x02; + +const headerHeaderCutter = 0xc0; +const headerDataCutter = 0x3f; + +export const HEADER_MAX_DATA = headerDataCutter; + +export class Header { + /** + * constructor + * + * @param {number} headerByte one byte data of the header + */ + constructor(headerByte) { + this.headerByte = headerByte; + } + + /** + * Return the header type + * + * @returns {number} Type number + * + */ + type() { + return this.headerByte & headerHeaderCutter; + } + + /** + * Return the header data + * + * @returns {number} Data number + * + */ + data() { + return this.headerByte & headerDataCutter; + } + + /** + * Set the reader data + * + * @param {number} data + */ + set(data) { + if (data > headerDataCutter) { + throw new Exception("data must not be greater than 0x3f", false); + } + + this.headerByte |= headerDataCutter & data; + } + + /** + * Return the header value + * + * @returns {number} Header byte data + * + */ + value() { + return this.headerByte; + } +} + +export const STREAM_HEADER_BYTE_LENGTH = 2; +export const STREAM_MAX_LENGTH = 0x1fff; +export const STREAM_MAX_MARKER = 0x07; + +const streamHeaderLengthFirstByteCutter = 0x1f; + +export class Stream { + /** + * constructor + * + * @param {number} headerByte1 First header byte + * @param {number} headerByte2 Second header byte + * + */ + constructor(headerByte1, headerByte2) { + this.headerByte1 = headerByte1; + this.headerByte2 = headerByte2; + } + + /** + * Return the marker data + * + * @returns {number} the marker + * + */ + marker() { + return this.headerByte1 >> 5; + } + + /** + * Return the stream data length + * + * @returns {number} Length of the stream data + * + */ + length() { + let r = 0; + + r |= this.headerByte1 & streamHeaderLengthFirstByteCutter; + r <<= 8; + r |= this.headerByte2; + + return r; + } + + /** + * Set the header + * + * @param {number} marker Header marker + * @param {number} length Stream data length + * + */ + set(marker, length) { + if (marker > STREAM_MAX_MARKER) { + throw new Exception("marker must not be greater than 0x07", false); + } + + if (length > STREAM_MAX_LENGTH) { + throw new Exception("n must not be greater than 0x1fff", false); + } + + this.headerByte1 = + (marker << 5) | ((length >> 8) & streamHeaderLengthFirstByteCutter); + this.headerByte2 = length & 0xff; + } + + /** + * Return the header data + * + * @returns {Uint8Array} Header data + * + */ + buffer() { + return new Uint8Array([this.headerByte1, this.headerByte2]); + } +} + +export class InitialStream extends Stream { + /** + * Return how large the data can be + * + * @returns {number} Max data size + * + */ + static maxDataSize() { + return 0x07ff; + } + + /** + * constructor + * + * @param {number} headerByte1 First header byte + * @param {number} headerByte2 Second header byte + * + */ + constructor(headerByte1, headerByte2) { + super(headerByte1, headerByte2); + } + + /** + * Return command ID + * + * @returns {number} Command ID + * + */ + command() { + return this.headerByte1 >> 4; + } + + /** + * Return data + * + * @returns {number} Data + * + */ + data() { + let r = 0; + + r |= this.headerByte1 & 0x07; + r <<= 8; + r |= this.headerByte2 & 0xff; + + return r; + } + + /** + * Return whether or not the respond is success + * + * @returns {boolean} True when the request is successful, false otherwise + * + */ + success() { + return (this.headerByte1 & 0x08) != 0; + } + + /** + * Set the header + * + * @param {number} commandID Command ID + * @param {number} data Stream data + * @param {boolean} success Whether or not the request is successful + * + */ + set(commandID, data, success) { + if (commandID > 0x0f) { + throw new Exception("Command ID must not greater than 0x0f", false); + } + + if (data > InitialStream.maxDataSize()) { + throw new Exception("Data must not greater than 0x07ff", false); + } + + let dd = data & InitialStream.maxDataSize(); + + if (success) { + dd |= 0x0800; + } + + this.headerByte1 = 0; + this.headerByte1 |= commandID << 4; + this.headerByte1 |= dd >> 8; + this.headerByte2 = 0; + this.headerByte2 |= dd & 0xff; + } +} + +/** + * Build a new Header + * + * @param {number} h Header number + * + * @returns {Header} The header which been built + * + */ +export function header(h) { + return new Header(h); +} diff --git a/ui/stream/header_test.js b/ui/stream/header_test.js new file mode 100644 index 0000000..7ad94b7 --- /dev/null +++ b/ui/stream/header_test.js @@ -0,0 +1,56 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import assert from "assert"; +import * as header from "./header.js"; + +describe("Header", () => { + it("Header", () => { + let h = new header.Header(header.ECHO); + + h.set(63); + + let n = new header.Header(h.value()); + + assert.strictEqual(h.type(), n.type()); + assert.strictEqual(h.data(), n.data()); + assert.strictEqual(n.type(), header.CONTROL); + assert.strictEqual(n.data(), 63); + }); + + it("Stream", () => { + let h = new header.Stream(0, 0); + + h.set(header.STREAM_MAX_MARKER, header.STREAM_MAX_LENGTH); + + assert.strictEqual(h.marker(), header.STREAM_MAX_MARKER); + assert.strictEqual(h.length(), header.STREAM_MAX_LENGTH); + + assert.strictEqual(h.headerByte1, 0xff); + assert.strictEqual(h.headerByte2, 0xff); + }); + + it("InitialStream", () => { + let h = new header.InitialStream(0, 0); + + h.set(15, 128, true); + + assert.strictEqual(h.command(), 15); + assert.strictEqual(h.data(), 128); + assert.strictEqual(h.success(), true); + }); +}); diff --git a/ui/stream/reader.js b/ui/stream/reader.js new file mode 100644 index 0000000..0182c2c --- /dev/null +++ b/ui/stream/reader.js @@ -0,0 +1,587 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import Exception from "./exception.js"; +import * as subscribe from "./subscribe.js"; + +export class Buffer { + /** + * constructor + * + * @param {Uint8Array} buffer Array buffer + * @param {function} depleted Callback that will be called when the buffer + * is depleted + */ + constructor(buffer, depleted) { + this.buffer = buffer; + this.used = 0; + this.onDepleted = depleted; + } + + /** + * Return the index of given byte inside current available (unused) read + * buffer + * + * @param {number} byteData Target data + * @param {number} maxLen Max search length + * + * @returns {number} Return number >= 0 when found, -1 when not + */ + searchBuffer(byteData, maxLen) { + let searchLen = this.remains(); + + if (searchLen > maxLen) { + searchLen = maxLen; + } + + for (let i = 0; i < searchLen; i++) { + if (this.buffer[i + this.used] !== byteData) { + continue; + } + + return i; + } + + return -1; + } + + /** + * Return the index of given byte inside current available (unused) read + * buffer + * + * @param {number} byteData Target data + * + * @returns {number} Return number >= 0 when found, -1 when not + */ + indexOf(byteData) { + return this.searchBuffer(byteData, this.remains()); + } + + /** + * Return how many bytes in the source + buffer is still available to be + * read, return 0 when reader is depleted and thus can be ditched + * + * @returns {number} Remaining size + * + */ + remains() { + return this.buffer.length - this.used; + } + + /** + * Return how many bytes is still availale in the buffer. + * + * Note: This reader don't have renewable data source, so when buffer + * depletes, the reader is done + * + * @returns {number} Remaining size + * + */ + buffered() { + return this.remains(); + } + + /** + * Export max n bytes from current buffer + * + * @param {number} n suggested max byte length, set to 0 to refresh buffer + * if current buffer is deplated + * + * @returns {Uint8Array} Exported data + * + * @throws {Exception} When reader has been depleted + * + */ + export(n) { + let remain = this.remains(); + + if (remain <= 0) { + throw new Exception("Reader has been depleted", false); + } + + if (remain > n) { + remain = n; + } + + let exported = this.buffer.slice(this.used, this.used + remain); + this.used += exported.length; + + if (this.remains() <= 0) { + this.onDepleted(); + } + + return exported; + } +} + +export class Multiple { + /** + * Constructor + * + * @param {function} depleted Callback will be called when all reader is + * depleted + * + */ + constructor(depleted) { + this.reader = null; + this.depleted = depleted; + this.subscribe = new subscribe.Subscribe(); + this.closed = false; + } + + /** + * Add new reader as sub reader + * + * @param {Buffer} reader + * @param {function} depleted Callback that will be called when given reader + * is depleted + * + * @throws {Exception} When the reader is closed + * + */ + feed(reader, depleted) { + if (this.closed) { + throw new Exception("Reader is closed", false); + } + + if (this.reader === null && this.subscribe.pendings() <= 0) { + this.reader = { + reader: reader, + depleted: depleted, + }; + + return; + } + + this.subscribe.resolve({ + reader: reader, + depleted: depleted, + }); + } + + /** + * Return the index of given byte inside current available (unused) read + * buffer + * + * @param {number} byteData Target data + * @param {number} maxLen Max search length + * + * @returns {number} Return number >= 0 when found, -1 when not + * + */ + searchBuffer(byteData, maxLen) { + if (this.reader === null) { + return -1; + } + + return this.reader.reader.searchBuffer(byteData, maxLen); + } + + /** + * Return the index of given byte inside current available (unused) read + * buffer + * + * @param {number} byteData Target data + * + * @returns {number} Return number >= 0 when found, -1 when not + */ + indexOf(byteData) { + return this.searchBuffer(byteData, this.buffered()); + } + + /** + * Return how many bytes still available in the buffer (How many bytes of + * buffer is left for read before reloading from data source) + * + * @returns {number} How many bytes left in the current buffer + */ + buffered() { + if (this.reader == null) { + return 0; + } + + return this.reader.reader.buffered(); + } + + /** + * close current reading + * + */ + close() { + return this.closeWithReason("Reader is closed"); + } + + /** + * close current reading + * + * @param {string} reason Reason + * + */ + closeWithReason(reason) { + if (this.closed) { + return; + } + + this.closed = true; + this.subscribe.reject(new Exception(reason, false)); + this.subscribe.disable(reason); + } + + /** + * Export max n bytes from current buffer + * + * @param {number} n suggested max byte length, set to 0 to refresh buffer + * if current buffer is deplated + * + * @returns {Uint8Array} Exported data + * + */ + async export(n) { + for (;;) { + if (this.reader !== null) { + let exported = await this.reader.reader.export(n); + + if (this.reader.reader.remains() <= 0) { + this.reader.depleted(); + + this.reader = null; + } + + return exported; + } + + this.depleted(this); + + this.reader = await this.subscribe.subscribe(); + } + } +} + +export class Reader { + /** + * constructor + * + * @param {Multiple} multiple Source reader + * @param {function} bufferConverter Function convert + * + */ + constructor(multiple, bufferConverter) { + this.multiple = multiple; + this.buffers = new subscribe.Subscribe(); + this.bufferConverter = + bufferConverter || + ((d) => { + return d; + }); + this.closed = false; + } + + /** + * Add buffer into current reader + * + * @param {Uint8Array} buffer buffer to add + * + * @throws {Exception} When the reader is closed + * + */ + feed(buffer) { + if (this.closed) { + throw new Exception("Reader is closed, new data has been deined", false); + } + + this.buffers.resolve(buffer); + } + + async reader() { + if (this.closed) { + throw new Exception("Reader is closed, unable to read", false); + } + + if (this.multiple.buffered() > 0) { + return this.multiple; + } + + let self = this, + converted = await this.bufferConverter(await self.buffers.subscribe()); + + this.multiple.feed(new Buffer(converted, () => {}), () => {}); + + return this.multiple; + } + + /** + * close current reading + * + */ + close() { + return this.closeWithReason("Reader is closed"); + } + + /** + * close current reading + * + * @param {string} reason Reason + * + */ + closeWithReason(reason) { + if (this.closed) { + return; + } + + this.closed = true; + this.buffers.reject(new Exception(reason, false)); + this.buffers.disable(reason); + + return this.multiple.close(); + } + + /** + * Return the index of given byte inside current available (unused) read + * buffer + * + * @param {number} byteData Target data + * @param {number} maxLen Max search length + * + * @returns {number} Return number >= 0 when found, -1 when not + */ + async searchBuffer(byteData, maxLen) { + return (await this.reader()).searchBuffer(byteData, maxLen); + } + + /** + * Return the index of given byte inside current available (unused) read + * buffer + * + * @param {number} byteData Target data + * + * @returns {number} Return number >= 0 when found, -1 when not + */ + async indexOf(byteData) { + return (await this.reader()).indexOf(byteData); + } + + /** + * Return how many bytes still available in the buffer (How many bytes of + * buffer is left for read before reloading from data source) + * + * @returns {number} How many bytes left in the current buffer + */ + async buffered() { + return (await this.reader()).buffered(); + } + + /** + * Export max n bytes from current buffer + * + * @param {number} n suggested max byte length, set to 0 to refresh buffer + * if current buffer is deplated + * + * @returns {Uint8Array} Exported data + * + */ + async export(n) { + return (await this.reader()).export(n); + } +} + +/** + * Read exactly one bytes from the reader + * + * @param {Reader} reader the source reader + * + * @returns {Uint8Array} Exported data + * + */ +export async function readOne(reader) { + for (;;) { + let d = await reader.export(1); + + if (d.length <= 0) { + continue; + } + + return d; + } +} + +/** + * Read exactly n bytes from the reader + * + * @param {Reader} reader the source reader + * @param {number} n length to read + * + * @returns {Uint8Array} Exported data + * + */ +export async function readN(reader, n) { + let readed = 0, + result = new Uint8Array(n); + + while (readed < n) { + let exported = await reader.export(n - readed); + + result.set(exported, readed); + readed += exported.length; + } + + return result; +} + +export class Limited { + /** + * Constructor + * + * @param {Reader} reader the source reader + * @param {number} maxN max bytes to read + * + * @returns {boolean} true when the reader is completed, false otherwise + * + */ + constructor(reader, maxN) { + this.reader = reader; + this.remain = maxN; + } + + /** + * Indicate whether or not the current reader is completed + * + * @returns {boolean} true when the reader is completed, false otherwise + * + */ + completed() { + return this.remain <= 0; + } + + /** + * Return the index of given byte inside current available (unused) read + * buffer + * + * @param {number} byteData Target data + * @param {number} maxLen Max search length + * + * @returns {number} Return number >= 0 when found, -1 when not + * + */ + searchBuffer(byteData, maxLen) { + return this.reader.searchBuffer( + byteData, + maxLen > this.remain ? this.remain : maxLen, + ); + } + + /** + * Return the index of given byte inside current read buffer + * + * @param {number} byteData Target data + * + * @returns {number} Return number >= 0 when found, -1 when not + */ + indexOf(byteData) { + return this.reader.searchBuffer(byteData, this.remain); + } + + /** + * Return how many bytes still available to be read + * + * @returns {number} Remaining size + * + */ + remains() { + return this.remain; + } + + /** + * Return how many bytes still available in the buffer (How many bytes of + * buffer is left for read before reloading from data source) + * + * @returns {number} Remaining size + * + */ + buffered() { + let buf = this.reader.buffered(); + + return buf > this.remain ? this.remain : buf; + } + + /** + * Export max n bytes from current buffer + * + * @param {number} n suggested max length + * + * @throws {Exception} when reading already completed + * + * @returns {Uint8Array} Exported data + * + */ + async export(n) { + if (this.completed()) { + throw new Exception("Reader already completed", false); + } + + let toRead = n > this.remain ? this.remain : n, + exported = await this.reader.export(toRead); + + this.remain -= exported.length; + + return exported; + } +} + +/** + * Read the whole Limited reader and return the result + * + * @param {Limited} limited the Limited reader + * + * @returns {Uint8Array} Exported data + * + */ +export async function readCompletely(limited) { + return await readN(limited, limited.remains()); +} + +/** + * Read until given byteData is reached. This function is guaranteed to spit + * out at least one byte + * + * @param {Reader} indexOfReader + * @param {number} byteData + */ +export async function readUntil(indexOfReader, byteData) { + let pos = await indexOfReader.indexOf(byteData), + buffered = await indexOfReader.buffered(); + + if (pos >= 0) { + return { + data: await readN(indexOfReader, pos + 1), + found: true, + }; + } + + if (buffered <= 0) { + let d = await readOne(indexOfReader); + + return { + data: d, + found: d[0] === byteData, + }; + } + + return { + data: await readN(indexOfReader, buffered), + found: false, + }; +} diff --git a/ui/stream/reader_test.js b/ui/stream/reader_test.js new file mode 100644 index 0000000..19ca148 --- /dev/null +++ b/ui/stream/reader_test.js @@ -0,0 +1,197 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import assert from "assert"; +import * as reader from "./reader.js"; + +describe("Reader", () => { + it("Buffer", async () => { + let buf = new reader.Buffer( + new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]), + () => {}, + ); + + let ex = buf.export(1); + + assert.strictEqual(ex.length, 1); + assert.strictEqual(ex[0], 0); + assert.strictEqual(buf.remains(), 7); + + ex = await reader.readCompletely(buf); + + assert.strictEqual(ex.length, 7); + assert.deepStrictEqual(ex, new Uint8Array([1, 2, 3, 4, 5, 6, 7])); + assert.strictEqual(buf.remains(), 0); + }); + + it("Reader", async () => { + const maxTests = 3; + let IntvCount = 0, + r = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return data; + }), + expected = [ + 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, + ], + feedIntv = setInterval(() => { + r.feed(Uint8Array.from(expected.slice(0, 8))); + + IntvCount++; + + if (IntvCount < maxTests) { + return; + } + + clearInterval(feedIntv); + }, 300); + + let result = []; + + while (result.length < expected.length) { + result.push((await r.export(1))[0]); + } + + assert.deepStrictEqual(result, expected); + }); + + it("readOne", async () => { + let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return data; + }); + + setTimeout(() => { + r.feed(Uint8Array.from([0, 1, 2, 3, 4, 5, 7])); + }, 100); + + let rr = await reader.readOne(r); + + assert.deepStrictEqual(rr, Uint8Array.from([0])); + + rr = await reader.readOne(r); + + assert.deepStrictEqual(rr, Uint8Array.from([1])); + }); + + it("readN", async () => { + let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return data; + }); + + setTimeout(() => { + r.feed(Uint8Array.from([0, 1, 2, 3, 4, 5, 7])); + }, 100); + + let rr = await reader.readN(r, 3); + + assert.deepStrictEqual(rr, Uint8Array.from([0, 1, 2])); + + rr = await reader.readN(r, 3); + + assert.deepStrictEqual(rr, Uint8Array.from([3, 4, 5])); + }); + + it("Limited", async () => { + const maxTests = 3; + let IntvCount = 0, + r = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return data; + }), + expected = [0, 1, 2, 3, 4, 5, 6, 7, 0, 1], + limited = new reader.Limited(r, 10), + feedIntv = setInterval(() => { + r.feed(Uint8Array.from(expected.slice(0, 8))); + + IntvCount++; + + if (IntvCount < maxTests) { + return; + } + + clearInterval(feedIntv); + }, 300); + + let result = []; + + while (!limited.completed()) { + result.push((await limited.export(1))[0]); + } + + assert.strictEqual(limited.completed(), true); + assert.deepStrictEqual(result, expected); + }); + + it("readCompletely", async () => { + const maxTests = 3; + let IntvCount = 0, + r = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return data; + }), + expected = [0, 1, 2, 3, 4, 5, 6, 7, 0, 1], + limited = new reader.Limited(r, 10), + feedIntv = setInterval(() => { + r.feed(Uint8Array.from(expected.slice(0, 8))); + + IntvCount++; + + if (IntvCount < maxTests) { + return; + } + + clearInterval(feedIntv); + }, 300); + + let result = await reader.readCompletely(limited); + + assert.strictEqual(limited.completed(), true); + assert.deepStrictEqual(result, Uint8Array.from(expected)); + }); + + it("readUntil", async () => { + const maxTests = 3; + let IntvCount = 0, + r = new reader.Reader(new reader.Multiple(() => {}), (data) => { + return data; + }), + sample = [0, 1, 2, 3, 4, 5, 6, 7, 0, 1], + expected1 = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]), + expected2 = new Uint8Array([0, 1]), + limited = new reader.Limited(r, 10), + feedIntv = setInterval(() => { + r.feed(Uint8Array.from(sample)); + + IntvCount++; + + if (IntvCount < maxTests) { + return; + } + + clearInterval(feedIntv); + }, 300); + + let result = await reader.readUntil(limited, 7); + + assert.strictEqual(limited.completed(), false); + assert.deepStrictEqual(result.data, expected1); + assert.deepStrictEqual(result.found, true); + + result = await reader.readUntil(limited, 7); + + assert.strictEqual(limited.completed(), true); + assert.deepStrictEqual(result.data, expected2); + assert.deepStrictEqual(result.found, false); + }); +}); diff --git a/ui/stream/sender.js b/ui/stream/sender.js new file mode 100644 index 0000000..7888d60 --- /dev/null +++ b/ui/stream/sender.js @@ -0,0 +1,225 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import Exception from "./exception.js"; +import * as subscribe from "./subscribe.js"; + +export class Sender { + /** + * constructor + * + * @param {function} sender Underlaying sender + * @param {integer} maxSegSize The size of max data segment + * @param {integer} bufferFlushDelay Buffer flush delay + * @param {integer} maxBufferedRequests Buffer flush delay + * + */ + constructor(sender, maxSegSize, bufferFlushDelay, maxBufferedRequests) { + this.sender = sender; + this.maxSegSize = maxSegSize; + this.subscribe = new subscribe.Subscribe(); + this.sendingPoc = this.sending(); + this.sendDelay = null; + this.bufferFlushDelay = bufferFlushDelay; + this.maxBufferedRequests = maxBufferedRequests; + this.buffer = new Uint8Array(maxSegSize); + this.bufferUsed = 0; + this.bufferReq = 0; + } + + /** + * Set the send delay of current sender + * + * @param {integer} newDelay the new delay + * + */ + setDelay(newDelay) { + this.bufferFlushDelay = newDelay; + } + + /** + * Sends data to the this.sender + * + * @param {Uint8Array} data to send + * @param {Array} callbacks to call to return send result + * + */ + async sendData(data, callbacks) { + try { + await this.sender(data); + + for (let i in callbacks) { + callbacks[i].resolve(); + } + } catch (e) { + for (let i in callbacks) { + callbacks[i].reject(e); + } + } + } + + /** + * Append data to the end of internal buffer + * + * @param {Uint8Array} data data to add + * + * @returns {integer} How many bytes of data is added + * + */ + appendBuffer(data) { + const remainSize = this.buffer.length - this.bufferUsed, + appendLength = data.length > remainSize ? remainSize : data.length; + + this.buffer.set(data.slice(0, appendLength), this.bufferUsed); + this.bufferUsed += appendLength; + + return appendLength; + } + + /** + * Export current buffer and reset it to empty + * + * @returns {Uint8Array} Exported buffer + * + */ + exportBuffer() { + const buffer = this.buffer.slice(0, this.bufferUsed); + + this.bufferUsed = 0; + this.bufferedRequests = 0; + + return buffer; + } + + /** + * Sender proc + * + */ + async sending() { + let callbacks = []; + + for (;;) { + const fetched = await this.subscribe.subscribe(); + + // Force flush? + if (fetched === true) { + if (this.bufferUsed <= 0) { + continue; + } + + await this.sendData(this.exportBuffer(), callbacks); + callbacks = []; + + continue; + } + + callbacks.push({ + resolve: fetched.resolve, + reject: fetched.reject, + }); + + // Add data to buffer and maybe flush when the buffer is full + let currentSendDataLen = 0; + + while (fetched.data.length > currentSendDataLen) { + const sentLen = this.appendBuffer( + fetched.data.slice(currentSendDataLen, fetched.data.length), + ); + + // Buffer not full, wait for the force flush + if (this.buffer.length > this.bufferUsed) { + break; + } + + currentSendDataLen += sentLen; + + await this.sendData(this.exportBuffer(), callbacks); + callbacks = []; + } + } + } + + /** + * Clear everything + * + */ + close() { + if (this.sendDelay !== null) { + clearTimeout(this.sendDelay); + this.sendDelay = null; + } + + this.buffered = null; + this.bufferUsed = 0; + this.bufferedRequests = 0; + + this.subscribe.reject(new Exception("Sender has been cleared", false)); + this.subscribe.disable(); + + this.sendingPoc.catch(() => {}); + } + + /** + * Send data + * + * @param {Uint8Array} data data to send + * + * @throws {Exception} when sending has been cancelled + * + * @returns {Promise} will be resolved when the data is send and will be + * rejected when the data is not + * + */ + send(data) { + let delayCleared = false; + + if (this.sendDelay !== null) { + clearTimeout(this.sendDelay); + this.sendDelay = null; + delayCleared = true; + } + + const self = this; + + return new Promise((resolve, reject) => { + self.subscribe.resolve({ + data: data, + resolve: resolve, + reject: reject, + }); + + if (self.bufferedRequests >= self.maxBufferedRequests) { + self.bufferedRequests = 0; + + self.subscribe.resolve(true); + + return; + } + + if (delayCleared) { + self.bufferedRequests++; + } + + self.sendDelay = setTimeout(() => { + self.sendDelay = null; + self.bufferedRequests = 0; + + self.subscribe.resolve(true); + }, self.bufferFlushDelay); + }); + } +} diff --git a/ui/stream/sender_test.js b/ui/stream/sender_test.js new file mode 100644 index 0000000..2a0436c --- /dev/null +++ b/ui/stream/sender_test.js @@ -0,0 +1,121 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import assert from "assert"; +import * as sender from "./sender.js"; + +describe("Sender", () => { + function generateTestData(size) { + let d = new Uint8Array(size); + + for (let i = 0; i < d.length; i++) { + d[i] = i % 256; + } + + return d; + } + + it("Send", async () => { + const maxSegSize = 64; + let result = []; + let sd = new sender.Sender( + (rawData) => { + return new Promise((resolve) => { + setTimeout(() => { + for (let i in rawData) { + result.push(rawData[i]); + } + + resolve(); + }, 5); + }); + }, + maxSegSize, + 300, + 3, + ); + let expected = generateTestData(maxSegSize * 16); + + sd.send(expected); + + let sendCompleted = new Promise((resolve) => { + let timer = setInterval(() => { + if (result.length < expected.length) { + return; + } + + clearInterval(timer); + timer = null; + resolve(); + }, 100); + }); + + await sendCompleted; + + assert.deepStrictEqual(new Uint8Array(result), expected); + }); + + it("Send (Multiple calls)", async () => { + const maxSegSize = 64; + let result = []; + let sd = new sender.Sender( + (rawData) => { + return new Promise((resolve) => { + setTimeout(() => { + for (let i in rawData) { + result.push(rawData[i]); + } + + resolve(); + }, 10); + }); + }, + maxSegSize, + 300, + 100, + ); + let expectedSingle = generateTestData(maxSegSize * 2), + expectedLen = expectedSingle.length * 16, + expected = new Uint8Array(expectedLen); + + for (let i = 0; i < expectedLen; i += expectedSingle.length) { + expected.set(expectedSingle, i); + } + + for (let i = 0; i < expectedLen; i += expectedSingle.length) { + setTimeout(() => { + sd.send(expectedSingle); + }, 100); + } + + let sendCompleted = new Promise((resolve) => { + let timer = setInterval(() => { + if (result.length < expectedLen) { + return; + } + + clearInterval(timer); + timer = null; + resolve(); + }, 100); + }); + + await sendCompleted; + + assert.deepStrictEqual(new Uint8Array(result), expected); + }); +}); diff --git a/ui/stream/stream.js b/ui/stream/stream.js new file mode 100644 index 0000000..1807871 --- /dev/null +++ b/ui/stream/stream.js @@ -0,0 +1,363 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as common from "./common.js"; +import Exception from "./exception.js"; +import * as header from "./header.js"; +import * as reader from "./reader.js"; +import * as sender from "./sender.js"; + +export class Sender { + /** + * constructor + * + * @param {number} id ID of the stream + * @param {sender.Sender} sd The data sender + * + */ + constructor(id, sd) { + this.id = id; + this.sender = sd; + this.closed = false; + } + + /** + * Sends data to remote + * + * @param {number} marker binary marker + * @param {Uint8Array} data data to be sent + * + * @throws {Exception} When the sender already been closed + * + */ + send(marker, data) { + if (this.closed) { + throw new Exception( + "Sender already been closed. No data can be send", + false, + ); + } + + let reqHeader = new header.Header(header.STREAM), + stHeader = new header.Stream(0, 0), + d = new Uint8Array(data.length + 3); + + reqHeader.set(this.id); + stHeader.set(marker, data.length); + + d[0] = reqHeader.value(); + d.set(stHeader.buffer(), 1); + d.set(data, 3); + + return this.sender.send(d); + } + + /** + * Sends data to remote, if the data is too long, it will be separated into + * different stream requests + * + * @param {number} marker binary marker + * @param {Uint8Array} data data to be sent + * + * @throws {Exception} When the sender already been closed + * + */ + async sendData(marker, data) { + if (this.closed) { + throw new Exception( + "Sender already been closed. No data can be send", + false, + ); + } + + let dataSeg = common.separateBuffer(data, header.STREAM_MAX_LENGTH), + reqHeader = new header.Header(header.STREAM); + + reqHeader.set(this.id); + + for (let i in dataSeg) { + let stHeader = new header.Stream(0, 0), + d = new Uint8Array(dataSeg[i].length + 3); + + stHeader.set(marker, dataSeg[i].length); + + d[0] = reqHeader.value(); + d.set(stHeader.buffer(), 1); + d.set(dataSeg[i], 3); + + await this.sender.send(d); + } + } + + /** + * Send stream signals + * + * @param {number} signal Signal value + * + * @throws {Exception} When the sender already been closed + * + */ + signal(signal) { + if (this.closed) { + throw new Exception( + "Sender already been closed. No signal can be send", + false, + ); + } + + let reqHeader = new header.Header(signal); + + reqHeader.set(this.id); + + return this.sender.send(new Uint8Array([reqHeader.value()])); + } + + /** + * Send close signal and close current sender + * + */ + close() { + if (this.closed) { + return; + } + + let r = this.signal(header.CLOSE); + + this.closed = true; + + return r; + } +} + +export class InitialSender { + /** + * constructor + * + * @param {number} id ID of the stream + * @param {number} commandID ID of the command + * @param {sender.Sender} sd The data sender + * + */ + constructor(id, commandID, sd) { + this.id = id; + this.command = commandID; + this.sender = sd; + } + + /** + * Return how large the data can be + * + * @returns {number} Max data size + * + */ + static maxDataLength() { + return header.InitialStream.maxDataSize(); + } + + /** + * Sends data to remote + * + * @param {Uint8Array} data data to be sent + * + */ + send(data) { + let reqHeader = new header.Header(header.STREAM), + stHeader = new header.InitialStream(0, 0), + d = new Uint8Array(data.length + 3); + + reqHeader.set(this.id); + stHeader.set(this.command, data.length, true); + + d[0] = reqHeader.value(); + d.set(stHeader.buffer(), 1); + d.set(data, 3); + + return this.sender.send(d); + } +} + +export class Stream { + /** + * constructor + * + * @param {number} id ID of the stream + * + */ + constructor(id) { + this.id = id; + this.command = null; + this.isInitializing = false; + this.isShuttingDown = false; + } + + /** + * Returns whether or not current stream is running + * + * @returns {boolean} True when it's running, false otherwise + * + */ + running() { + return this.command !== null; + } + + /** + * Returns whether or not current stream is initializing + * + * @returns {boolean} True when it's initializing, false otherwise + * + */ + initializing() { + return this.isInitializing; + } + + /** + * Unsets current stream + * + */ + clear() { + this.command = null; + this.isInitializing = false; + this.isShuttingDown = false; + } + + /** + * Request the stream for a new command + * + * @param {number} commandID Command ID + * @param {function} commandBuilder Function that returns a command + * @param {sender.Sender} sd Data sender + * + * @throws {Exception} when stream already running + * + */ + run(commandID, commandBuilder, sd) { + if (this.running()) { + throw new Exception( + "Stream already running, cannot accept new commands", + false, + ); + } + + this.isInitializing = true; + this.command = commandBuilder(new Sender(this.id, sd)); + + return this.command.run(new InitialSender(this.id, commandID, sd)); + } + + /** + * Called when initialization respond has been received + * + * @param {header.InitialStream} streamInitialHeader Stream Initial header + * + * @throws {Exception} When the stream is not running, or been shutting down + * + */ + initialize(hd) { + if (!this.running()) { + throw new Exception( + "Cannot initialize a stream that is not running", + false, + ); + } + + if (this.isShuttingDown) { + throw new Exception( + "Cannot initialize a stream that is about to shutdown", + false, + ); + } + + this.command.initialize(hd); + + if (!hd.success()) { + this.clear(); + + return; + } + + this.isInitializing = false; + } + + /** + * Called when Stream data has been received + * + * @param {header.Stream} streamHeader Stream header + * @param {reader.Limited} rd Data reader + * + * @throws {Exception} When the stream is not running, or shutting down + * + */ + tick(streamHeader, rd) { + if (!this.running()) { + throw new Exception("Cannot tick a stream that is not running", false); + } + + if (this.isShuttingDown) { + throw new Exception( + "Cannot tick a stream that is about to shutdown", + false, + ); + } + + return this.command.tick(streamHeader, rd); + } + + /** + * Called when stream close request has been received + * + * @throws {Exception} When the stream is not running, or shutting down + * + */ + close() { + if (!this.running()) { + throw new Exception("Cannot close a stream that is not running", false); + } + + if (this.isShuttingDown) { + throw new Exception( + "Cannot close a stream that is about to shutdown", + false, + ); + } + + this.isShuttingDown = true; + this.command.close(); + } + + /** + * Called when stream completed respond has been received + * + * @throws {Exception} When stream isn't running, or not shutting down + * + */ + completed() { + if (!this.running()) { + throw new Exception("Cannot close a stream that is not running", false); + } + + if (!this.isShuttingDown) { + throw new Exception( + "Can't complete current stream because Close " + + "signal is not received", + false, + ); + } + + this.command.completed(); + this.clear(); + } +} diff --git a/ui/stream/streams.js b/ui/stream/streams.js new file mode 100644 index 0000000..4072cdf --- /dev/null +++ b/ui/stream/streams.js @@ -0,0 +1,436 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as common from "./common.js"; +import Exception from "./exception.js"; +import * as header from "./header.js"; +import * as reader from "./reader.js"; +import * as sender from "./sender.js"; +import * as stream from "./stream.js"; + +export const ECHO_FAILED = -1; + +export class Requested { + /** + * constructor + * + * @param {stream.Stream} stream The selected stream + * @param {any} result Result of the run + * + */ + constructor(stream, result) { + this.stream = stream; + this.result = result; + } +} + +export class Streams { + /** + * constructor + * + * @param {reader.Reader} reader The data reader + * @param {sender.Sender} sender The data sender + * @param {object} config Configuration + */ + constructor(reader, sender, config) { + this.reader = reader; + this.sender = sender; + this.config = config; + this.echoTimer = null; + this.lastEchoTime = null; + this.lastEchoData = null; + this.stop = false; + + this.streams = []; + + for (let i = 0; i <= header.HEADER_MAX_DATA; i++) { + this.streams.push(new stream.Stream(i)); + } + } + + /** + * Starts stream proccessing + * + * @returns {Promise} When service is completed + * + * @throws {Exception} When the process already started + * + */ + async serve() { + if (this.echoTimer !== null) { + throw new Exception("Already started", false); + } + + this.echoTimer = setInterval(() => { + this.sendEcho(); + }, this.config.echoInterval); + + this.stop = false; + + this.sendEcho(); + + let ee = null; + + while (!this.stop && ee === null) { + try { + await this.tick(); + } catch (e) { + if (!e.temporary) { + ee = e; + } + } + } + + this.clear(ee); + + if (ee !== null) { + throw new Exception("Streams is closed: " + ee, false); + } + } + + /** + * Clear current proccess + * + * @param {Exception} e An error caused this clear. Null when no error + * + */ + clear(e) { + if (this.stop) { + return; + } + + this.stop = true; + + if (this.echoTimer != null) { + clearInterval(this.echoTimer); + this.echoTimer = null; + } + + for (let i in this.streams) { + if (!this.streams[i].running()) { + continue; + } + + try { + this.streams[i].close(); + } catch (e) { + // Do nothing + } + + try { + this.streams[i].completed(); + } catch (e) { + //Do nothing + } + } + + try { + this.sender.close(); + } catch (e) { + process.env.NODE_ENV === "development" && console.trace(e); + } + + try { + this.reader.close(); + } catch (e) { + process.env.NODE_ENV === "development" && console.trace(e); + } + + this.config.cleared(e); + } + + /** + * Request remote to pause stream sending + * + */ + pause() { + let pauseHeader = header.header(header.CONTROL); + + pauseHeader.set(1); + + return this.sender.send( + new Uint8Array([pauseHeader.value(), header.CONTROL_PAUSESTREAM]), + ); + } + + /** + * Request remote to resume stream sending + * + */ + resume() { + let pauseHeader = header.header(header.CONTROL); + + pauseHeader.set(1); + + return this.sender.send( + new Uint8Array([pauseHeader.value(), header.CONTROL_RESUMESTREAM]), + ); + } + + /** + * Request stream for given command + * + * @param {number} commandID Command ID + * @param {function} commandBuilder Command builder + * + * @returns {Requested} The result of the stream command + * + */ + request(commandID, commandBuilder) { + try { + for (let i in this.streams) { + if (this.streams[i].running()) { + continue; + } + + return new Requested( + this.streams[i], + this.streams[i].run(commandID, commandBuilder, this.sender), + ); + } + + throw new Exception("No stream is currently available", true); + } catch (e) { + throw new Exception("Stream request has failed: " + e, true); + } + } + + /** + * Send echo request + * + */ + sendEcho() { + let echoHeader = header.header(header.CONTROL), + randomNum = new Uint8Array(common.getRands(8, 0, 255)); + + echoHeader.set(randomNum.length - 1); + + randomNum[0] = echoHeader.value(); + randomNum[1] = header.CONTROL_ECHO; + + this.sender.send(randomNum).then(() => { + if (this.lastEchoTime !== null || this.lastEchoData !== null) { + this.lastEchoTime = null; + this.lastEchoData = null; + + this.config.echoUpdater(ECHO_FAILED); + } + + this.lastEchoTime = new Date(); + this.lastEchoData = randomNum.slice(2, randomNum.length); + }); + } + + /** + * handle received control request + * + * @param {reader.Reader} rd The reader + * + */ + async handleControl(rd) { + let controlType = await reader.readOne(rd), + delay = 0, + echoBytes = null; + + switch (controlType[0]) { + case header.CONTROL_ECHO: + echoBytes = await reader.readCompletely(rd); + + if (this.lastEchoTime === null || this.lastEchoData === null) { + return; + } + + if (this.lastEchoData.length !== echoBytes.length) { + return; + } + + for (let i in this.lastEchoData) { + if (this.lastEchoData[i] == echoBytes[i]) { + continue; + } + + this.lastEchoTime = null; + this.lastEchoData = null; + + this.config.echoUpdater(ECHO_FAILED); + + return; + } + + delay = new Date().getTime() - this.lastEchoTime.getTime(); + + if (delay < 0) { + delay = 0; + } + + this.lastEchoTime = null; + this.lastEchoData = null; + + this.config.echoUpdater(delay); + + return; + } + + await reader.readCompletely(rd); + + throw new Exception("Unknown control signal: " + controlType); + } + + /** + * handle received stream respond + * + * @param {header.Header} hd The header + * @param {reader.Reader} rd The reader + * + * @throws {Exception} when given stream is not running + * + */ + async handleStream(hd, rd) { + if (hd.data() >= this.streams.length) { + return; + } + + let stream = this.streams[hd.data()]; + + if (!stream.running()) { + // WARNING: Connection must be reset at this point because we cannot + // determine how many bytes to read + throw new Exception( + 'Remote is requesting for stream "' + + hd.data() + + '" which is not running', + false, + ); + } + + let initialHeaderBytes = await reader.readN(rd, 2); + + // WARNING: It's the stream's responsibility to ensure stream data is + // completely readed before return + if (stream.initializing()) { + let streamHeader = new header.InitialStream( + initialHeaderBytes[0], + initialHeaderBytes[1], + ); + + return stream.initialize(streamHeader); + } + + let streamHeader = new header.Stream( + initialHeaderBytes[0], + initialHeaderBytes[1], + ), + streamReader = new reader.Limited(rd, streamHeader.length()); + + let tickResult = await stream.tick(streamHeader, streamReader); + + await reader.readCompletely(streamReader); + + return tickResult; + } + + /** + * handle received close respond + * + * @param {header.Header} hd The header + * + * @throws {Exception} when given stream is not running + * + */ + async handleClose(hd) { + if (hd.data() >= this.streams.length) { + return; + } + + let stream = this.streams[hd.data()]; + + if (!stream.running()) { + // WARNING: Connection must be reset at this point because we cannot + // determine how many bytes to read + throw new Exception( + 'Remote is requesting for stream "' + + hd.data() + + '" to be closed, but the stream is not running', + false, + ); + } + + let cResult = await stream.close(); + + let completedHeader = new header.Header(header.COMPLETED); + completedHeader.set(hd.data()); + this.sender.send(new Uint8Array([completedHeader.value()])); + + return cResult; + } + + /** + * handle received close respond + * + * @param {header.Header} hd The header + * + * @throws {Exception} when given stream is not running + * + */ + async handleCompleted(hd) { + if (hd.data() >= this.streams.length) { + return; + } + + let stream = this.streams[hd.data()]; + + if (!stream.running()) { + // WARNING: Connection must be reset at this point because we cannot + // determine how many bytes to read + throw new Exception( + 'Remote is requesting for stream "' + + hd.data() + + '" to be completed, but the stream is not running', + false, + ); + } + + return stream.completed(); + } + + /** + * Main proccess loop + * + * @throws {Exception} when encountered an unknown header + */ + async tick() { + let headerBytes = await reader.readOne(this.reader), + hd = new header.Header(headerBytes[0]); + + switch (hd.type()) { + case header.CONTROL: + return this.handleControl(new reader.Limited(this.reader, hd.data())); + + case header.STREAM: + return this.handleStream(hd, this.reader); + + case header.CLOSE: + return this.handleClose(hd); + + case header.COMPLETED: + return this.handleCompleted(hd); + + default: + throw new Exception("Unknown header", false); + } + } +} diff --git a/ui/stream/streams_test.js b/ui/stream/streams_test.js new file mode 100644 index 0000000..7f8e841 --- /dev/null +++ b/ui/stream/streams_test.js @@ -0,0 +1,20 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +describe("Streams", () => { + it("Header", () => {}); +}); diff --git a/ui/stream/subscribe.js b/ui/stream/subscribe.js new file mode 100644 index 0000000..2e5dd0a --- /dev/null +++ b/ui/stream/subscribe.js @@ -0,0 +1,129 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import Exception from "./exception.js"; + +const typeReject = 0; +const typeResolve = 1; + +export class Subscribe { + /** + * constructor + * + */ + constructor() { + this.res = null; + this.rej = null; + this.pending = []; + this.disabled = null; + } + + /** + * Returns how many resolve/reject in the pending + */ + pendings() { + return ( + this.pending.length + (this.rej !== null || this.res !== null ? 1 : 0) + ); + } + + /** + * Resolve the subscribe waiter + * + * @param {any} d Resolve data which will be send to the subscriber + */ + resolve(d) { + if (this.res !== null) { + this.res(d); + + return; + } + + this.pending.push([typeResolve, d]); + } + + /** + * Reject the subscribe waiter + * + * @param {any} e Error message that will be send to the subscriber + * + */ + reject(e) { + if (this.rej !== null) { + this.rej(e); + + return; + } + + this.pending.push([typeReject, e]); + } + + /** + * Waiting and receive subscribe data + * + * @returns {Promise} Data receiver + * + */ + subscribe() { + if (this.pending.length > 0) { + let p = this.pending.shift(); + + switch (p[0]) { + case typeReject: + throw p[1]; + + case typeResolve: + return p[1]; + + default: + throw new Exception("Unknown pending type", false); + } + } + + if (this.disabled) { + throw new Exception(this.disabled, false); + } + + let self = this; + + return new Promise((resolve, reject) => { + self.res = (d) => { + self.res = null; + self.rej = null; + + resolve(d); + }; + + self.rej = (e) => { + self.res = null; + self.rej = null; + + reject(e); + }; + }); + } + + /** + * Disable current subscriber when all internal data is readed + * + * @param {string} reason Reason of the disable + * + */ + disable(reason) { + this.disabled = reason; + } +} diff --git a/ui/widgets/busy.svg b/ui/widgets/busy.svg new file mode 100644 index 0000000..44d3640 --- /dev/null +++ b/ui/widgets/busy.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ui/widgets/chart.vue b/ui/widgets/chart.vue new file mode 100644 index 0000000..3a8d42d --- /dev/null +++ b/ui/widgets/chart.vue @@ -0,0 +1,316 @@ + + + + + diff --git a/ui/widgets/connect.css b/ui/widgets/connect.css new file mode 100644 index 0000000..8de0f7d --- /dev/null +++ b/ui/widgets/connect.css @@ -0,0 +1,120 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "utf-8"; + +#connect { + z-index: 999999; + top: 40px; + left: 159px; + display: none; + background: #333; + width: 700px; +} + +#connect-frame { + z-index: 0; + position: relative; +} + +#connect .window-frame { + max-height: calc(100vh - 40px); + overflow: auto; +} + +#connect:before { + left: 30px; + background: #333; +} + +@media (max-width: 1024px) { + #connect { + left: 20px; + right: 20px; + width: auto; + } + + #connect:before { + left: 169px; + } +} + +@media (max-width: 768px) { + #connect:before { + left: 149px; + } +} + +#connect.display { + display: block; +} + +#connect h1 { + padding: 15px 15px 0 15px; + margin-bottom: 10px; + color: #999; +} + +#connect-close { + cursor: pointer; + color: #999; + right: 10px; + top: 20px; +} + +#connect-busy-overlay { + z-index: 2; + background: #2229 url("busy.svg") center center no-repeat; + top: 0; + left: 0; + bottom: 0; + right: 0; + position: absolute; + backdrop-filter: blur(1px); +} + +#connect-warning { + padding: 20px; + font-size: 0.85em; + background: #b44; + color: #fff; +} + +#connect-warning-icon { + float: left; + display: block; + margin: 5px 20px 5px 0; +} + +#connect-warning-icon::after { + background: #c55; +} + +#connect-warning-msg { + overflow: auto; +} + +#connect-warning-msg p { + margin: 0 0 5px 0; +} + +#connect-warning-msg a { + color: #faa; + text-decoration: underline; +} diff --git a/ui/widgets/connect.vue b/ui/widgets/connect.vue new file mode 100644 index 0000000..7e6b598 --- /dev/null +++ b/ui/widgets/connect.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/ui/widgets/connect_known.css b/ui/widgets/connect_known.css new file mode 100644 index 0000000..9696ceb --- /dev/null +++ b/ui/widgets/connect_known.css @@ -0,0 +1,245 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "utf-8"; + +#connect-known-list { + min-height: 200px; + font-size: 0.75em; + background: #3a3a3a; + display: flex; + flex-direction: column; + position: relative; +} + +#connect-known-list h3 { + font-size: 1.1em; + color: #999; + margin: 5px 0 15px 0; + text-transform: uppercase; + font-weight: bold; +} + +#connect-known-list.reloaded { +} + +#connect-known-list.reloaded::after { + opacity: 0; + z-index: 2; + content: " "; + display: block; + position: absolute; + width: 100%; + height: 0; + top: -2px; + left: 0; + right: 0; + background: #fff; + animation-name: home-window-display-flash; + animation-duration: 0.5s; + animation-iteration-count: 1; + box-shadow: 0 0 10px #fff; +} + +#connect-known-list-list, +#connect-known-list-empty { + flex: auto; +} + +#connect-known-list-empty { + text-align: center; + color: #999; + font-size: 1.2em; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +#connect-known-list-import { + margin: 15px; + color: #aaa; + font-size: 1.1em; + text-align: center; +} + +#connect-known-list-import a { + color: #e9a; + text-decoration: none; +} + +#connect-known-list-list { + padding: 15px 20px 20px 20px; +} + +#connect-known-list-list li { + width: 50%; + position: relative; +} + +@media (max-width: 768px) { + #connect-known-list-list li { + width: 100%; + } +} + +#connect-known-list-list li > .lst-wrap { + cursor: pointer; +} + +#connect-known-list-list li > .lst-wrap:hover { + background: #444; +} + +#connect-known-list-list li > .labels { + position: absolute; + top: 0; + left: 0; + text-transform: uppercase; + font-size: 0.85em; + letter-spacing: 1px; +} + +#connect-known-list-list li > .labels > .type { + display: inline-block; + padding: 3px; + background: #a56; + color: #fff; +} + +#connect-known-list-list li > .labels > .opt { + display: none; + padding: 3px; + background: #a56; + color: #fff; + text-decoration: none; + z-index: 2; +} + +@media (max-width: 768px) { + #connect-known-list-list li > .labels > .opt { + display: inline-block; + } +} + +#connect-known-list-list li > .labels > .opt.link { + background: #287; + color: #fff; +} + +#connect-known-list-list li > .labels > .opt.del { + background: #a56; + color: #fff; +} + +#connect-known-list-list li > .labels > .opt.clr { + background: #b71; + color: #fff; +} + +#connect-known-list-list li:hover > .labels > .opt, +#connect-known-list-list li:focus > .labels > .opt { + display: inline-block; +} + +#connect-known-list-list li > .lst-wrap > h4 { + font-size: 1.5em; + margin-top: 5px; + margin-bottom: 5px; + text-overflow: ellipsis; + overflow: hidden; +} + +#connect-known-list-list li > .lst-wrap > h4::before { + content: ">_"; + color: #555; + font-size: 0.8em; + margin-right: 5px; + font-weight: normal; + padding: 0 2px; + border-radius: 2px; +} + +#connect-known-list-list li > .lst-wrap > h4.highlight::before { + color: #eee; + background: #555; +} + +#connect-known-list-presets { + margin-top: 10px; + padding: 15px 20px 20px 20px; +} + +#connect-known-list-presets.last-planel { + background: #3f3f3f; +} + +#connect-known-list-presets li { + width: 50%; + position: relative; +} + +#connect-known-list-presets li.disabled { + opacity: 0.5; +} + +@media (max-width: 768px) { + #connect-known-list-presets li { + width: 100%; + } +} + +#connect-known-list-presets li > .lst-wrap { + cursor: pointer; + border-radius: 0 3px 3px 3px; + margin: 12px 10px 10px 0; + padding: 10px; +} + +#connect-known-list-presets li > .lst-wrap > .labels { + position: absolute; + top: 0; + left: 0; + text-transform: uppercase; + font-size: 0.85em; + letter-spacing: 1px; +} + +#connect-known-list-presets li > .lst-wrap > .labels > .type { + display: inline-block; + padding: 3px; + background: #a56; + color: #fff; + border-radius: 3px 3px 3px 0; +} + +#connect-known-list-presets li > .lst-wrap > h4 { + font-size: 1.3em; + text-overflow: ellipsis; + overflow: hidden; +} + +#connect-known-list-presets-alert { + font-size: 1.15em; + color: #fff; + background: #c73; + padding: 10px; + margin-top: 10px; + line-height: 1.5; +} diff --git a/ui/widgets/connect_known.vue b/ui/widgets/connect_known.vue new file mode 100644 index 0000000..4fdc706 --- /dev/null +++ b/ui/widgets/connect_known.vue @@ -0,0 +1,352 @@ + + + + + diff --git a/ui/widgets/connect_new.css b/ui/widgets/connect_new.css new file mode 100644 index 0000000..8770d39 --- /dev/null +++ b/ui/widgets/connect_new.css @@ -0,0 +1,58 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "utf-8"; + +#connect-new { + min-height: 200px; + background: #3a3a3a; + font-size: 0.75em; + padding: 15px; +} + +#connect-new li .lst-wrap:hover { + background: #544; +} + +#connect-new li .lst-wrap:active { + background: #444; +} + +#connect-new li .lst-wrap { + cursor: pointer; + color: #aaa; + padding: 15px; +} + +#connect-new li h2 { + color: #e9a; +} + +#connect-new li h2::before { + content: ">"; + margin: 0 5px 0 0; + color: #555; + font-weight: normal; + transition: ease 0.3s margin; +} + +#connect-new li .lst-wrap:hover h2::before { + content: ">"; + margin: 0 3px 0 2px; +} diff --git a/ui/widgets/connect_new.vue b/ui/widgets/connect_new.vue new file mode 100644 index 0000000..02df628 --- /dev/null +++ b/ui/widgets/connect_new.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/ui/widgets/connect_switch.css b/ui/widgets/connect_switch.css new file mode 100644 index 0000000..aadbe41 --- /dev/null +++ b/ui/widgets/connect_switch.css @@ -0,0 +1,56 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "utf-8"; + +#connect-switch { + font-size: 0.88em; + color: #aaa; + clear: both; + border-color: #555; +} + +#connect-switch li .label { + padding: 2px 7px; + margin-left: 3px; + font-size: 0.85em; + background: #444; + border-radius: 3px; +} + +#connect-switch li.active { + border-color: #555; + background: #3a3a3a; +} + +#connect-switch li.active .label { + background: #888; +} + +#connect-switch li.disabled { + color: #666; +} + +#connect-switch.red { + border-color: #a56; +} + +#connect-switch.red li.active { + border-color: #a56; +} diff --git a/ui/widgets/connect_switch.vue b/ui/widgets/connect_switch.vue new file mode 100644 index 0000000..44812c7 --- /dev/null +++ b/ui/widgets/connect_switch.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/ui/widgets/connecting.svg b/ui/widgets/connecting.svg new file mode 100644 index 0000000..2803fd1 --- /dev/null +++ b/ui/widgets/connecting.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/widgets/connector.css b/ui/widgets/connector.css new file mode 100644 index 0000000..3ace407 --- /dev/null +++ b/ui/widgets/connector.css @@ -0,0 +1,124 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "utf-8"; + +#connector { + padding: 0 20px 40px 20px; +} + +#connector-cancel { + text-decoration: none; + color: #e9a; +} + +#connector-cancel.disabled { + color: #444; +} + +#connector-cancel::before { + content: "\000AB"; + margin-right: 3px; +} + +#connector-title { + margin-top: 10px; + text-align: center; + font-size: 0.9em; + color: #aaa; +} + +#connector-title > h2 { + color: #e9a; + font-size: 1.3em; + font-weight: bold; + margin: 3px 0; +} + +#connector-title.big { + margin: 50px 0; +} + +#connector-title.big > h2 { + margin: 10px 0; +} + +#connector-fields { + margin-top: 10px; + font-size: 0.9em; +} + +#connector-continue { + margin-top: 10px; + font-size: 0.9em; +} + +#connector-proccess { + margin-top: 10px; + text-align: center; + font-size: 0.9em; + color: #aaa; +} + +#connector-proccess-message { + margin: 30px 0; +} + +#connector-proccess-message > h2 { + font-weight: normal; + margin: 10px 0; + color: #e9a; + font-size: 1.2em; +} + +#connector-proccess-message > h2 > span { + padding: 2px 10px; + border: 2px solid transparent; + display: inline-block; +} + +@keyframes connector-proccess-message-alert { + 0% { + border-color: transparent; + } + + 50% { + outline: 2px solid #e9a; + } + + 60% { + border-color: #e9a; + outline: none; + } +} + +#connector-proccess-message.alert > h2 > span { + outline: 2px solid transparent; + animation-name: connector-proccess-message-alert; + animation-duration: 1.5s; + animation-iteration-count: infinite; + animation-direction: normal; + animation-timing-function: steps(1, end); +} + +#connector-proccess-indicater { + width: 100%; + margin: 20px auto; + padding: 0; +} diff --git a/ui/widgets/connector.vue b/ui/widgets/connector.vue new file mode 100644 index 0000000..43fd9fc --- /dev/null +++ b/ui/widgets/connector.vue @@ -0,0 +1,834 @@ + + + + + diff --git a/ui/widgets/connector_field_builder.js b/ui/widgets/connector_field_builder.js new file mode 100644 index 0000000..6ffbea7 --- /dev/null +++ b/ui/widgets/connector_field_builder.js @@ -0,0 +1,222 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +function getTabIndex(tabIndex, field) { + if (field.readonly) { + return 0; + } + + switch (field.type) { + case "text": + case "password": + case "checkbox": + case "textarea": + case "textfile": + case "select": + case "radio": + return tabIndex; + + default: + return 0; + } +} + +export function build(tabIndex, i, field) { + return { + verified: false, + modified: false, + inputted: false, + highlighted: false, + error: "", + message: "", + field: field, + autofocus: tabIndex === 1 && !field.readonly, + tabIndex: getTabIndex(tabIndex, field), + blockedSuggestionValue: "", + blockingSuggestion: false, + nextTabIndex() { + let nextTabIndex = 0; + + if (this.field.readonly) { + nextTabIndex = this.tabIndex; + } else { + switch (this.field.type) { + case "radio": + nextTabIndex = this.tabIndex + this.field.example.split(",").length; + break; + + default: + nextTabIndex = this.tabIndex + 1; + } + } + + if (tabIndex >= nextTabIndex) { + return tabIndex; + } + + return nextTabIndex; + }, + nextSubTabIndex(subIndex) { + if (this.field.readonly) { + return 0; + } + + return this.tabIndex + subIndex; + }, + suggestion: { + selected: -1, + suggestions: [], + orignalValue: "", + orignalValueStored: false, + holding: false, + needsReset: false, + reset() { + this.selected = -1; + this.suggestions = []; + this.holding = false; + this.needsReset = false; + this.clearStored(); + + return true; + }, + softReset() { + if (this.holding) { + this.needsReset = true; + + return false; + } + + return this.reset(); + }, + hold(toHold) { + this.holding = toHold; + + if (this.holding || !this.needsReset) { + return; + } + + this.reset(); + }, + storeOrignal(val) { + if (this.orignalValueStored) { + return; + } + + this.orignalValue = val; + this.orignalValueStored = true; + }, + loadStored(defaultValue) { + return this.orignalValueStored ? this.orignalValue : defaultValue; + }, + clearStored() { + this.orignalValue = ""; + this.orignalValueStored = false; + }, + select(index, fieldValue) { + if (this.selected < 0) { + this.storeOrignal(fieldValue); + } + + if (index < -1 || index >= this.suggestions.length) { + return; + } + + this.selected = index; + }, + cursorUp(fieldValue) { + this.select(this.selected - 1, fieldValue); + }, + cursorDown(fieldValue) { + this.select(this.selected + 1, fieldValue); + }, + cursorMove(toUp, fieldValue) { + toUp ? this.cursorUp(fieldValue) : this.cursorDown(fieldValue); + }, + reload(fieldValue, suggestions) { + this.selected = -1; + this.suggestions = []; + + this.clearStored(); + + if (suggestions.length === 1 && suggestions[0].value === fieldValue) { + return; + } + + for (let v in suggestions) { + this.suggestions.push({ + title: suggestions[v].title, + value: suggestions[v].value, + fields: suggestions[v].meta, + }); + } + }, + current(defaultValue) { + if (this.selected < 0) { + return { + title: "Input", + value: this.loadStored(defaultValue), + fields: {}, + }; + } + + return this.suggestions[this.selected]; + }, + }, + disableSuggestionsForInput(val) { + this.blockedSuggestionValue = val; + this.blockingSuggestion = true; + }, + enableInputSuggestionsOnAllInput() { + this.blockedSuggestionValue = ""; + this.blockingSuggestion = false; + }, + suggestionsPending() { + return this.suggestion.suggestions.length > 0; + }, + reloadSuggestions() { + if ( + this.blockingSuggestion && + this.field.value === this.blockedSuggestionValue + ) { + return; + } + + this.suggestion.reload( + this.field.value, + this.field.suggestions(this.field.value), + ); + }, + resetSuggestions(force) { + return force ? this.suggestion.reset() : this.suggestion.softReset(); + }, + holdSuggestions(toHold) { + this.suggestion.hold(toHold); + }, + moveSuggestionsCursor(toUp) { + this.suggestion.cursorMove(toUp, this.field.value); + }, + selectSuggestion(index) { + this.suggestion.select(index, this.field.value); + }, + curentSuggestion() { + return this.suggestion.current(this.field.value); + }, + selectedSuggestionIndex() { + return this.suggestion.selected; + }, + }; +} diff --git a/ui/widgets/screen_console.css b/ui/widgets/screen_console.css new file mode 100644 index 0000000..fa2f9cf --- /dev/null +++ b/ui/widgets/screen_console.css @@ -0,0 +1,246 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "utf-8"; + +@import "~hack-font/build/web/hack.css"; +@import "~@azurity/pure-nerd-font/pure-nerd-font.css"; + +#connector-resource-preload-control-console { + font-family: PureNerdFont, Hack; +} +#connector-resource-preload-control-console::after { + content: " "; + font-family: PureNerdFont, Hack; + font-weight: bold; +} +#connector-resource-preload-control-console::before { + content: " "; + font-family: PureNerdFont, Hack; + font-style: italic; +} + +#home-content > .screen > .screen-screen > .screen-console { + position: relative; + min-height: 1px; +} + +#home-content > .screen > .screen-screen > .screen-console > .console-toolbar { + position: absolute; + top: 0; + left: 0; + right: 0; + width: 100%; + max-height: 100%; + overflow: auto; + background: #222; + color: #fff; + box-shadow: 0 0 5px #0006; + z-index: 1; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-toolbar + > .console-toolbar-group.console-toolbar-group-left { + border-right: 1px solid #0001; + margin-right: -1px; + float: left; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-toolbar + > .console-toolbar-group.console-toolbar-group-main { + border-left: 1px solid #0001; + overflow: auto; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-toolbar + > .console-toolbar-group + > .console-toolbar-item { + padding: 15px; + float: left; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-toolbar + > .console-toolbar-group + > .console-toolbar-item + .tb-title { + font-size: 0.7em; + text-transform: uppercase; + margin: 0 0 5px 10px; + color: #fff9; + text-shadow: 1px 1px 1px #0005; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-toolbar + > .console-toolbar-group + > .console-toolbar-item + .tb-item { + display: block; + font-size: 0.7em; + padding: 10px; + text-decoration: none; + color: inherit; + border-radius: 3px; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-toolbar + > .console-toolbar-group + > .console-toolbar-item + .tb-item:active { + background: #fff3; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-toolbar + > .console-toolbar-group + > .console-toolbar-item + .tb-item + > .tb-key-icon { + margin: 0 5px; + background: #fff2; + color: #fff; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-toolbar + > .console-toolbar-group + > .console-toolbar-item + .tb-item + > .tb-key-icon.tb-key-resize-icon { + display: block; + padding: 5px; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-toolbar + > .console-toolbar-group + > .console-toolbar-item + .tb-item + > .tb-key-icon:first-child { + margin-left: 0; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-toolbar + > .console-toolbar-group + > .console-toolbar-item + .tb-item + > .tb-key-icon:last-child { + margin-right: 0; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-toolbar + > .console-toolbar-group + > .console-toolbar-item + .tb-item:active + .tb-key-icon { + opacity: 0.5; +} + +#home-content > .screen > .screen-screen > .screen-console > .console-console { + color: #fff; + width: 100%; + height: 100%; + padding: 0; + margin: 0; + position: relative; + z-index: 0; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-console + > .console-loading { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + display: flex; + flex-direction: row; + align-items: center; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-console + > .console-loading + > .console-loading-frame { + text-align: center; + font-size: 1em; + font-weight: lighter; + padding: 20px; + margin: 10px auto; + flex: auto; +} + +#home-content + > .screen + > .screen-screen + > .screen-console + > .console-console + > .console-loading + > .console-loading-frame + > .console-loading-icon { + background: url(./busy.svg) 50% no-repeat; + width: 100%; + height: 100px; +} diff --git a/ui/widgets/screen_console.vue b/ui/widgets/screen_console.vue new file mode 100644 index 0000000..919e28e --- /dev/null +++ b/ui/widgets/screen_console.vue @@ -0,0 +1,578 @@ + + + + + diff --git a/ui/widgets/screen_console_keys.js b/ui/widgets/screen_console_keys.js new file mode 100644 index 0000000..f45b75c --- /dev/null +++ b/ui/widgets/screen_console_keys.js @@ -0,0 +1,677 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Generated by: +// +// +// +// +// KEYBOARDEVENT KEY DUMP + +// + +// + +//
+//
+// + +export const consoleScreenKeys = [ + { + title: "Function Keys", + keys: [ + [ + "F1", + { + altKey: false, + charCode: 0, + code: "F1", + ctrlKey: false, + key: "F1", + keyCode: 112, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 112, + }, + ], + [ + "F2", + { + altKey: false, + charCode: 0, + code: "F2", + ctrlKey: false, + key: "F2", + keyCode: 113, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 113, + }, + ], + [ + "F3", + { + altKey: false, + charCode: 0, + code: "F3", + ctrlKey: false, + key: "F3", + keyCode: 114, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 114, + }, + ], + [ + "F4", + { + altKey: false, + charCode: 0, + code: "F4", + ctrlKey: false, + key: "F4", + keyCode: 115, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 115, + }, + ], + [ + "F5", + { + altKey: false, + charCode: 0, + code: "F5", + ctrlKey: false, + key: "F5", + keyCode: 116, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 116, + }, + ], + [ + "F6", + { + altKey: false, + charCode: 0, + code: "F6", + ctrlKey: false, + key: "F6", + keyCode: 117, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 117, + }, + ], + [ + "F7", + { + altKey: false, + charCode: 0, + code: "F7", + ctrlKey: false, + key: "F7", + keyCode: 118, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 118, + }, + ], + [ + "F8", + { + altKey: false, + charCode: 0, + code: "F8", + ctrlKey: false, + key: "F8", + keyCode: 119, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 119, + }, + ], + [ + "F9", + { + altKey: false, + charCode: 0, + code: "F9", + ctrlKey: false, + key: "F9", + keyCode: 120, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 120, + }, + ], + [ + "F10", + { + altKey: false, + charCode: 0, + code: "F10", + ctrlKey: false, + key: "F10", + keyCode: 121, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 121, + }, + ], + [ + "F11", + { + altKey: false, + charCode: 0, + code: "F11", + ctrlKey: false, + key: "F11", + keyCode: 122, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 122, + }, + ], + [ + "F12", + { + altKey: false, + charCode: 0, + code: "F12", + ctrlKey: false, + key: "F12", + keyCode: 123, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 123, + }, + ], + ], + }, + { + title: "Misc Keys", + keys: [ + [ + "Escape", + { + altKey: false, + charCode: 0, + code: "Escape", + ctrlKey: false, + key: "Escape", + keyCode: 27, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 27, + }, + ], + [ + "Tab", + { + altKey: false, + charCode: 0, + code: "Tab", + ctrlKey: false, + key: "Tab", + keyCode: 9, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 9, + }, + ], + [ + "Insert", + { + altKey: false, + charCode: 0, + code: "Insert", + ctrlKey: false, + key: "Insert", + keyCode: 45, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 45, + }, + ], + [ + "Delete", + { + altKey: false, + charCode: 0, + code: "Delete", + ctrlKey: false, + key: "Delete", + keyCode: 46, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 46, + }, + ], + ], + }, + { + title: "Navigation Keys", + keys: [ + [ + "Home", + { + altKey: false, + charCode: 0, + code: "Home", + ctrlKey: false, + key: "Home", + keyCode: 36, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 36, + }, + ], + [ + "End", + { + altKey: false, + charCode: 0, + code: "End", + ctrlKey: false, + key: "End", + keyCode: 35, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 35, + }, + ], + [ + "Up " + String.fromCharCode(8593), + { + altKey: false, + charCode: 0, + code: "ArrowUp", + ctrlKey: false, + key: "ArrowUp", + keyCode: 38, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 38, + }, + ], + [ + "Down " + String.fromCharCode(8595), + { + altKey: false, + charCode: 0, + code: "ArrowDown", + ctrlKey: false, + key: "ArrowDown", + keyCode: 40, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 40, + }, + ], + [ + "Left " + String.fromCharCode(8592), + { + altKey: false, + charCode: 0, + code: "ArrowLeft", + ctrlKey: false, + key: "ArrowLeft", + keyCode: 37, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 37, + }, + ], + [ + "Right " + String.fromCharCode(8594), + { + altKey: false, + charCode: 0, + code: "ArrowRight", + ctrlKey: false, + key: "ArrowRight", + keyCode: 39, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 39, + }, + ], + [ + "Page Up " + String.fromCharCode(9652), + { + altKey: false, + charCode: 0, + code: "PageUp", + ctrlKey: false, + key: "PageUp", + keyCode: 33, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 33, + }, + ], + [ + "Page Down " + String.fromCharCode(9662), + { + altKey: false, + charCode: 0, + code: "PageDown", + ctrlKey: false, + key: "PageDown", + keyCode: 34, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 34, + }, + ], + ], + }, + { + title: "Control Keys", + keys: [ + [ + "Ctrl+C", + { + altKey: false, + charCode: 0, + code: "KeyC", + ctrlKey: true, + key: "c", + keyCode: 67, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 67, + }, + ], + [ + "Ctrl+Z", + { + altKey: false, + charCode: 0, + code: "KeyZ", + ctrlKey: true, + key: "z", + keyCode: 90, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 90, + }, + ], + [ + "Ctrl+R", + { + altKey: false, + charCode: 0, + code: "KeyR", + ctrlKey: true, + key: "r", + keyCode: 82, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 82, + }, + ], + [ + "Ctrl+L", + { + altKey: false, + charCode: 0, + code: "KeyL", + ctrlKey: true, + key: "l", + keyCode: 76, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 76, + }, + ], + [ + "Ctrl+A", + { + altKey: false, + charCode: 0, + code: "KeyA", + ctrlKey: true, + key: "a", + keyCode: 65, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 65, + }, + ], + [ + "Ctrl+E", + { + altKey: false, + charCode: 0, + code: "KeyE", + ctrlKey: true, + key: "e", + keyCode: 69, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 69, + }, + ], + [ + "Ctrl+W", + { + altKey: false, + charCode: 0, + code: "KeyW", + ctrlKey: true, + key: "w", + keyCode: 87, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 87, + }, + ], + [ + "Ctrl+U", + { + altKey: false, + charCode: 0, + code: "KeyU", + ctrlKey: true, + key: "u", + keyCode: 85, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 85, + }, + ], + [ + "Ctrl+K", + { + altKey: false, + charCode: 0, + code: "KeyK", + ctrlKey: true, + key: "k", + keyCode: 75, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 75, + }, + ], + [ + "Ctrl+D", + { + altKey: false, + charCode: 0, + code: "KeyD", + ctrlKey: true, + key: "d", + keyCode: 68, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 68, + }, + ], + [ + "Ctrl+Q", + { + altKey: false, + charCode: 0, + code: "KeyQ", + ctrlKey: true, + key: "q", + keyCode: 81, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 81, + }, + ], + [ + "Ctrl+X", + { + altKey: false, + charCode: 0, + code: "KeyX", + ctrlKey: true, + key: "x", + keyCode: 88, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 88, + }, + ], + [ + "Ctrl+T", + { + altKey: false, + charCode: 0, + code: "KeyT", + ctrlKey: true, + key: "t", + keyCode: 84, + location: 0, + metaKey: false, + repeat: false, + shiftKey: false, + which: 84, + }, + ], + ], + }, +]; diff --git a/ui/widgets/screens.css b/ui/widgets/screens.css new file mode 100644 index 0000000..82b43d6 --- /dev/null +++ b/ui/widgets/screens.css @@ -0,0 +1,75 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "utf-8"; + +#home-content.active { + min-height: 0; +} + +#home-content > .screen { + display: flex; + justify-content: start; + flex-direction: column; + font-size: 1em; + overflow: hidden; + flex: auto; +} + +#home-content > .screen.screen-inactive { + flex: 0 0 0; +} + +#home-content > .screen > .screen-error { + display: block; + padding: 10px; + background: #b44; + color: #fff; + font-size: 0.75em; + flex: 0 0; +} + +#home-content > .screen > .screen-error.screen-error-level-error { + background: #b44; +} + +#home-content > .screen > .screen-error.screen-error-level-warning { + background: #b82; +} + +#home-content > .screen > .screen-error.screen-error-level-info { + background: #28b; +} + +#home-content > .screen > .screen-screen { + flex: auto; + padding: 0; + margin: 0; + position: relative; + min-height: 0; +} + +#home-content > .screen > .screen-screen > .screen-content { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + position: relative; + overflow: hidden; +} diff --git a/ui/widgets/screens.vue b/ui/widgets/screens.vue new file mode 100644 index 0000000..fa214ad --- /dev/null +++ b/ui/widgets/screens.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/ui/widgets/status.css b/ui/widgets/status.css new file mode 100644 index 0000000..4fbefc8 --- /dev/null +++ b/ui/widgets/status.css @@ -0,0 +1,221 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "utf-8"; + +#conn-status { + z-index: 999999; + top: 40px; + left: 96px; + display: none; + width: 500px; + background: #262626; +} + +#conn-status .window-frame { + max-height: calc(100vh - 40px); + overflow: auto; +} + +#conn-status:before { + left: 30px; +} + +@media (max-width: 768px) { + #conn-status { + left: 20px; + right: 20px; + width: auto; + } + + #conn-status:before { + left: 91px; + } +} + +#conn-status.display { + display: block; +} + +#conn-status h1 { + padding: 15px 15px 10px 15px; + background: #a56; +} + +#conn-status-info { + padding: 0 15px 15px 15px; + font-size: 0.9em; + line-height: 1.5; + background: #a56; +} + +#conn-status.green:before { + background: #287; +} + +#conn-status.green h1 { + color: #6ba; + background: #287; +} + +#conn-status.green #conn-status-info { + background: #287; +} + +#conn-status.yellow:before { + background: #da0; +} + +#conn-status.yellow h1 { + color: #fff; + background: #da0; +} + +#conn-status.yellow #conn-status-info { + background: #da0; +} + +#conn-status.orange:before { + background: #b73; +} + +#conn-status.orange h1 { + color: #fff; + background: #b73; +} + +#conn-status.orange #conn-status-info { + background: #b73; +} + +#conn-status.red:before { + background: #a33; +} + +#conn-status.red h1 { + color: #fff; + background: #a33; +} + +#conn-status.red #conn-status-info { + background: #a33; +} + +.conn-status-chart { +} + +.conn-status-chart > .counters { + width: 100%; + overflow: auto; + margin-bottom: 10px; +} + +.conn-status-chart > .counters > .counter { + width: 50%; + display: block; + float: left; + margin: 10px 0; + text-align: center; +} + +.conn-status-chart > .counters > .counter .name { + font-size: 0.8em; + color: #777; + text-transform: uppercase; + font-weight: bold; + letter-spacing: 1px; +} + +.conn-status-chart > .counters > .counter .value { + font-size: 1.5em; + font-weight: lighter; +} + +.conn-status-chart > .counters > .counter .value span { + font-size: 0.7em; +} + +.conn-status-chart > .chart { + margin: 0 10px; +} + +.conn-status-chart > .chart g { + fill: none; + stroke-width: 7px; + stroke-linecap: round; + stroke-linejoin: round; +} + +.conn-status-chart > .chart .zero { + stroke: #3a3a3a; + stroke-width: 3px; +} + +.conn-status-chart > .chart .expired { + stroke: #3a3a3a; +} + +#conn-status-delay { + padding: 15px 0; + background: #292929; +} + +#conn-status-delay > .counters > .counter { + width: 100%; + float: none; +} + +#conn-status-delay-chart-background { + --color-start: #e43989; + --color-stop: #9a5fca; +} + +#conn-status-delay-chart > g { + fill: none; + stroke: url(#conn-status-delay-chart-background) #2a6; +} + +#conn-status-traffic { + padding: 15px 0; +} + +#conn-status-traffic-chart-in-background { + --color-start: #0287a8; + --color-stop: #06e7b6; +} + +#conn-status-traffic-chart-in > g { + stroke: url(#conn-status-traffic-chart-in-background) #2a6; +} + +#conn-status-traffic-chart-out-background { + --color-start: #e46226; + --color-stop: #da356c; +} + +#conn-status-traffic-chart-out > g { + stroke: url(#conn-status-traffic-chart-out-background) #2a6; +} + +#conn-status-close { + /* ID mainly use for document.getElementById */ + cursor: pointer; + right: 10px; + top: 20px; +} diff --git a/ui/widgets/status.vue b/ui/widgets/status.vue new file mode 100644 index 0000000..a3b1c52 --- /dev/null +++ b/ui/widgets/status.vue @@ -0,0 +1,251 @@ + + + + + diff --git a/ui/widgets/tab_list.vue b/ui/widgets/tab_list.vue new file mode 100644 index 0000000..9cd3b6e --- /dev/null +++ b/ui/widgets/tab_list.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/ui/widgets/tab_window.css b/ui/widgets/tab_window.css new file mode 100644 index 0000000..1fd0970 --- /dev/null +++ b/ui/widgets/tab_window.css @@ -0,0 +1,148 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "utf-8"; + +#tab-window { + z-index: 999999; + top: 40px; + right: 0px; + display: none; + width: 400px; + background: #333; +} + +#tab-window .window-frame { + max-height: calc(100vh - 40px); + overflow: auto; +} + +#tab-window:before { + right: 19px; + background: #333; +} + +@media (max-width: 768px) { + #tab-window { + width: 80%; + } +} + +#tab-window.display { + display: block; +} + +#tab-window-close { + cursor: pointer; + right: 10px; + top: 20px; + color: #999; +} + +#tab-window h1 { + padding: 15px 15px 0 15px; + margin-bottom: 10px; + color: #999; +} + +#tab-window-list > li > .lst-wrap { + padding: 10px 20px; + cursor: pointer; +} + +#tab-window-list > li { + border-bottom: none; +} + +#tab-window-tabs { + flex: auto; + overflow: hidden; +} + +#tab-window-tabs > li { + display: flex; + position: relative; + padding: 15px; + opacity: 0.5; + color: #999; + cursor: pointer; +} + +#tab-window-tabs > li::after { + content: " "; + display: block; + position: absolute; + top: 5px; + bottom: 5px; + left: 0; + width: 0; + transition: all 0.1s linear; + transition-property: width, top, bottom; +} + +#tab-window-tabs > li.active::after { + top: 0; + bottom: 0; +} + +#tab-window-tabs > li.updated::after { + background: #fff3; + width: 5px; +} + +#tab-window-tabs > li.error::after { + background: #d55; + width: 5px; +} + +#tab-window-tabs > li > span.title { + text-overflow: ellipsis; + overflow: hidden; + display: inline-block; +} + +#tab-window-tabs > li > span.title > span.type { + display: inline-block; + font-size: 0.85em; + font-weight: bold; + margin-right: 3px; + text-transform: uppercase; + color: #fff; + background: #222; + padding: 1px 4px; + border-radius: 2px; +} + +#tab-window-tabs > li > .icon-close { + display: block; + position: absolute; + top: 50%; + right: 10px; + margin-top: -5px; + color: #fff6; +} + +#tab-window-tabs > li.active { + color: #fff; + opacity: 1; +} + +#tab-window-tabs > li.active > span.title { + padding-right: 20px; +} diff --git a/ui/widgets/tab_window.vue b/ui/widgets/tab_window.vue new file mode 100644 index 0000000..7c2f945 --- /dev/null +++ b/ui/widgets/tab_window.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/ui/widgets/tabs.vue b/ui/widgets/tabs.vue new file mode 100644 index 0000000..3a80a4f --- /dev/null +++ b/ui/widgets/tabs.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/ui/widgets/window.css b/ui/widgets/window.css new file mode 100644 index 0000000..4de8443 --- /dev/null +++ b/ui/widgets/window.css @@ -0,0 +1,20 @@ +/* +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +*/ + +@charset "utf-8"; diff --git a/ui/widgets/window.vue b/ui/widgets/window.vue new file mode 100644 index 0000000..78d0331 --- /dev/null +++ b/ui/widgets/window.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/ui/xhr.js b/ui/xhr.js new file mode 100644 index 0000000..50d215e --- /dev/null +++ b/ui/xhr.js @@ -0,0 +1,54 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +function send(method, url, headers) { + return new Promise((res, rej) => { + let authReq = new XMLHttpRequest(); + + authReq.addEventListener("readystatechange", () => { + if (authReq.readyState !== authReq.DONE) { + return; + } + + res(authReq); + }); + + authReq.addEventListener("error", (e) => { + rej(e); + }); + + authReq.addEventListener("timeout", (e) => { + rej(e); + }); + + authReq.open(method, url, true); + + for (let h in headers) { + authReq.setRequestHeader(h, headers[h]); + } + + authReq.send(); + }); +} + +export function get(url, headers) { + return send("GET", url, headers); +} + +export function options(url, headers) { + return send("OPTIONS", url, headers); +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..113da91 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,456 @@ +// Sshwifty - A Web SSH client +// +// Copyright (C) 2019-2023 Ni Rui +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const webpack = require("webpack"), + { spawn } = require("child_process"), + path = require("path"), + os = require("os"), + HtmlWebpackPlugin = require("html-webpack-plugin"), + MiniCssExtractPlugin = require("mini-css-extract-plugin"), + CssMinimizerPlugin = require("css-minimizer-webpack-plugin"), + ImageMinimizerPlugin = require("image-minimizer-webpack-plugin"), + { VueLoaderPlugin } = require("vue-loader"), + WebpackFavicons = require("webpack-favicons"), + CopyPlugin = require("copy-webpack-plugin"), + TerserPlugin = require("terser-webpack-plugin"), + { CleanWebpackPlugin } = require("clean-webpack-plugin"), + ESLintPlugin = require("eslint-webpack-plugin"); + +const inDevMode = process.env.NODE_ENV === "development"; + +process.traceDeprecation = true; + +let appSpawnProc = null, + appBuildProc = null; + +const killSpawnProc = (proc, then) => { + if (proc === null) { + then(); + + return; + } + + process.stdout.write("Shutdown application ...\n"); + + process.kill(-proc.proc.pid, "SIGINT"); + + let forceKill = setTimeout(() => { + process.kill(-proc.proc.pid); + }, 3000); + + proc.waiter.then(() => { + clearTimeout(forceKill); + + then(); + }); +}; + +const startAppSpawnProc = (onExit) => { + killSpawnProc(appSpawnProc, () => { + let mEnv = {}; + + for (let i in process.env) { + mEnv[i] = process.env[i]; + } + + mEnv["SSHWIFTY_CONFIG"] = path.join( + __dirname, + "sshwifty.conf.example.json" + ); + + mEnv["SSHWIFTY_DEBUG"] = "_"; + + process.stdout.write("Starting application ...\n"); + + let proc = spawn("go", ["run", "sshwifty.go"], { + env: mEnv, + detached: true, + }), + waiter = new Promise((resolve) => { + let closed = false; + + proc.stdout.on("data", (msg) => { + process.stdout.write(msg.toString()); + }); + + proc.stderr.on("data", (msg) => { + process.stderr.write(msg.toString()); + }); + + proc.on("exit", (n) => { + process.stdout.write("Application process is exited.\n"); + + if (closed) { + return; + } + + closed = true; + + appSpawnProc = null; + resolve(n); + + onExit(); + }); + }); + + appSpawnProc = { + proc, + waiter, + }; + }); +}; + +const startBuildSpawnProc = (onExit) => { + killSpawnProc(appBuildProc, () => { + let mEnv = {}; + + for (let i in process.env) { + mEnv[i] = process.env[i]; + } + + mEnv["NODE_ENV"] = process.env.NODE_ENV; + + process.stdout.write("Generating source code ...\n"); + + let proc = spawn("go", ["generate", "./..."], { + env: mEnv, + detached: true, + }), + waiter = new Promise((resolve) => { + let closed = false; + + proc.stdout.on("data", (msg) => { + process.stdout.write(msg.toString()); + }); + + proc.stderr.on("data", (msg) => { + process.stderr.write(msg.toString()); + }); + + proc.on("exit", (n) => { + process.stdout.write("Code generation process is exited.\n"); + + if (closed) { + return; + } + + closed = true; + + appBuildProc = null; + resolve(n); + + onExit(); + }); + }); + + appBuildProc = { + proc, + waiter, + }; + }); +}; + +const killAllProc = () => { + if (appBuildProc !== null) { + killSpawnProc(appBuildProc, () => { + killSpawnProc(appSpawnProc, () => { + process.exit(0); + }); + }); + + return; + } + + killSpawnProc(appSpawnProc, () => { + process.exit(0); + }); +}; + +process.on("SIGTERM", killAllProc); +process.on("SIGINT", killAllProc); + +module.exports = { + entry: { + app: path.join(__dirname, "ui", "app.js"), + }, + devtool: inDevMode ? "inline-source-map" : "source-map", + output: { + publicPath: "/sshwifty/assets/", + path: path.join(__dirname, ".tmp", "dist"), + filename: "[name]-[contenthash:8].js", + chunkFormat: "array-push", + chunkFilename: "chunk[contenthash:8].js", + assetModuleFilename: "asset[contenthash:8][ext]", + clean: true, + charset: true, + }, + resolve: { + alias: { + vue$: "vue/dist/vue.esm.js", + }, + }, + optimization: { + nodeEnv: process.env.NODE_ENV, + concatenateModules: true, + runtimeChunk: "single", + mergeDuplicateChunks: true, + flagIncludedChunks: true, + providedExports: true, + usedExports: true, + realContentHash: false, + innerGraph: true, + splitChunks: inDevMode + ? false + : { + chunks: "all", + minSize: 20000, + maxSize: 90000, + minRemainingSize: 0, + minChunks: 1, + enforceSizeThreshold: 50000, + name(module, chunks, cacheGroupKey) { + const moduleFileName = module + .identifier() + .split("/") + .reduceRight((item) => item); + const allChunksNames = chunks.map((item) => item.name).join("~"); + return `${cacheGroupKey}~${allChunksNames}~${moduleFileName}`; + }, + cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + priority: -10, + reuseExistingChunk: true, + }, + default: { + priority: -20, + reuseExistingChunk: true, + }, + }, + }, + minimize: !inDevMode, + minimizer: inDevMode + ? [] + : [ + new CssMinimizerPlugin(), + new TerserPlugin({ + test: /\.js(\?.*)?$/i, + terserOptions: { + ecma: undefined, + parse: {}, + compress: {}, + mangle: true, + module: false, + }, + extractComments: /^\**!|@preserve|@license|@cc_on/i, + }), + ], + }, + module: { + rules: [ + { + test: /\.vue$/, + use: "vue-loader", + }, + { + test: /\.css$/, + use: [ + inDevMode ? "vue-style-loader" : MiniCssExtractPlugin.loader, + "css-loader", + ], + }, + { + test: /\.html$/, + use: "html-loader", + }, + { + test: /\.(ico|jpe?g|png|gif|svg|woff2?)$/i, + type: "asset", + }, + { + test: /\.js$/, + exclude: /(node_modules)/, + use: "babel-loader", + }, + ], + }, + plugins: (function () { + var plugins = [ + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 7, + }), + new webpack.optimize.MinChunkSizePlugin({ + minChunkSize: 56000, + }), + new webpack.DefinePlugin( + !inDevMode + ? { + "process.env": { + NODE_ENV: JSON.stringify(process.env.NODE_ENV), + }, + } + : {} + ), + new webpack.LoaderOptionsPlugin({ + options: { + handlebarsLoader: {}, + }, + }), + new webpack.BannerPlugin({ + banner: + "This file is a part of Sshwifty. Automatically " + + "generated at " + + new Date().toTimeString() + + ", DO NOT MODIFIY", + }), + new ESLintPlugin({}), + new CopyPlugin({ + patterns: [ + { + from: path.join(__dirname, "ui", "robots.txt"), + to: path.join(__dirname, ".tmp", "dist"), + }, + { + from: path.join(__dirname, "README.md"), + to: path.join(__dirname, ".tmp", "dist"), + }, + { + from: path.join(__dirname, "DEPENDENCIES.md"), + to: path.join(__dirname, ".tmp", "dist"), + }, + { + from: path.join(__dirname, "LICENSE.md"), + to: path.join(__dirname, ".tmp", "dist"), + }, + ], + }), + new VueLoaderPlugin(), + { + apply(compiler) { + compiler.hooks.afterEmit.tapAsync( + "AfterEmittedPlugin", + (_params, callback) => { + killSpawnProc(appBuildProc, () => { + startBuildSpawnProc(() => { + callback(); + + if (!inDevMode) { + return; + } + + startAppSpawnProc(() => { + process.stdout.write("Application is closed\n"); + }); + }); + }); + } + ); + }, + }, + new WebpackFavicons({ + src: path.join(__dirname, "ui", "sshwifty.svg"), + appName: "Sshwifty SSH Client", + appShortName: "Sshwifty", + appDescription: "Web SSH Client", + developerName: "Ni Rui", + developerURL: "https://nirui.org", + background: "#333", + theme_color: "#333", + appleStatusBarStyle: "black", + display: "standalone", + icons: { + android: { offset: 0, overlayGlow: false, overlayShadow: true }, + appleIcon: { offset: 5, overlayGlow: false }, + appleStartup: { offset: 5, overlayGlow: false }, + coast: false, + favicons: { overlayGlow: false }, + windows: { offset: 5, overlayGlow: false }, + yandex: false, + }, + }), + new HtmlWebpackPlugin({ + inject: true, + template: path.join(__dirname, "ui", "index.html"), + meta: [ + { + name: "description", + content: "Connect to a SSH Server from your web browser", + }, + ], + mobile: true, + lang: "en-US", + inlineManifestWebpackName: "webpackManifest", + title: "Sshwifty Web SSH Client", + minify: { + html5: true, + collapseWhitespace: !inDevMode, + caseSensitive: true, + removeComments: true, + removeEmptyElements: false, + }, + }), + new HtmlWebpackPlugin({ + filename: "error.html", + inject: true, + template: path.join(__dirname, "ui", "error.html"), + meta: [ + { + name: "description", + content: "Connect to a SSH Server from your web browser", + }, + ], + mobile: true, + lang: "en-US", + minify: { + html5: true, + collapseWhitespace: !inDevMode, + caseSensitive: true, + removeComments: true, + removeEmptyElements: false, + }, + }), + new MiniCssExtractPlugin({ + filename: inDevMode ? "[name].css" : "[name]-[contenthash:8].css", + chunkFilename: inDevMode + ? "[name].css" + : "[name]-chunk[contenthash:8].css", + }), + ]; + + if (!inDevMode) { + plugins.push( + new ImageMinimizerPlugin({ + concurrency: os.cpus().length, + minimizer: { + implementation: ImageMinimizerPlugin.imageminMinify, + options: { + plugins: [ + ["imagemin-gifsicle", { interlaced: true }], + ["imagemin-mozjpeg", { progressive: true }], + ["imagemin-pngquant", { quality: [0.02, 0.2] }], + ["imagemin-svgo", { plugins: ["preset-default"] }], + ], + }, + }, + }) + ); + plugins.push(new CleanWebpackPlugin()); + } + + return plugins; + })(), +};