├── .ci.sh ├── .dockerignore ├── .github └── workflows │ └── workflow.yml ├── .gitignore ├── DEPENDENCIES.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── Screenshot.png ├── application ├── application.go ├── command │ ├── commander.go │ ├── commands.go │ ├── fsm.go │ ├── handler.go │ ├── handler_echo_test.go │ ├── handler_stream_test.go │ ├── handler_test.go │ ├── header.go │ ├── hook_exec.go │ ├── hook_exec_command_default.go │ ├── hook_exec_command_unix.go │ ├── hook_exec_command_windows.go │ ├── hook_exec_test.go │ ├── hooks.go │ ├── streams.go │ └── streams_test.go ├── commands │ ├── address.go │ ├── address_test.go │ ├── commands.go │ ├── integer.go │ ├── integer_test.go │ ├── ssh.go │ ├── string.go │ ├── string_test.go │ └── telnet.go ├── configuration │ ├── common.go │ ├── config.go │ ├── loader.go │ ├── loader_direct.go │ ├── loader_enviro.go │ ├── loader_file.go │ ├── loader_redundant.go │ ├── string.go │ └── string_test.go ├── controller │ ├── base.go │ ├── common.go │ ├── common_test.go │ ├── controller.go │ ├── error.go │ ├── failure.go │ ├── home.go │ ├── socket.go │ ├── socket_verify.go │ ├── static.go │ └── static_page_generater │ │ └── main.go ├── log │ ├── ditch.go │ ├── log.go │ ├── writer.go │ └── writer_nodebug.go ├── network │ ├── conn.go │ ├── conn_timeout.go │ ├── dial.go │ ├── dial_ac.go │ └── dial_socks5.go ├── plate.go ├── rw │ ├── fetch.go │ ├── fetch_test.go │ ├── limited.go │ └── rw.go └── server │ ├── conn.go │ └── server.go ├── babel.config.cjs ├── docker-compose.example.yaml ├── eslint.config.mjs ├── go.mod ├── go.sum ├── package-lock.json ├── package.json ├── preset.example.json ├── sshwifty.conf.example.json ├── sshwifty.go ├── ui ├── app.css ├── app.js ├── auth.vue ├── commands │ ├── address.js │ ├── address_test.js │ ├── color.js │ ├── commands.js │ ├── common.js │ ├── common_test.js │ ├── controls.js │ ├── events.js │ ├── exception.js │ ├── history.js │ ├── integer.js │ ├── integer_test.js │ ├── presets.js │ ├── ssh.js │ ├── string.js │ ├── string_test.js │ └── telnet.js ├── common.css ├── control │ ├── ssh.js │ └── telnet.js ├── crypto.js ├── error.html ├── history.js ├── home.css ├── home.vue ├── home_historyctl.js ├── home_socketctl.js ├── index.html ├── landing.css ├── loading.vue ├── robots.txt ├── socket.js ├── sshwifty.svg ├── stream │ ├── common.js │ ├── common_test.js │ ├── exception.js │ ├── header.js │ ├── header_test.js │ ├── reader.js │ ├── reader_test.js │ ├── sender.js │ ├── sender_test.js │ ├── stream.js │ ├── streams.js │ ├── streams_test.js │ └── subscribe.js ├── widgets │ ├── busy.svg │ ├── chart.vue │ ├── connect.css │ ├── connect.vue │ ├── connect_known.css │ ├── connect_known.vue │ ├── connect_new.css │ ├── connect_new.vue │ ├── connect_switch.css │ ├── connect_switch.vue │ ├── connecting.svg │ ├── connector.css │ ├── connector.vue │ ├── connector_field_builder.js │ ├── screen_console.css │ ├── screen_console.vue │ ├── screen_console_keys.js │ ├── screens.css │ ├── screens.vue │ ├── status.css │ ├── status.vue │ ├── tab_list.vue │ ├── tab_window.css │ ├── tab_window.vue │ ├── tabs.vue │ ├── window.css │ └── window.vue └── xhr.js └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | **/* 2 | 3 | !application/* 4 | !ui/* 5 | !*.md 6 | !*.go 7 | !*.sum 8 | !*.mod 9 | !*.json 10 | !*.js 11 | !*.cjs 12 | !*.mjs -------------------------------------------------------------------------------- /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Sshwifty-CI 2 | 3 | on: 4 | push: 5 | branches: [master, dev] 6 | tags: ["**-release"] 7 | pull_request: 8 | 9 | jobs: 10 | CI: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checking out source code 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 20 18 | - name: Setting up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: 'stable' 22 | - name: Run CI 23 | run: | 24 | export GITHUB_USER="${{ github.repository_owner }}" 25 | export GITHUB_USER_TOKEN="${{ secrets.GITHUB_TOKEN }}" 26 | export DOCKER_HUB_USER="${{ secrets.DOCKER_HUB_USERNAME }}" 27 | export DOCKER_HUB_PASSWORD="${{ secrets.DOCKER_HUB_PASSWORD }}" 28 | export COVERALLS_TOKEN="${{ secrets.COVERALLS_TOKEN }}" 29 | 30 | export DOCKER_CUSTOM_COMMAND='echo "GitHub Action Build"' 31 | 32 | sudo apt-get update -y 33 | sudo apt-get upgrade -y 34 | sudo apt-get install libvips libvips-dev -y 35 | 36 | cp ./.ci.sh ./.github-action-ci-script.sh && chmod +x ./.github-action-ci-script.sh && time ./.github-action-ci-script.sh && rm ./.github-action-ci-script.sh 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .tmp/ 3 | node_modules/ 4 | vendor/ 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | *.test 11 | *.out 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | lerna-debug.log* 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | lib-cov 23 | coverage 24 | *.lcov 25 | .nyc_output 26 | typings/ 27 | *.tsbuildinfo 28 | .npm 29 | .eslintcache 30 | .node_repl_history 31 | *.tgz 32 | .yarn-integrity 33 | .env 34 | .env.test 35 | .cache 36 | .next 37 | .nuxt 38 | .vuepress/dist 39 | .serverless/ 40 | .fusebox/ 41 | .dynamodb/ 42 | application/controller/static_pages/ 43 | application/controller/static_pages.go 44 | sshwifty 45 | -------------------------------------------------------------------------------- /DEPENDENCIES.md: -------------------------------------------------------------------------------- 1 | # Dependencies used by Sshwifty 2 | 3 | Sshwifty uses many third-party components. Those components is required in order 4 | for Sshwifty to function. 5 | 6 | A list of used components can be found inside `package.json` and `go.mod` file. 7 | 8 | Major dependencies includes: 9 | 10 | ## For front-end application 11 | 12 | - [Vue](https://vuejs.org), Licensed under MIT license 13 | - [Babel](https://babeljs.io/), Licensed under MIT license 14 | - [XTerm.js](https://xtermjs.org/), Licensed under MIT license 15 | - [normalize.css](https://github.com/necolas/normalize.css), Licensed under MIT license 16 | - [Roboto font](https://en.wikipedia.org/wiki/Roboto), Licensed under Apache license 17 | Packaged by [Christian Hoffmeister](https://github.com/choffmeister/roboto-fontface-bower), Licensed under Apache 2.0 18 | - [iconv-lite](https://github.com/ashtuchkin/iconv-lite), Licensed under MIT license 19 | - [buffer](https://github.com/feross/buffer), Licensed under MIT license 20 | - [fontfaceobserver](https://github.com/bramstein/fontfaceobserver), [View license](https://github.com/bramstein/fontfaceobserver/blob/master/LICENSE) 21 | - [Hack Font](https://github.com/source-foundry/Hack), [View license](https://github.com/source-foundry/Hack/blob/master/LICENSE.md) 22 | - [Nerd Fonts](https://www.nerdfonts.com/), packaged by [@azurity/pure-nerd-font](http://github.com/azurity/pure-nerd-font) 23 | includes icons from following fonts: 24 | - [Powerline Extra Symbols](https://github.com/ryanoasis/powerline-extra-symbols), Licensed under MIT license 25 | - [Font Awesome](https://github.com/FortAwesome/Font-Awesome), [View license](https://github.com/FortAwesome/Font-Awesome/blob/6.x/LICENSE.txt) 26 | - [Font Awesome Extension](https://github.com/AndreLZGava/font-awesome-extension), Licensed under MIT license 27 | - [Material Design Icons](https://github.com/Templarian/MaterialDesign), [View license](https://github.com/Templarian/MaterialDesign/blob/master/LICENSE) 28 | - [Weather Icons](https://github.com/erikflowers/weather-icons), Licensed under SIL OFL 1.1 29 | - [Devicons](https://github.com/vorillaz/devicons), Licensed under MIT license 30 | - [Octicons](https://github.com/primer/octicons), Licensed under MIT license 31 | - [Codicons](https://github.com/microsoft/vscode-codicons), Licensed under MIT License 32 | - [Font Logos (Formerly Font Linux)](https://github.com/Lukas-W/font-logos), Licensed under Unlicense license 33 | - [Pomicons](https://github.com/gabrielelana/pomicons), Licensed under OFL-1.1 license 34 | - ... and more, see [full list](https://github.com/ryanoasis/nerd-fonts/tree/master/src/glyphs) 35 | 36 | ## For back-end application 37 | 38 | - [Go programming language](https://golang.org), [View license](https://github.com/golang/go/blob/master/LICENSE) 39 | - `github.com/gorilla/websocket`, Licensed under BSD-2-Cause license 40 | - `golang.org/x/net/proxy` [View license](https://github.com/golang/net/blob/master/LICENSE) 41 | - `golang.org/x/crypto`, [View license](https://github.com/golang/crypto/blob/master/LICENSE) 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the build base environment 2 | FROM ubuntu:rolling AS base 3 | RUN set -ex && \ 4 | cd / && \ 5 | 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 && \ 6 | 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 && \ 7 | export PATH=$PATH:/ && \ 8 | export DEBIAN_FRONTEND=noninteractive && \ 9 | ([ -z "$HTTP_PROXY" ] || (echo "Acquire::http::Proxy \"$HTTP_PROXY\";" >> /etc/apt/apt.conf)) && \ 10 | ([ -z "$HTTPS_PROXY" ] || (echo "Acquire::https::Proxy \"$HTTPS_PROXY\";" >> /etc/apt/apt.conf)) && \ 11 | (echo "Acquire::Retries \"8\";" >> /etc/apt/apt.conf) && \ 12 | 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 && \ 13 | /try.sh /install.sh && rm /install.sh && \ 14 | /try.sh update-ca-certificates -f && c_rehash && \ 15 | ([ -z "$HTTP_PROXY" ] || (git config --global http.proxy "$HTTP_PROXY" && npm config set proxy "$HTTP_PROXY")) && \ 16 | ([ -z "$HTTPS_PROXY" ] || (git config --global https.proxy "$HTTPS_PROXY" && npm config set https-proxy "$HTTPS_PROXY")) && \ 17 | export PATH=$PATH:"$(go env GOPATH)/bin" && \ 18 | ([ -z "$CUSTOM_COMMAND" ] || (echo "Running custom command: $CUSTOM_COMMAND" && $CUSTOM_COMMAND)) && \ 19 | 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 && \ 20 | git version && \ 21 | go version && \ 22 | npm version 23 | 24 | # Build the base environment for application libraries 25 | FROM base AS libbase 26 | COPY . /tmp/.build/sshwifty 27 | RUN set -ex && \ 28 | cd / && \ 29 | export PATH=$PATH:/ && \ 30 | export DEBIAN_FRONTEND=noninteractive && \ 31 | export CPPFLAGS='-DPNG_ARM_NEON_OPT=0' && \ 32 | /try.sh apt-get install libpng-dev -y && \ 33 | ls -l /tmp/.build/sshwifty && \ 34 | /child.sh \ 35 | "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" \ 36 | 'cd /tmp/.build/sshwifty && /try.sh go mod download' 37 | 38 | # Main building environment 39 | FROM libbase AS builder 40 | RUN set -ex && \ 41 | cd / && \ 42 | export PATH=$PATH:/ && \ 43 | ([ -z "$HTTP_PROXY" ] || (git config --global http.proxy "$HTTP_PROXY" && npm config set proxy "$HTTP_PROXY")) && \ 44 | ([ -z "$HTTPS_PROXY" ] || (git config --global https.proxy "$HTTPS_PROXY" && npm config set https-proxy "$HTTPS_PROXY")) && \ 45 | (cd /tmp/.build/sshwifty && /try.sh npm run build && mv ./sshwifty /) 46 | 47 | # Build the final image for running 48 | FROM alpine:latest 49 | ENV SSHWIFTY_HOSTNAME= \ 50 | SSHWIFTY_SHAREDKEY= \ 51 | SSHWIFTY_DIALTIMEOUT=10 \ 52 | SSHWIFTY_SOCKS5= \ 53 | SSHWIFTY_SOCKS5_USER= \ 54 | SSHWIFTY_SOCKS5_PASSWORD= \ 55 | SSHWIFTY_HOOK_BEFORE_CONNECTING= \ 56 | SSHWIFTY_HOOKTIMEOUT=30 \ 57 | SSHWIFTY_LISTENINTERFACE=0.0.0.0 \ 58 | SSHWIFTY_LISTENPORT=8182 \ 59 | SSHWIFTY_INITIALTIMEOUT=0 \ 60 | SSHWIFTY_READTIMEOUT=0 \ 61 | SSHWIFTY_WRITETIMEOUT=0 \ 62 | SSHWIFTY_HEARTBEATTIMEOUT=0 \ 63 | SSHWIFTY_READDELAY=0 \ 64 | SSHWIFTY_WRITEELAY=0 \ 65 | SSHWIFTY_TLSCERTIFICATEFILE= \ 66 | SSHWIFTY_TLSCERTIFICATEKEYFILE= \ 67 | SSHWIFTY_DOCKER_TLSCERT= \ 68 | SSHWIFTY_DOCKER_TLSCERTKEY= \ 69 | SSHWIFTY_PRESETS= \ 70 | SSHWIFTY_SERVERMESSAGE= \ 71 | SSHWIFTY_ONLYALLOWPRESETREMOTES= 72 | COPY --from=builder /sshwifty / 73 | COPY . /sshwifty-src 74 | RUN set -ex && \ 75 | adduser -D sshwifty && \ 76 | chmod +x /sshwifty && \ 77 | 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 78 | USER sshwifty 79 | EXPOSE 8182 80 | ENTRYPOINT [ "/sshwifty.sh" ] 81 | CMD [] 82 | -------------------------------------------------------------------------------- /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirui/sshwifty/a89be2fe6399adbf8a7a569327eb8072d688d2a6/Screenshot.png -------------------------------------------------------------------------------- /application/command/commander.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package command 19 | 20 | import ( 21 | "io" 22 | "sync" 23 | "time" 24 | 25 | "github.com/nirui/sshwifty/application/log" 26 | "github.com/nirui/sshwifty/application/network" 27 | "github.com/nirui/sshwifty/application/rw" 28 | ) 29 | 30 | // Configuration contains configuration data needed to run command 31 | type Configuration struct { 32 | Dial network.Dial 33 | DialTimeout time.Duration 34 | } 35 | 36 | // Commander command control 37 | type Commander struct { 38 | commands Commands 39 | } 40 | 41 | // New creates a new Commander 42 | func New(cs Commands) Commander { 43 | return Commander{ 44 | commands: cs, 45 | } 46 | } 47 | 48 | // New Adds a new client 49 | func (c Commander) New( 50 | cfg Configuration, 51 | receiver rw.FetchReader, 52 | sender io.Writer, 53 | senderLock *sync.Mutex, 54 | receiveDelay time.Duration, 55 | sendDelay time.Duration, 56 | l log.Logger, 57 | hooks Hooks, 58 | ) (Handler, error) { 59 | return newHandler( 60 | cfg, 61 | &c.commands, 62 | receiver, 63 | sender, 64 | senderLock, 65 | receiveDelay, 66 | sendDelay, 67 | l, 68 | hooks, 69 | ), nil 70 | } 71 | -------------------------------------------------------------------------------- /application/command/commands.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package command 19 | 20 | import ( 21 | "errors" 22 | "fmt" 23 | 24 | "github.com/nirui/sshwifty/application/configuration" 25 | "github.com/nirui/sshwifty/application/log" 26 | ) 27 | 28 | // Consts 29 | const ( 30 | MaxCommandID = 0x0f 31 | ) 32 | 33 | // Errors 34 | var ( 35 | ErrCommandRunUndefinedCommand = errors.New( 36 | "undefined Command") 37 | ) 38 | 39 | // Command represents a command handler machine builder 40 | type Command func( 41 | l log.Logger, 42 | h Hooks, 43 | w StreamResponder, 44 | cfg Configuration, 45 | ) FSMMachine 46 | 47 | // Builder builds a command 48 | type Builder struct { 49 | name string 50 | command Command 51 | configurator configuration.PresetReloader 52 | } 53 | 54 | // Register builds a Builder for registration 55 | func Register(name string, c Command, p configuration.PresetReloader) Builder { 56 | return Builder{ 57 | name: name, 58 | command: c, 59 | configurator: p, 60 | } 61 | } 62 | 63 | // Commands contains data of all commands 64 | type Commands [MaxCommandID + 1]Builder 65 | 66 | // Register registers a new command 67 | func (c *Commands) Register( 68 | id byte, 69 | name string, 70 | cb Command, 71 | ps configuration.PresetReloader, 72 | ) { 73 | if id > MaxCommandID { 74 | panic("Command ID must be not greater than MaxCommandID") 75 | } 76 | 77 | if (*c)[id].command != nil { 78 | panic(fmt.Sprintf("Command %d already been registered", id)) 79 | } 80 | 81 | (*c)[id] = Register(name, cb, ps) 82 | } 83 | 84 | // Run creates command executer 85 | func (c Commands) Run( 86 | id byte, 87 | l log.Logger, 88 | hooks Hooks, 89 | w StreamResponder, 90 | cfg Configuration, 91 | ) (FSM, error) { 92 | if id > MaxCommandID { 93 | return FSM{}, ErrCommandRunUndefinedCommand 94 | } 95 | 96 | cc := c[id] 97 | 98 | if cc.command == nil { 99 | return FSM{}, ErrCommandRunUndefinedCommand 100 | } 101 | 102 | return newFSM(cc.command(l, hooks, w, cfg)), nil 103 | } 104 | 105 | // Reconfigure lets commands reset configuration 106 | func (c Commands) Reconfigure( 107 | p []configuration.Preset, 108 | ) ([]configuration.Preset, error) { 109 | newP := make([]configuration.Preset, 0, len(p)) 110 | 111 | for i := range c { 112 | for pp := range p { 113 | if c[i].name != p[pp].Type { 114 | continue 115 | } 116 | 117 | newPP, pErr := c[i].configurator(p[pp]) 118 | 119 | if pErr == nil { 120 | newP = append(newP, newPP) 121 | 122 | continue 123 | } 124 | 125 | return nil, pErr 126 | } 127 | } 128 | 129 | return newP, nil 130 | } 131 | -------------------------------------------------------------------------------- /application/command/fsm.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package command 19 | 20 | import ( 21 | "errors" 22 | 23 | "github.com/nirui/sshwifty/application/rw" 24 | ) 25 | 26 | // Errors 27 | var ( 28 | ErrFSMMachineClosed = errors.New( 29 | "FSM Machine is already closed, it cannot do anything but be released") 30 | ) 31 | 32 | // FSMError Represents an error from FSM 33 | type FSMError struct { 34 | code StreamError 35 | message string 36 | succeed bool 37 | } 38 | 39 | // ToFSMError converts error to FSMError 40 | func ToFSMError(e error, c StreamError) FSMError { 41 | return FSMError{ 42 | code: c, 43 | message: e.Error(), 44 | succeed: false, 45 | } 46 | } 47 | 48 | // NoFSMError return a FSMError that represents a success operation 49 | func NoFSMError() FSMError { 50 | return FSMError{ 51 | code: 0, 52 | message: "No error", 53 | succeed: true, 54 | } 55 | } 56 | 57 | // Error return the error message 58 | func (e FSMError) Error() string { 59 | return e.message 60 | } 61 | 62 | // Code return the error code 63 | func (e FSMError) Code() StreamError { 64 | return e.code 65 | } 66 | 67 | // Succeed returns whether or not current error represents a succeed operation 68 | func (e FSMError) Succeed() bool { 69 | return e.succeed 70 | } 71 | 72 | // FSMState represents a state of a machine 73 | type FSMState func(f *FSM, r *rw.LimitedReader, h StreamHeader, b []byte) error 74 | 75 | // FSMMachine State machine 76 | type FSMMachine interface { 77 | // Bootup boots up the machine 78 | Bootup(r *rw.LimitedReader, b []byte) (FSMState, FSMError) 79 | 80 | // Close stops the machine and get it ready for release. 81 | // 82 | // NOTE: Close function is responsible in making sure the HeaderClose signal 83 | // is sent before it returns. 84 | // (It may not need to send the header by itself, but it have to 85 | // make sure the header is sent) 86 | Close() error 87 | 88 | // Release shuts the machine down completely and release it's resources 89 | Release() error 90 | } 91 | 92 | // FSM state machine control 93 | type FSM struct { 94 | m FSMMachine 95 | s FSMState 96 | closed bool 97 | } 98 | 99 | // newFSM creates a new FSM 100 | func newFSM(m FSMMachine) FSM { 101 | return FSM{ 102 | m: m, 103 | s: nil, 104 | closed: false, 105 | } 106 | } 107 | 108 | // emptyFSM creates a empty FSM 109 | func emptyFSM() FSM { 110 | return FSM{ 111 | m: nil, 112 | s: nil, 113 | } 114 | } 115 | 116 | // bootup initialize the machine 117 | func (f *FSM) bootup(r *rw.LimitedReader, b []byte) FSMError { 118 | s, err := f.m.Bootup(r, b) 119 | 120 | if s == nil { 121 | panic("FSMState must not be nil") 122 | } 123 | 124 | if !err.Succeed() { 125 | return err 126 | } 127 | 128 | f.s = s 129 | 130 | return err 131 | } 132 | 133 | // running returns whether or not current FSM is running 134 | func (f *FSM) running() bool { 135 | return f.s != nil 136 | } 137 | 138 | // tick ticks current machine 139 | func (f *FSM) tick(r *rw.LimitedReader, h StreamHeader, b []byte) error { 140 | if f.closed { 141 | return ErrFSMMachineClosed 142 | } 143 | 144 | return f.s(f, r, h, b) 145 | } 146 | 147 | // Release shuts down current machine and release it's resource 148 | func (f *FSM) release() error { 149 | f.s = nil 150 | 151 | if !f.closed { 152 | f.close() 153 | } 154 | 155 | rErr := f.m.Release() 156 | 157 | f.m = nil 158 | 159 | if rErr != nil { 160 | return rErr 161 | } 162 | 163 | return nil 164 | } 165 | 166 | // Close stops the machine and get it ready to release 167 | func (f *FSM) close() error { 168 | f.closed = true 169 | 170 | return f.m.Close() 171 | } 172 | 173 | // Switch switch to specificied State for the next tick 174 | func (f *FSM) Switch(s FSMState) { 175 | f.s = s 176 | } 177 | -------------------------------------------------------------------------------- /application/command/handler_echo_test.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package command 19 | 20 | import ( 21 | "bytes" 22 | "io" 23 | "sync" 24 | "testing" 25 | 26 | "github.com/nirui/sshwifty/application/configuration" 27 | "github.com/nirui/sshwifty/application/log" 28 | "github.com/nirui/sshwifty/application/rw" 29 | ) 30 | 31 | func testDummyFetchGen(data []byte) rw.FetchReaderFetcher { 32 | current := 0 33 | 34 | return func() ([]byte, error) { 35 | if current >= len(data) { 36 | return nil, io.EOF 37 | } 38 | 39 | oldCurrent := current 40 | current++ 41 | 42 | return data[oldCurrent:current], nil 43 | } 44 | } 45 | 46 | type dummyWriter struct { 47 | written []byte 48 | } 49 | 50 | func (d *dummyWriter) Write(b []byte) (int, error) { 51 | d.written = append(d.written, b...) 52 | 53 | return len(b), nil 54 | } 55 | 56 | func TestHandlerHandleEcho(t *testing.T) { 57 | w := dummyWriter{ 58 | written: make([]byte, 0, 64), 59 | } 60 | s := []byte{ 61 | byte(HeaderControl | 13), 62 | HeaderControlEcho, 63 | 'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D', '1', 64 | byte(HeaderControl | 13), 65 | HeaderControlEcho, 66 | 'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D', '2', 67 | byte(HeaderControl | HeaderMaxData), 68 | HeaderControlEcho, 69 | '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', 70 | '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', 71 | '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', 72 | '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', 73 | '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', 74 | '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', 75 | '2', '2', 76 | byte(HeaderControl | 13), 77 | HeaderControlEcho, 78 | 'H', 'E', 'L', 'L', 'O', ' ', 'W', 'O', 'R', 'L', 'D', '3', 79 | } 80 | lock := sync.Mutex{} 81 | handler := newHandler( 82 | Configuration{}, 83 | nil, 84 | rw.NewFetchReader(testDummyFetchGen(s)), 85 | &w, 86 | &lock, 87 | 0, 88 | 0, 89 | log.NewDitch(), 90 | NewHooks(configuration.HookSettings{}), 91 | ) 92 | 93 | hErr := handler.Handle() 94 | 95 | if hErr != nil && hErr != io.EOF { 96 | t.Error("Failed to write due to error:", hErr) 97 | 98 | return 99 | } 100 | 101 | if !bytes.Equal(w.written, s) { 102 | t.Errorf("Expecting the data to be %d, got %d instead", s, w.written) 103 | 104 | return 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /application/command/handler_test.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package command 19 | -------------------------------------------------------------------------------- /application/command/header.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package command 19 | 20 | import "fmt" 21 | 22 | // Header Packet Type 23 | type Header byte 24 | 25 | // Packet Types 26 | const ( 27 | // 00------: Control signals 28 | // Remaing bits: Data length 29 | // 30 | // Format: 31 | // 0011111 [63 bytes long data] - 63 bytes of control data 32 | // 33 | HeaderControl Header = 0x00 34 | 35 | // 01------: Bidirectional stream data 36 | // Remaining bits: Stream ID 37 | // Followed by: Parameter or data 38 | // 39 | // Format: 40 | // 0111111 [Command parameters / data] - Open/use stream 63 to execute 41 | // command or transmit data 42 | HeaderStream Header = 0x40 43 | 44 | // 10------: Close stream 45 | // Remaining bits: Stream ID 46 | // 47 | // Format: 48 | // 1011111 - Close stream 63 49 | // 50 | // WARNING: The requester MUST NOT send any data to this stream once this 51 | // header is sent. 52 | // 53 | // WARNING: The receiver MUST reply with a Completed header to indicate 54 | // the success of the Close action. Until a Completed header is 55 | // replied, all data from the sender must be proccessed as normal. 56 | HeaderClose Header = 0x80 57 | 58 | // 11------: Stream has been closed/completed in respond to client request 59 | // Remaining bits: Stream ID 60 | // 61 | // Format: 62 | // 1111111 - Stream 63 is completed 63 | // 64 | // WARNING: This header can ONLY be send in respond to a Close header 65 | // 66 | // WARNING: The sender of this header MUST NOT send any data to the stream 67 | // once this header is sent until this stream been re-opened by a 68 | // Data header 69 | HeaderCompleted Header = 0xc0 70 | ) 71 | 72 | // Control signal types 73 | const ( 74 | HeaderControlEcho = 0x00 75 | HeaderControlPauseStream = 0x01 76 | HeaderControlResumeStream = 0x02 77 | ) 78 | 79 | // Consts 80 | const ( 81 | HeaderMaxData = 0x3f 82 | ) 83 | 84 | // Cutters 85 | const ( 86 | headerHeaderCutter = 0xc0 87 | headerDataCutter = 0x3f 88 | ) 89 | 90 | // Type get packet type 91 | func (p Header) Type() Header { 92 | return (p & headerHeaderCutter) 93 | } 94 | 95 | // Data returns the data of current Packet header 96 | func (p Header) Data() byte { 97 | return byte(p & headerDataCutter) 98 | } 99 | 100 | // Set set a new value of the Header 101 | func (p *Header) Set(data byte) { 102 | if data > headerDataCutter { 103 | panic("data must not be greater than 0x3f") 104 | } 105 | 106 | *p |= (headerDataCutter & Header(data)) 107 | } 108 | 109 | // Set set a new value of the Header 110 | func (p Header) String() string { 111 | switch p.Type() { 112 | case HeaderControl: 113 | return fmt.Sprintf("Control (%d bytes)", p.Data()) 114 | 115 | case HeaderStream: 116 | return fmt.Sprintf("Stream (%d)", p.Data()) 117 | 118 | case HeaderClose: 119 | return fmt.Sprintf("Close (Stream %d)", p.Data()) 120 | 121 | case HeaderCompleted: 122 | return fmt.Sprintf("Completed (Stream %d)", p.Data()) 123 | 124 | default: 125 | return "Unknown" 126 | } 127 | } 128 | 129 | // IsStreamControl returns true when the header is for stream control, false 130 | // when otherwise 131 | func (p Header) IsStreamControl() bool { 132 | switch p { 133 | case HeaderStream: 134 | fallthrough 135 | case HeaderClose: 136 | fallthrough 137 | case HeaderCompleted: 138 | return true 139 | 140 | default: 141 | return false 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /application/command/hook_exec.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | // Predefined prefixes 13 | const ( 14 | EXECHOOK_ENV_EXCLUDE_PREFIX = "SSHWIFTY" 15 | EXECHOOK_ENV_PARAMETER_PREFIX = "SSHWIFTY_HOOK_" 16 | ) 17 | 18 | // isAllowedExecHookEnv returns true when given `env` is allowed to be passed 19 | // to the ExecHook 20 | func isAllowedExecHookEnv(env string) bool { 21 | vs := strings.Index(env, "=") 22 | if vs < 0 { 23 | return false // No "="? not allowed 24 | } 25 | envName := strings.ToUpper(strings.TrimSpace(env[:vs])) 26 | return !strings.HasPrefix( 27 | envName, 28 | EXECHOOK_ENV_EXCLUDE_PREFIX, 29 | ) // Don't leak SSHWIFTY envs 30 | } 31 | 32 | // filterExecHookEnviron modifies `env` so it only contain allowed environment 33 | // variables 34 | func filterExecHookEnviron(envs []string) (c int) { 35 | i := 0 36 | for ; i < len(envs); i++ { 37 | if !isAllowedExecHookEnv(envs[i]) { 38 | continue 39 | } 40 | envs[c] = envs[i] 41 | c++ 42 | } 43 | return c 44 | } 45 | 46 | // buildInitialExecHookEnvirons builds the initial envs for ExecHook instances 47 | func buildInitialExecHookEnvirons() []string { 48 | envs := os.Environ() 49 | return envs[:filterExecHookEnviron(envs)] 50 | } 51 | 52 | // Pre-initialized data needed by ExecHooks 53 | var ( 54 | defaultHookEnvirons = buildInitialExecHookEnvirons() 55 | ) 56 | 57 | // ExecHook launches an external process when invoked 58 | type ExecHook []string 59 | 60 | // NewExecHook creates a new ExecHook out of given `command` 61 | func NewExecHook(command []string) ExecHook { 62 | return ExecHook(command) 63 | } 64 | 65 | // getWorkDir returns current working directory 66 | func (e ExecHook) getWorkDir() (string, error) { 67 | wd, err := os.Getwd() 68 | if err != nil { 69 | return "", fmt.Errorf( 70 | "unable to obtain current working directory which is required "+ 71 | "to execute the hook: %s", 72 | err, 73 | ) 74 | } 75 | return wd, nil 76 | } 77 | 78 | // mergeParametersWithEnvirons adds given `params` into `environs` 79 | func (e ExecHook) mergeParametersWithEnvirons( 80 | params HookParameters, 81 | environs []string, 82 | ) []string { 83 | newEnvs := make([]string, len(environs)+params.Items()) 84 | if copy(newEnvs, environs) != len(environs) { 85 | panic("Not all environ items were copied") 86 | } 87 | params.Iter(func(name, value string) { 88 | newEnvs = append(newEnvs, strings.Join([]string{ 89 | EXECHOOK_ENV_PARAMETER_PREFIX + 90 | strings.ToUpper(strings.ReplaceAll(name, " ", "_")), 91 | value, 92 | }, "=")) 93 | }) 94 | return newEnvs 95 | } 96 | 97 | // Errors for ExecHook.Run 98 | var ( 99 | errExecHookUnspecifiedCommand = errors.New( 100 | "hook command is unspecified") 101 | ) 102 | 103 | // Run implements Hook 104 | func (e ExecHook) Run( 105 | ctx context.Context, 106 | params HookParameters, 107 | output HookOutput, 108 | ) (err error) { 109 | if len(e) <= 0 { 110 | err = errExecHookUnspecifiedCommand 111 | return 112 | } 113 | 114 | cmd, args := e[0], e[1:] 115 | exec := exec.CommandContext(ctx, cmd, args...) 116 | configureExecCommand(exec) 117 | exec.Stdout = HookOutputWriter(output.Out) 118 | exec.Stderr = HookOutputWriter(output.Err) 119 | exec.Env = e.mergeParametersWithEnvirons(params, defaultHookEnvirons) 120 | exec.Dir, err = e.getWorkDir() 121 | if err != nil { 122 | return 123 | } 124 | 125 | err = exec.Run() 126 | if err != nil { 127 | return 128 | } 129 | 130 | // A non-zero exit code should already trigger an error when `exec.Run` is 131 | // returning, but we still guard it here in case the behaver is inconsistent 132 | // across different OSs since we can't test them all 133 | exitCode := exec.ProcessState.ExitCode() 134 | if exitCode != 0 { 135 | err = fmt.Errorf("unsuccessfully exited with code: %d", exitCode) 136 | } 137 | return 138 | } 139 | -------------------------------------------------------------------------------- /application/command/hook_exec_command_default.go: -------------------------------------------------------------------------------- 1 | //go:build !(darwin || dragonfly || freebsd || linux || netbsd || openbsd || windows) 2 | 3 | package command 4 | 5 | import "os/exec" 6 | 7 | // configureExecCommand configures given `e` 8 | func configureExecCommand(e *exec.Cmd) { 9 | // By default, do nothing 10 | } 11 | -------------------------------------------------------------------------------- /application/command/hook_exec_command_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd 2 | 3 | package command 4 | 5 | import ( 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | // configureExecCommand configures given `e` for Unix-like systems 11 | func configureExecCommand(e *exec.Cmd) { 12 | e.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 13 | } 14 | -------------------------------------------------------------------------------- /application/command/hook_exec_command_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package command 4 | 5 | import ( 6 | "os/exec" 7 | "syscall" 8 | ) 9 | 10 | // configureExecCommand configures given `e` for Windows 11 | func configureExecCommand(e *exec.Cmd) { 12 | e.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} 13 | } 14 | -------------------------------------------------------------------------------- /application/command/streams_test.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package command 19 | 20 | import ( 21 | "testing" 22 | ) 23 | 24 | func TestStreamInitialHeader(t *testing.T) { 25 | hd := streamInitialHeader{} 26 | 27 | hd.set(15, 128, true) 28 | 29 | if hd.command() != 15 { 30 | t.Errorf("Expecting command to be %d, got %d instead", 31 | 15, hd.command()) 32 | 33 | return 34 | } 35 | 36 | if hd.data() != 128 { 37 | t.Errorf("Expecting data to be %d, got %d instead", 128, hd.data()) 38 | 39 | return 40 | } 41 | 42 | if hd.success() != true { 43 | t.Errorf("Expecting success to be %v, got %v instead", 44 | true, hd.success()) 45 | 46 | return 47 | } 48 | 49 | hd.set(0, 2047, false) 50 | 51 | if hd.command() != 0 { 52 | t.Errorf("Expecting command to be %d, got %d instead", 53 | 0, hd.command()) 54 | 55 | return 56 | } 57 | 58 | if hd.data() != 2047 { 59 | t.Errorf("Expecting data to be %d, got %d instead", 2047, hd.data()) 60 | 61 | return 62 | } 63 | 64 | if hd.success() != false { 65 | t.Errorf("Expecting success to be %v, got %v instead", 66 | false, hd.success()) 67 | 68 | return 69 | } 70 | } 71 | 72 | func TestStreamHeader(t *testing.T) { 73 | s := StreamHeader{} 74 | 75 | s.Set(StreamHeaderMaxMarker, StreamHeaderMaxLength) 76 | 77 | if s.Marker() != StreamHeaderMaxMarker { 78 | t.Errorf("Expecting the marker to be %d, got %d instead", 79 | StreamHeaderMaxMarker, s.Marker()) 80 | 81 | return 82 | } 83 | 84 | if s.Length() != StreamHeaderMaxLength { 85 | t.Errorf("Expecting the length to be %d, got %d instead", 86 | StreamHeaderMaxLength, s.Length()) 87 | 88 | return 89 | } 90 | 91 | if s[0] != s[1] || s[0] != 0xff { 92 | t.Errorf("Expecting the header to be 255, 255, got %d, %d instead", 93 | s[0], s[1]) 94 | 95 | return 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /application/commands/address_test.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package commands 19 | 20 | import ( 21 | "bytes" 22 | "strings" 23 | "testing" 24 | ) 25 | 26 | func testParseAddress( 27 | t *testing.T, 28 | input []byte, 29 | buf []byte, 30 | expectedType AddressType, 31 | expectedData []byte, 32 | expectedPort uint16, 33 | expectedHostPortString string, 34 | ) { 35 | source := bytes.NewBuffer(input) 36 | addr, addrErr := ParseAddress(source.Read, buf) 37 | 38 | if addrErr != nil { 39 | t.Error("Failed to parse due to error:", addrErr) 40 | 41 | return 42 | } 43 | 44 | if addr.Type() != expectedType { 45 | t.Errorf("Expecting the Type to be %d, got %d instead", 46 | expectedType, addr.Type()) 47 | 48 | return 49 | } 50 | 51 | if !bytes.Equal(addr.Data(), expectedData) { 52 | t.Errorf("Expecting the Data to be %d, got %d instead", 53 | expectedData, addr.Data()) 54 | 55 | return 56 | } 57 | 58 | if addr.Port() != expectedPort { 59 | t.Errorf("Expecting the Port to be %d, got %d instead", 60 | expectedPort, addr.Port()) 61 | 62 | return 63 | } 64 | 65 | if addr.String() != expectedHostPortString { 66 | t.Errorf("Expecting the Host Port string to be \"%s\", "+ 67 | "got \"%s\" instead", 68 | expectedHostPortString, addr.String()) 69 | 70 | return 71 | } 72 | 73 | output := make([]byte, len(input)) 74 | mLen, mErr := addr.Marshal(output) 75 | 76 | if mErr != nil { 77 | t.Error("Failed to marshal due to error:", mErr) 78 | 79 | return 80 | } 81 | 82 | if !bytes.Equal(output[:mLen], input) { 83 | t.Errorf("Expecting marshaled result to be %d, got %d instead", 84 | input, output[:mLen]) 85 | 86 | return 87 | } 88 | } 89 | 90 | func TestParseAddress(t *testing.T) { 91 | testParseAddress( 92 | t, []byte{0x04, 0x1e, 0x00}, make([]byte, 3), LoopbackAddr, nil, 1054, 93 | "localhost:1054") 94 | 95 | testParseAddress( 96 | t, 97 | []byte{ 98 | 0x04, 0x1e, 0x40, 99 | 0x7f, 0x00, 0x00, 0x01, 100 | }, 101 | make([]byte, 4), IPv4Addr, []byte{0x7f, 0x00, 0x00, 0x01}, 1054, 102 | "127.0.0.1:1054") 103 | 104 | testParseAddress( 105 | t, 106 | []byte{ 107 | 0x04, 0x1e, 0x80, 108 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 109 | 0x00, 0x7f, 0x00, 0x00, 0x01, 110 | }, 111 | make([]byte, 16), IPv6Addr, []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 112 | 0x00, 0x00, 0x00, 0x00, 0x00, 113 | 0x00, 0x7f, 0x00, 0x00, 0x01}, 1054, 114 | "[::7f00:1]:1054") 115 | 116 | testParseAddress( 117 | t, 118 | []byte{ 119 | 0x04, 0x1e, 0xff, 120 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 121 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 122 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 123 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 124 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 125 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 126 | '1', '2', '3', 127 | }, 128 | make([]byte, 63), HostNameAddr, []byte{ 129 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 130 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 131 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 132 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 133 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 134 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 135 | '1', '2', '3', 136 | }, 1054, 137 | strings.Repeat("ABCDEFGHIJ", 6)+"123:1054") 138 | } 139 | -------------------------------------------------------------------------------- /application/commands/commands.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package commands 19 | 20 | import ( 21 | "github.com/nirui/sshwifty/application/command" 22 | ) 23 | 24 | // New creates a new commands group 25 | func New() command.Commands { 26 | return command.Commands{ 27 | command.Register("Telnet", newTelnet, parseTelnetConfig), 28 | command.Register("SSH", newSSH, parseSSHConfig), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /application/commands/integer.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package commands 19 | 20 | import ( 21 | "errors" 22 | 23 | "github.com/nirui/sshwifty/application/rw" 24 | ) 25 | 26 | // Errors 27 | var ( 28 | ErrIntegerMarshalNotEnoughBuffer = errors.New( 29 | "not enough buffer to marshal the integer") 30 | 31 | ErrIntegerMarshalTooLarge = errors.New( 32 | "integer cannot be marshalled, because the vaule was too large") 33 | ) 34 | 35 | // Integer is a 16bit unsigned integer data 36 | // 37 | // Format: 38 | // +-------------------------------------+--------------+ 39 | // | 1 bit | 7 bits | 40 | // +-------------------------------------+--------------+ 41 | // | 1 when current byte is the end byte | Integer data | 42 | // +-------------------------------------+--------------+ 43 | // 44 | // Example: 45 | // - 00000000 00000000: 0 46 | // - 01111111: 127 47 | // - 11111111 01000000: 255 48 | type Integer uint16 49 | 50 | const ( 51 | integerHasNextBit = 0x80 52 | integerValueCutter = 0x7f 53 | ) 54 | 55 | // Consts 56 | const ( 57 | MaxInteger = 0x3fff 58 | MaxIntegerBytes = 2 59 | ) 60 | 61 | // ByteSize returns how many bytes current integer will be encoded into 62 | func (i *Integer) ByteSize() int { 63 | if *i > integerValueCutter { 64 | return 2 65 | } 66 | 67 | return 1 68 | } 69 | 70 | // Int returns a int of current Integer 71 | func (i *Integer) Int() int { 72 | return int(*i) 73 | } 74 | 75 | // Marshal build serialized data of the integer 76 | func (i *Integer) Marshal(b []byte) (int, error) { 77 | bLen := len(b) 78 | 79 | if *i > MaxInteger { 80 | return 0, ErrIntegerMarshalTooLarge 81 | } 82 | 83 | if bLen < i.ByteSize() { 84 | return 0, ErrIntegerMarshalNotEnoughBuffer 85 | } 86 | 87 | if *i <= integerValueCutter { 88 | b[0] = byte(*i & integerValueCutter) 89 | 90 | return 1, nil 91 | } 92 | 93 | b[0] = byte((*i >> 7) | integerHasNextBit) 94 | b[1] = byte(*i & integerValueCutter) 95 | 96 | return 2, nil 97 | } 98 | 99 | // Unmarshal read data and parse the integer 100 | func (i *Integer) Unmarshal(reader rw.ReaderFunc) error { 101 | buf := [1]byte{} 102 | 103 | for j := 0; j < MaxIntegerBytes; j++ { 104 | _, rErr := rw.ReadFull(reader, buf[:]) 105 | 106 | if rErr != nil { 107 | return rErr 108 | } 109 | 110 | *i |= Integer(buf[0] & integerValueCutter) 111 | 112 | if integerHasNextBit&buf[0] == 0 { 113 | return nil 114 | } 115 | 116 | *i <<= 7 117 | } 118 | 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /application/commands/integer_test.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package commands 19 | 20 | import ( 21 | "bytes" 22 | "testing" 23 | ) 24 | 25 | func TestInteger(t *testing.T) { 26 | ii := Integer(0x3fff) 27 | result := Integer(0) 28 | buf := make([]byte, 2) 29 | 30 | mLen, mErr := ii.Marshal(buf) 31 | 32 | if mErr != nil { 33 | t.Error("Failed to marshal:", mErr) 34 | 35 | return 36 | } 37 | 38 | mData := bytes.NewBuffer(buf[:mLen]) 39 | mErr = result.Unmarshal(mData.Read) 40 | 41 | if mErr != nil { 42 | t.Error("Failed to unmarshal:", mErr) 43 | 44 | return 45 | } 46 | 47 | if result != ii { 48 | t.Errorf("Expecting result to be %d, got %d instead", ii, result) 49 | 50 | return 51 | } 52 | } 53 | 54 | func TestIntegerSingleByte1(t *testing.T) { 55 | ii := Integer(102) 56 | result := Integer(0) 57 | buf := make([]byte, 2) 58 | 59 | mLen, mErr := ii.Marshal(buf) 60 | 61 | if mErr != nil { 62 | t.Error("Failed to marshal:", mErr) 63 | 64 | return 65 | } 66 | 67 | if mLen != 1 { 68 | t.Errorf("Expecting the Integer to be marshalled into %d bytes, got "+ 69 | "%d instead", 1, mLen) 70 | 71 | return 72 | } 73 | 74 | mData := bytes.NewBuffer(buf[:mLen]) 75 | mErr = result.Unmarshal(mData.Read) 76 | 77 | if mErr != nil { 78 | t.Error("Failed to unmarshal:", mErr) 79 | 80 | return 81 | } 82 | 83 | if result != ii { 84 | t.Errorf("Expecting result to be %d, got %d instead", ii, result) 85 | 86 | return 87 | } 88 | } 89 | 90 | func TestIntegerSingleByte2(t *testing.T) { 91 | ii := Integer(127) 92 | result := Integer(0) 93 | buf := make([]byte, 2) 94 | 95 | mLen, mErr := ii.Marshal(buf) 96 | 97 | if mErr != nil { 98 | t.Error("Failed to marshal:", mErr) 99 | 100 | return 101 | } 102 | 103 | if mLen != 1 { 104 | t.Errorf("Expecting the Integer to be marshalled into %d bytes, got "+ 105 | "%d instead", 1, mLen) 106 | 107 | return 108 | } 109 | 110 | mData := bytes.NewBuffer(buf[:mLen]) 111 | mErr = result.Unmarshal(mData.Read) 112 | 113 | if mErr != nil { 114 | t.Error("Failed to unmarshal:", mErr) 115 | 116 | return 117 | } 118 | 119 | if result != ii { 120 | t.Errorf("Expecting result to be %d, got %d instead", ii, result) 121 | 122 | return 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /application/commands/string.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package commands 19 | 20 | import ( 21 | "errors" 22 | 23 | "github.com/nirui/sshwifty/application/rw" 24 | ) 25 | 26 | // Errors 27 | var ( 28 | ErrStringParseBufferTooSmall = errors.New( 29 | "not enough buffer space to parse given string") 30 | 31 | ErrStringMarshalBufferTooSmall = errors.New( 32 | "not enough buffer space to marshal given string") 33 | ) 34 | 35 | // String data 36 | type String struct { 37 | len Integer 38 | data []byte 39 | } 40 | 41 | // ParseString build the String according to readed data 42 | func ParseString(reader rw.ReaderFunc, b []byte) (String, error) { 43 | lenData := Integer(0) 44 | 45 | mErr := lenData.Unmarshal(reader) 46 | 47 | if mErr != nil { 48 | return String{}, mErr 49 | } 50 | 51 | bLen := len(b) 52 | 53 | if bLen < lenData.Int() { 54 | return String{}, ErrStringParseBufferTooSmall 55 | } 56 | 57 | _, rErr := rw.ReadFull(reader, b[:lenData]) 58 | 59 | if rErr != nil { 60 | return String{}, rErr 61 | } 62 | 63 | return String{ 64 | len: lenData, 65 | data: b[:lenData], 66 | }, nil 67 | } 68 | 69 | // NewString create a new String 70 | func NewString(d []byte) String { 71 | dLen := len(d) 72 | 73 | if dLen > MaxInteger { 74 | panic("Data was too long for a String") 75 | } 76 | 77 | return String{ 78 | len: Integer(dLen), 79 | data: d, 80 | } 81 | } 82 | 83 | // Data returns the data of the string 84 | func (s String) Data() []byte { 85 | return s.data 86 | } 87 | 88 | // Marshal the string to give buffer 89 | func (s String) Marshal(b []byte) (int, error) { 90 | bLen := len(b) 91 | 92 | if bLen < s.len.ByteSize()+len(s.data) { 93 | return 0, ErrStringMarshalBufferTooSmall 94 | } 95 | 96 | mLen, mErr := s.len.Marshal(b) 97 | 98 | if mErr != nil { 99 | return 0, mErr 100 | } 101 | 102 | return copy(b[mLen:], s.data) + mLen, nil 103 | } 104 | -------------------------------------------------------------------------------- /application/commands/string_test.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package commands 19 | 20 | import ( 21 | "bytes" 22 | "testing" 23 | ) 24 | 25 | func testString(t *testing.T, str []byte) { 26 | ss := NewString(str) 27 | mm := make([]byte, len(str)+2) 28 | 29 | mLen, mErr := ss.Marshal(mm) 30 | 31 | if mErr != nil { 32 | t.Error("Failed to marshal:", mErr) 33 | 34 | return 35 | } 36 | 37 | buf := make([]byte, mLen) 38 | source := bytes.NewBuffer(mm[:mLen]) 39 | result, rErr := ParseString(source.Read, buf) 40 | 41 | if rErr != nil { 42 | t.Error("Failed to parse:", rErr) 43 | 44 | return 45 | } 46 | 47 | if !bytes.Equal(result.Data(), ss.Data()) { 48 | t.Errorf("Expecting the data to be %d, got %d instead", 49 | ss.Data(), result.Data()) 50 | 51 | return 52 | } 53 | } 54 | 55 | func TestString(t *testing.T) { 56 | testString(t, []byte{ 57 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 58 | }) 59 | 60 | testString(t, []byte{ 61 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 62 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 63 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 64 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 65 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 66 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 67 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 68 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 69 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 70 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 71 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 72 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 73 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 74 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 75 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 76 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 77 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 78 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 79 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 80 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 81 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'i', 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /application/configuration/common.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package configuration 19 | 20 | func durationAtLeast(current, min int) int { 21 | if current > min { 22 | return current 23 | } 24 | 25 | return min 26 | } 27 | -------------------------------------------------------------------------------- /application/configuration/loader.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package configuration 19 | 20 | import ( 21 | "github.com/nirui/sshwifty/application/log" 22 | ) 23 | 24 | // PresetReloader reloads preset 25 | type PresetReloader func(p Preset) (Preset, error) 26 | 27 | // Loader Configuration loader 28 | type Loader func(log log.Logger) (name string, cfg Configuration, err error) 29 | -------------------------------------------------------------------------------- /application/configuration/loader_direct.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package configuration 19 | 20 | import ( 21 | "github.com/nirui/sshwifty/application/log" 22 | ) 23 | 24 | const ( 25 | directTypeName = "Direct" 26 | ) 27 | 28 | // Direct creates a loader that return raw configuration data directly. 29 | // Good for integration. 30 | func Direct(cfg Configuration) Loader { 31 | return func(log log.Logger) (string, Configuration, error) { 32 | return directTypeName, cfg, nil 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /application/configuration/loader_redundant.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package configuration 19 | 20 | import ( 21 | "fmt" 22 | 23 | "github.com/nirui/sshwifty/application/log" 24 | ) 25 | 26 | const ( 27 | redundantTypeName = "Redundant" 28 | ) 29 | 30 | // Redundant creates a group of loaders. They will be executed one by one until 31 | // one of it successfully returned a configuration 32 | func Redundant(loaders ...Loader) Loader { 33 | return func(log log.Logger) (string, Configuration, error) { 34 | ll := log.Context("Redundant") 35 | 36 | for i := range loaders { 37 | lLoaderName, lCfg, lErr := loaders[i](ll) 38 | 39 | if lErr != nil { 40 | ll.Warning("Unable to load configuration from \"%s\": %s", 41 | lLoaderName, lErr) 42 | 43 | continue 44 | } 45 | 46 | return lLoaderName, lCfg, nil 47 | } 48 | 49 | return redundantTypeName, Configuration{}, fmt.Errorf( 50 | "all existing redundant loader has failed") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /application/configuration/string.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package configuration 19 | 20 | import ( 21 | "fmt" 22 | "io/ioutil" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | ) 27 | 28 | // String represents a config string 29 | type String string 30 | 31 | // Parse parses current string and return the parsed result 32 | func (s String) Parse() (string, error) { 33 | ss := string(s) 34 | 35 | sSchemeLeadIdx := strings.Index(ss, "://") 36 | 37 | if sSchemeLeadIdx < 0 { 38 | return ss, nil 39 | } 40 | 41 | sSchemeLeadEnd := sSchemeLeadIdx + 3 42 | 43 | switch strings.ToLower(ss[:sSchemeLeadIdx]) { 44 | case "file": 45 | fPath, e := filepath.Abs(ss[sSchemeLeadEnd:]) 46 | 47 | if e != nil { 48 | return ss, e 49 | } 50 | 51 | f, e := os.Open(fPath) 52 | 53 | if e != nil { 54 | return "", fmt.Errorf("unable to open %s: %s", fPath, e) 55 | } 56 | 57 | defer f.Close() 58 | 59 | fData, e := ioutil.ReadAll(f) 60 | 61 | if e != nil { 62 | return "", fmt.Errorf("unable to read from %s: %s", fPath, e) 63 | } 64 | 65 | return string(fData), nil 66 | 67 | case "enviroment": // You see what I did there. Remove this a later 68 | fallthrough 69 | case "environment": 70 | return os.Getenv(ss[sSchemeLeadEnd:]), nil 71 | 72 | case "literal": 73 | return ss[sSchemeLeadEnd:], nil 74 | 75 | default: 76 | return "", fmt.Errorf( 77 | "scheme \"%s\" was unsupported", ss[:sSchemeLeadIdx]) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /application/configuration/string_test.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package configuration 19 | 20 | import ( 21 | "os" 22 | "testing" 23 | ) 24 | 25 | func TestStringString(t *testing.T) { 26 | ss := String("aaaaaaaaaaaaa") 27 | 28 | result, err := ss.Parse() 29 | 30 | if err != nil { 31 | t.Error("Unable to parse:", err) 32 | 33 | return 34 | } 35 | 36 | if result != "aaaaaaaaaaaaa" { 37 | t.Errorf( 38 | "Expecting the result to be %s, got %s instead", 39 | "aaaaaaaaaaaaa", 40 | result, 41 | ) 42 | 43 | return 44 | } 45 | } 46 | 47 | func TestStringFile(t *testing.T) { 48 | const testFilename = "sshwifty.configuration.test.string.file.tmp" 49 | 50 | filePath := os.TempDir() + string(os.PathSeparator) + testFilename 51 | 52 | f, err := os.Create(filePath) 53 | 54 | if err != nil { 55 | t.Error("Unable to create file:", err) 56 | 57 | return 58 | } 59 | 60 | defer os.Remove(filePath) 61 | 62 | f.WriteString("TestAAAA") 63 | f.Close() 64 | 65 | ss := String("file://" + filePath) 66 | 67 | result, err := ss.Parse() 68 | 69 | if err != nil { 70 | t.Error("Unable to parse:", err) 71 | 72 | return 73 | } 74 | 75 | if result != "TestAAAA" { 76 | t.Errorf( 77 | "Expecting the result to be %s, got %s instead", 78 | "TestAAAA", 79 | result, 80 | ) 81 | 82 | return 83 | } 84 | 85 | ss = String("file://" + filePath + ".notexist") 86 | 87 | _, err = ss.Parse() 88 | 89 | if err == nil { 90 | t.Error("Parsing an non-existing file should result an error") 91 | 92 | return 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /application/controller/base.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package controller 19 | 20 | import ( 21 | "net/http" 22 | "strings" 23 | 24 | "github.com/nirui/sshwifty/application/log" 25 | ) 26 | 27 | // Error 28 | var ( 29 | ErrControllerNotImplemented = NewError( 30 | http.StatusNotImplemented, "Server does not know how to handle the "+ 31 | "request") 32 | ) 33 | 34 | type controller interface { 35 | Get(w http.ResponseWriter, r *http.Request, l log.Logger) error 36 | Head(w http.ResponseWriter, r *http.Request, l log.Logger) error 37 | Post(w http.ResponseWriter, r *http.Request, l log.Logger) error 38 | Put(w http.ResponseWriter, r *http.Request, l log.Logger) error 39 | Delete(w http.ResponseWriter, r *http.Request, l log.Logger) error 40 | Connect(w http.ResponseWriter, r *http.Request, l log.Logger) error 41 | Options(w http.ResponseWriter, r *http.Request, l log.Logger) error 42 | Trace(w http.ResponseWriter, r *http.Request, l log.Logger) error 43 | Patch(w http.ResponseWriter, r *http.Request, l log.Logger) error 44 | Other( 45 | method string, 46 | w http.ResponseWriter, 47 | r *http.Request, 48 | l log.Logger, 49 | ) error 50 | } 51 | 52 | type baseController struct{} 53 | 54 | func (b baseController) Get( 55 | w http.ResponseWriter, r *http.Request, l log.Logger) error { 56 | return ErrControllerNotImplemented 57 | } 58 | 59 | func (b baseController) Head( 60 | w http.ResponseWriter, r *http.Request, l log.Logger) error { 61 | return ErrControllerNotImplemented 62 | } 63 | 64 | func (b baseController) Post( 65 | w http.ResponseWriter, r *http.Request, l log.Logger) error { 66 | return ErrControllerNotImplemented 67 | } 68 | 69 | func (b baseController) Put( 70 | w http.ResponseWriter, r *http.Request, l log.Logger) error { 71 | return ErrControllerNotImplemented 72 | } 73 | 74 | func (b baseController) Delete( 75 | w http.ResponseWriter, r *http.Request, l log.Logger) error { 76 | return ErrControllerNotImplemented 77 | } 78 | 79 | func (b baseController) Connect( 80 | w http.ResponseWriter, r *http.Request, l log.Logger) error { 81 | return ErrControllerNotImplemented 82 | } 83 | 84 | func (b baseController) Options( 85 | w http.ResponseWriter, r *http.Request, l log.Logger) error { 86 | return ErrControllerNotImplemented 87 | } 88 | 89 | func (b baseController) Trace( 90 | w http.ResponseWriter, r *http.Request, l log.Logger) error { 91 | return ErrControllerNotImplemented 92 | } 93 | 94 | func (b baseController) Patch( 95 | w http.ResponseWriter, r *http.Request, l log.Logger) error { 96 | return ErrControllerNotImplemented 97 | } 98 | 99 | func (b baseController) Other( 100 | method string, w http.ResponseWriter, r *http.Request, l log.Logger) error { 101 | return ErrControllerNotImplemented 102 | } 103 | 104 | func serveController( 105 | c controller, 106 | w http.ResponseWriter, 107 | r *http.Request, 108 | l log.Logger, 109 | ) error { 110 | switch strings.ToUpper(r.Method) { 111 | case "GET": 112 | return c.Get(w, r, l) 113 | case "HEAD": 114 | return c.Head(w, r, l) 115 | case "POST": 116 | return c.Post(w, r, l) 117 | case "PUT": 118 | return c.Put(w, r, l) 119 | case "DELETE": 120 | return c.Delete(w, r, l) 121 | case "CONNECT": 122 | return c.Connect(w, r, l) 123 | case "OPTIONS": 124 | return c.Options(w, r, l) 125 | case "TRACE": 126 | return c.Trace(w, r, l) 127 | case "PATCH": 128 | return c.Patch(w, r, l) 129 | default: 130 | return c.Other(r.Method, w, r, l) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /application/controller/common.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package controller 19 | 20 | import ( 21 | "net/http" 22 | "regexp" 23 | "strings" 24 | ) 25 | 26 | func clientSupportGZIP(r *http.Request) bool { 27 | // Should be good enough 28 | return strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") 29 | } 30 | 31 | var ( 32 | serverMessageFormatLink = regexp.MustCompile(`\[(.*?)\]\((.*?)\)`) 33 | ) 34 | 35 | func parseServerMessage(input string) (result string) { 36 | // Yep, this is a new low, throwing regexp at a flat text format now...will 37 | // rewrite the entire thing in a new version with a proper parser, maybe 38 | // Con: Barely work when we only need to support exactly one text format 39 | // Pro: Expecting a debugging battle, wrote the thing in one go instead 40 | found := serverMessageFormatLink.FindAllStringSubmatchIndex(input, -1) 41 | if len(found) <= 0 { 42 | return input 43 | } 44 | currentStart := 0 45 | for _, f := range found { 46 | if len(f) != 6 { // Expecting 6 parameters from the given expression 47 | return input 48 | } 49 | segStart, segEnd, titleStart, titleEnd, linkStart, linkEnd := 50 | f[0], f[1], f[2], f[3], f[4], f[5] 51 | result += input[currentStart:segStart] 52 | result += "" + 55 | input[titleStart:titleEnd] + 56 | "" 57 | currentStart = segEnd 58 | } 59 | result += input[currentStart:] 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /application/controller/common_test.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package controller 19 | 20 | import ( 21 | "html" 22 | "testing" 23 | ) 24 | 25 | func TestParseServerMessage(t *testing.T) { 26 | for _, test := range [][]string{ 27 | { 28 | "This is a [测试](http://nirui.org) " + 29 | "[for link support](http://nirui.org).", 30 | "<b>This is a " + 31 | "测试 " + 32 | "for link support" + 33 | "</b>.", 34 | }, 35 | { 36 | "[测试](http://nirui.org)", 37 | "测试", 38 | }, 39 | { 40 | "[测试](http://nirui.org).", 41 | "测试.", 42 | }, 43 | { 44 | ".[测试](http://nirui.org)", 45 | ".测试", 46 | }, 47 | } { 48 | result := parseServerMessage(html.EscapeString(test[0])) 49 | if result != test[1] { 50 | t.Errorf("Expecting %v, got %v instead", test[1], result) 51 | return 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /application/controller/controller.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package controller 19 | 20 | import ( 21 | "net/http" 22 | "strings" 23 | "time" 24 | 25 | "github.com/nirui/sshwifty/application/command" 26 | "github.com/nirui/sshwifty/application/configuration" 27 | "github.com/nirui/sshwifty/application/log" 28 | "github.com/nirui/sshwifty/application/server" 29 | ) 30 | 31 | // Errors 32 | var ( 33 | ErrNotFound = NewError( 34 | http.StatusNotFound, "Page not found") 35 | ) 36 | 37 | const ( 38 | assetsURLPrefix = "/sshwifty/assets/" 39 | assetsURLPrefixLen = len(assetsURLPrefix) 40 | ) 41 | 42 | // handler is the main service dispatcher 43 | type handler struct { 44 | hostNameChecker string 45 | commonCfg configuration.Common 46 | logger log.Logger 47 | homeCtl home 48 | socketCtl socket 49 | socketVerifyCtl socketVerification 50 | } 51 | 52 | func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 53 | var err error 54 | 55 | clientLogger := h.logger.Context("Client (%s)", r.RemoteAddr) 56 | 57 | if len(h.commonCfg.HostName) > 0 { 58 | hostPort := r.Host 59 | 60 | if len(hostPort) <= 0 { 61 | hostPort = r.URL.Host 62 | } 63 | 64 | if h.commonCfg.HostName != hostPort && 65 | !strings.HasPrefix(hostPort, h.hostNameChecker) { 66 | clientLogger.Warning("Requested invalid host \"%s\", denied access", 67 | r.Host) 68 | 69 | serveFailure( 70 | NewError(http.StatusForbidden, "Invalid host"), w, r, h.logger) 71 | 72 | return 73 | } 74 | } 75 | 76 | w.Header().Add("Date", time.Now().UTC().Format(time.RFC1123)) 77 | 78 | switch r.URL.Path { 79 | case "/": 80 | err = serveController(h.homeCtl, w, r, clientLogger) 81 | 82 | case "/sshwifty/socket": 83 | err = serveController(h.socketCtl, w, r, clientLogger) 84 | case "/sshwifty/socket/verify": 85 | err = serveController(h.socketVerifyCtl, w, r, clientLogger) 86 | 87 | case "/robots.txt": 88 | err = serveStaticCacheData( 89 | "robots.txt", 90 | staticFileExt(".txt"), 91 | w, 92 | r, 93 | clientLogger) 94 | 95 | case "/favicon.ico": 96 | err = serveStaticCacheData( 97 | "favicon.ico", 98 | staticFileExt(".ico"), 99 | w, 100 | r, 101 | clientLogger) 102 | 103 | case "/manifest.json": 104 | err = serveStaticCacheData( 105 | "manifest.json", 106 | staticFileExt(".json"), 107 | w, 108 | r, 109 | clientLogger) 110 | 111 | case "/browserconfig.xml": 112 | err = serveStaticCacheData( 113 | "browserconfig.xml", 114 | staticFileExt(".xml"), 115 | w, 116 | r, 117 | clientLogger) 118 | 119 | default: 120 | if strings.HasPrefix(r.URL.Path, assetsURLPrefix) && 121 | strings.ToUpper(r.Method) == "GET" { 122 | err = serveStaticCacheData( 123 | r.URL.Path[assetsURLPrefixLen:], 124 | staticFileExt(r.URL.Path[assetsURLPrefixLen:]), 125 | w, 126 | r, 127 | clientLogger) 128 | } else { 129 | err = ErrNotFound 130 | } 131 | } 132 | 133 | if err == nil { 134 | clientLogger.Info("Request completed: %s", r.URL.String()) 135 | 136 | return 137 | } 138 | 139 | clientLogger.Warning("Request ended with error: %s: %s", 140 | r.URL.String(), err) 141 | 142 | controllerErr, isControllerErr := err.(Error) 143 | 144 | if isControllerErr { 145 | serveFailure(controllerErr, w, r, h.logger) 146 | 147 | return 148 | } 149 | 150 | serveFailure( 151 | NewError(http.StatusInternalServerError, err.Error()), w, r, h.logger) 152 | } 153 | 154 | // Builder returns a http controller builder 155 | func Builder(cmds command.Commands) server.HandlerBuilder { 156 | return func( 157 | commonCfg configuration.Common, 158 | cfg configuration.Server, 159 | logger log.Logger, 160 | ) http.Handler { 161 | hooks := command.NewHooks(commonCfg.Hooks) 162 | socketCtl := newSocketCtl(commonCfg, cfg, cmds, hooks) 163 | 164 | return handler{ 165 | hostNameChecker: commonCfg.HostName + ":", 166 | commonCfg: commonCfg, 167 | logger: logger, 168 | homeCtl: home{}, 169 | socketCtl: socketCtl, 170 | socketVerifyCtl: newSocketVerification(socketCtl, cfg, commonCfg), 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /application/controller/error.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package controller 19 | 20 | import "fmt" 21 | 22 | // Error Controller error 23 | type Error struct { 24 | code int 25 | message string 26 | } 27 | 28 | // NewError creates a new Error 29 | func NewError(code int, message string) Error { 30 | return Error{ 31 | code: code, 32 | message: message, 33 | } 34 | } 35 | 36 | // Code return the error code 37 | func (f Error) Code() int { 38 | return f.code 39 | } 40 | 41 | // Error returns the error message 42 | func (f Error) Error() string { 43 | return fmt.Sprintf("HTTP Error (%d): %s", f.code, f.message) 44 | } 45 | -------------------------------------------------------------------------------- /application/controller/failure.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package controller 19 | 20 | import ( 21 | "net/http" 22 | 23 | "github.com/nirui/sshwifty/application/log" 24 | ) 25 | 26 | func serveFailure( 27 | err Error, 28 | w http.ResponseWriter, 29 | r *http.Request, 30 | l log.Logger, 31 | ) error { 32 | return serveStaticPage("error.html", err.Code(), w, r, l) 33 | } 34 | -------------------------------------------------------------------------------- /application/controller/home.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package controller 19 | 20 | import ( 21 | "net/http" 22 | 23 | "github.com/nirui/sshwifty/application/log" 24 | ) 25 | 26 | // home controller 27 | type home struct { 28 | baseController 29 | } 30 | 31 | func (h home) Get(w http.ResponseWriter, r *http.Request, l log.Logger) error { 32 | return serveStaticPage("index.html", http.StatusOK, w, r, l) 33 | } 34 | -------------------------------------------------------------------------------- /application/controller/static.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | //go:generate go run ./static_page_generater ../../.tmp/dist ./static_pages.go 19 | //go:generate go fmt ./static_pages.go 20 | 21 | package controller 22 | 23 | import ( 24 | "net/http" 25 | "strconv" 26 | "strings" 27 | "time" 28 | 29 | "github.com/nirui/sshwifty/application/log" 30 | ) 31 | 32 | type staticData struct { 33 | data []byte 34 | dataHash string 35 | compressd []byte 36 | compressdHash string 37 | created time.Time 38 | contentType string 39 | } 40 | 41 | func (s staticData) hasCompressed() bool { 42 | return len(s.compressd) > 0 43 | } 44 | 45 | func staticFileExt(fileName string) string { 46 | extIdx := strings.LastIndex(fileName, ".") 47 | if extIdx < 0 { 48 | return "" 49 | } 50 | return strings.ToLower(fileName[extIdx:]) 51 | } 52 | 53 | func serveStaticCacheData( 54 | dataName string, 55 | fileExt string, 56 | w http.ResponseWriter, 57 | r *http.Request, 58 | l log.Logger, 59 | ) error { 60 | if fileExt == ".html" || fileExt == ".htm" { 61 | return ErrNotFound 62 | } 63 | return serveStaticCachePage(dataName, w, r, l) 64 | } 65 | 66 | func serveStaticCachePage( 67 | dataName string, 68 | w http.ResponseWriter, 69 | r *http.Request, 70 | l log.Logger, 71 | ) error { 72 | d, dFound := staticPages[dataName] 73 | if !dFound { 74 | return ErrNotFound 75 | } 76 | selectedData := d.data 77 | selectedLength := len(d.data) 78 | compressEnabled := false 79 | if clientSupportGZIP(r) && d.hasCompressed() { 80 | selectedData = d.compressd 81 | selectedLength = len(d.compressd) 82 | compressEnabled = true 83 | w.Header().Add("Vary", "Accept-Encoding") 84 | } 85 | w.Header().Add("Cache-Control", "public, max-age=5184000") 86 | w.Header().Add("Content-Type", d.contentType) 87 | if compressEnabled { 88 | w.Header().Add("Content-Encoding", "gzip") 89 | } 90 | w.Header().Add("Content-Length", 91 | strconv.FormatInt(int64(selectedLength), 10)) 92 | _, wErr := w.Write(selectedData) 93 | return wErr 94 | } 95 | 96 | func serveStaticPage( 97 | dataName string, 98 | code int, 99 | w http.ResponseWriter, 100 | r *http.Request, 101 | l log.Logger, 102 | ) error { 103 | d, dFound := staticPages[dataName] 104 | if !dFound { 105 | return ErrNotFound 106 | } 107 | selectedData := d.data 108 | selectedLength := len(d.data) 109 | compressEnabled := false 110 | if clientSupportGZIP(r) && d.hasCompressed() { 111 | selectedData = d.compressd 112 | selectedLength = len(d.compressd) 113 | compressEnabled = true 114 | w.Header().Add("Vary", "Accept-Encoding") 115 | } 116 | w.Header().Add("Content-Type", d.contentType) 117 | if compressEnabled { 118 | w.Header().Add("Content-Encoding", "gzip") 119 | } 120 | w.Header().Add("Content-Length", 121 | strconv.FormatInt(int64(selectedLength), 10)) 122 | w.WriteHeader(code) 123 | _, wErr := w.Write(selectedData) 124 | return wErr 125 | } 126 | -------------------------------------------------------------------------------- /application/log/ditch.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package log 19 | 20 | // Ditch ditch all logs 21 | type Ditch struct{} 22 | 23 | // NewDitch creates a new Ditch 24 | func NewDitch() Ditch { 25 | return Ditch{} 26 | } 27 | 28 | // Context build a new Sub context 29 | func (w Ditch) Context(name string, params ...interface{}) Logger { 30 | return w 31 | } 32 | 33 | // Write writes default error 34 | func (w Ditch) Write(b []byte) (int, error) { 35 | return len(b), nil 36 | } 37 | 38 | // Info write an info message 39 | func (w Ditch) Info(msg string, params ...interface{}) {} 40 | 41 | // Debug write an debug message 42 | func (w Ditch) Debug(msg string, params ...interface{}) {} 43 | 44 | // Warning write an warning message 45 | func (w Ditch) Warning(msg string, params ...interface{}) {} 46 | 47 | // Error write an error message 48 | func (w Ditch) Error(msg string, params ...interface{}) {} 49 | -------------------------------------------------------------------------------- /application/log/log.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package log 19 | 20 | // Logger represents a logger 21 | type Logger interface { 22 | Context(name string, params ...interface{}) Logger 23 | Write(b []byte) (int, error) 24 | Info(msg string, params ...interface{}) 25 | Debug(msg string, params ...interface{}) 26 | Warning(msg string, params ...interface{}) 27 | Error(msg string, params ...interface{}) 28 | } 29 | -------------------------------------------------------------------------------- /application/log/writer.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package log 19 | 20 | import ( 21 | "fmt" 22 | "io" 23 | "time" 24 | ) 25 | 26 | // Writer will write logs to the underlaying writer 27 | type Writer struct { 28 | c string 29 | w io.Writer 30 | } 31 | 32 | // NewWriter creates a new Writer 33 | func NewWriter(context string, w io.Writer) Writer { 34 | return Writer{ 35 | c: context, 36 | w: w, 37 | } 38 | } 39 | 40 | // Context build a new Sub context 41 | func (w Writer) Context(name string, params ...interface{}) Logger { 42 | return NewWriter(w.c+" > "+fmt.Sprintf(name, params...), w.w) 43 | } 44 | 45 | // Write writes default error 46 | func (w Writer) Write(b []byte) (int, error) { 47 | _, wErr := w.write("DEF", string(b)) 48 | 49 | if wErr != nil { 50 | return 0, wErr 51 | } 52 | 53 | return len(b), nil 54 | } 55 | 56 | func (w Writer) write( 57 | prefix string, msg string, params ...interface{}) (int, error) { 58 | return fmt.Fprintf(w.w, "["+prefix+"] "+ 59 | time.Now().Format(time.RFC1123)+" "+w.c+": "+msg+"\r\n", params...) 60 | } 61 | 62 | // Info write an info message 63 | func (w Writer) Info(msg string, params ...interface{}) { 64 | w.write("INF", msg, params...) 65 | } 66 | 67 | // Debug write an debug message 68 | func (w Writer) Debug(msg string, params ...interface{}) { 69 | w.write("DBG", msg, params...) 70 | } 71 | 72 | // Warning write an warning message 73 | func (w Writer) Warning(msg string, params ...interface{}) { 74 | w.write("WRN", msg, params...) 75 | } 76 | 77 | // Error write an error message 78 | func (w Writer) Error(msg string, params ...interface{}) { 79 | w.write("ERR", msg, params...) 80 | } 81 | -------------------------------------------------------------------------------- /application/log/writer_nodebug.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package log 19 | 20 | import ( 21 | "fmt" 22 | "io" 23 | ) 24 | 25 | // NonDebugWriter will write logs to the underlaying writer 26 | type NonDebugWriter struct { 27 | Writer 28 | } 29 | 30 | // NewNonDebugWriter creates a new Writer with debug output disabled 31 | func NewNonDebugWriter(context string, w io.Writer) NonDebugWriter { 32 | return NonDebugWriter{ 33 | Writer: NewWriter(context, w), 34 | } 35 | } 36 | 37 | // NewDebugOrNonDebugWriter creates debug or nondebug log depends on 38 | // given `useDebug` 39 | func NewDebugOrNonDebugWriter( 40 | useDebug bool, context string, w io.Writer) Logger { 41 | if useDebug { 42 | return NewWriter(context, w) 43 | } 44 | 45 | return NewNonDebugWriter(context, w) 46 | } 47 | 48 | // Context build a new Sub context 49 | func (w NonDebugWriter) Context(name string, params ...interface{}) Logger { 50 | return NewNonDebugWriter(w.c+" > "+fmt.Sprintf(name, params...), w.w) 51 | } 52 | 53 | // Debug ditchs debug operation 54 | func (w NonDebugWriter) Debug(msg string, params ...interface{}) {} 55 | -------------------------------------------------------------------------------- /application/network/conn.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package network 19 | 20 | import ( 21 | "time" 22 | ) 23 | 24 | var ( 25 | emptyTime = time.Time{} 26 | ) 27 | -------------------------------------------------------------------------------- /application/network/dial.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package network 19 | 20 | import ( 21 | "context" 22 | "net" 23 | ) 24 | 25 | // Dial dial to remote machine 26 | type Dial func( 27 | ctx context.Context, 28 | network string, 29 | address string, 30 | ) (net.Conn, error) 31 | 32 | // TCPDial build a TCP dialer 33 | func TCPDial() Dial { 34 | return func( 35 | ctx context.Context, 36 | network string, 37 | address string, 38 | ) (net.Conn, error) { 39 | dial := net.Dialer{} 40 | return dial.DialContext(ctx, network, address) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /application/network/dial_ac.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package network 19 | 20 | import ( 21 | "context" 22 | "errors" 23 | "net" 24 | ) 25 | 26 | // Errors 27 | var ( 28 | ErrAccessControlDialTargetHostNotAllowed = errors.New( 29 | "unable to dial to the specified remote host due to restriction") 30 | ) 31 | 32 | // AllowedHosts contains a map of allowed remote hosts 33 | type AllowedHosts map[string]struct{} 34 | 35 | // Allowed returns whether or not given host is allowed 36 | func (a AllowedHosts) Allowed(host string) bool { 37 | _, ok := a[host] 38 | return ok 39 | } 40 | 41 | // AllowedHost returns whether or not give host is allowed 42 | type AllowedHost interface { 43 | Allowed(host string) bool 44 | } 45 | 46 | // AccessControlDial creates an access controlled Dial 47 | func AccessControlDial(allowed AllowedHost, dial Dial) Dial { 48 | return func( 49 | ctx context.Context, 50 | network string, 51 | address string, 52 | ) (net.Conn, error) { 53 | if !allowed.Allowed(address) { 54 | return nil, ErrAccessControlDialTargetHostNotAllowed 55 | } 56 | return dial(ctx, network, address) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /application/network/dial_socks5.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package network 19 | 20 | import ( 21 | "context" 22 | "net" 23 | 24 | "golang.org/x/net/proxy" 25 | ) 26 | 27 | type socks5Dial struct { 28 | dialer net.Dialer 29 | ctx context.Context 30 | } 31 | 32 | func (s socks5Dial) Dial( 33 | network string, 34 | address string, 35 | ) (net.Conn, error) { 36 | return s.dialer.DialContext(s.ctx, network, address) 37 | } 38 | 39 | // BuildSocks5Dial builds a Socks5 dialer 40 | func BuildSocks5Dial( 41 | socks5Address string, 42 | userName string, 43 | password string, 44 | ) (Dial, error) { 45 | var auth *proxy.Auth 46 | if len(userName) > 0 || len(password) > 0 { 47 | auth = &proxy.Auth{ 48 | User: userName, 49 | Password: password, 50 | } 51 | } 52 | 53 | return func(ctx context.Context, n string, addr string) (net.Conn, error) { 54 | dialCfg := socks5Dial{ 55 | dialer: net.Dialer{}, 56 | ctx: ctx, 57 | } 58 | 59 | dial, dialErr := proxy.SOCKS5("tcp", socks5Address, auth, &dialCfg) 60 | if dialErr != nil { 61 | return nil, dialErr 62 | } 63 | 64 | dialConn, dialErr := dial.Dial(n, addr) 65 | if dialErr != nil { 66 | return nil, dialErr 67 | } 68 | 69 | return dialConn, nil 70 | }, nil 71 | } 72 | -------------------------------------------------------------------------------- /application/plate.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package application 19 | 20 | // Plate information 21 | const ( 22 | Name = "Sshwifty" 23 | FullName = "Sshwifty Web SSH Client" 24 | Author = "Ni Rui " 25 | URL = "https://github.com/nirui/sshwifty" 26 | ) 27 | 28 | // Banner message 29 | const ( 30 | banner = "\r\n %s %s\r\n\r\n Copyright (C) %s\r\n %s\r\n\r\n" 31 | ) 32 | 33 | // Version 34 | var ( 35 | version = "dev" 36 | ) 37 | -------------------------------------------------------------------------------- /application/rw/fetch.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package rw 19 | 20 | import "errors" 21 | 22 | // Errors 23 | var ( 24 | ErrFetchReaderNotEnoughBuffer = errors.New( 25 | "not enough buffer") 26 | ) 27 | 28 | // FetchReaderFetcher generates data for SourceReader 29 | type FetchReaderFetcher func() ([]byte, error) 30 | 31 | // FetchReader read from the source and increase your lifespan if used correctly 32 | type FetchReader struct { 33 | source FetchReaderFetcher // Source data fetcher 34 | data []byte // Fetched source data 35 | dataUsed int // Used source data 36 | } 37 | 38 | // Fetch fetchs 39 | type Fetch func(n int) ([]byte, error) 40 | 41 | // FetchOneByte fetchs one byte from the Fetch, or return an error when it fails 42 | func FetchOneByte(f Fetch) ([]byte, error) { 43 | for { 44 | d, dErr := f(1) 45 | 46 | if dErr != nil { 47 | return nil, dErr 48 | } 49 | 50 | if len(d) <= 0 { 51 | continue 52 | } 53 | 54 | return d, nil 55 | } 56 | } 57 | 58 | // NewFetchReader creates a new FetchReader 59 | func NewFetchReader(g FetchReaderFetcher) FetchReader { 60 | return FetchReader{ 61 | source: g, 62 | data: nil, 63 | dataUsed: 0, 64 | } 65 | } 66 | 67 | func (r FetchReader) dataRemain() int { 68 | return len(r.data) - r.dataUsed 69 | } 70 | 71 | // Remain Returns how many bytes is waiting to be readed 72 | func (r *FetchReader) Remain() int { 73 | return r.dataRemain() 74 | } 75 | 76 | // Export directly exports from buffer, never read from source 77 | // 78 | // Params: 79 | // - n: Exact amount of bytes to fetch (0 to n, n included). If number n is 80 | // unreachable, an error will be returned, and no internal status will 81 | // be changed 82 | // 83 | // Returns: 84 | // - Fetched data 85 | // - Read error 86 | func (r *FetchReader) Export(n int) ([]byte, error) { 87 | remain := r.dataRemain() 88 | 89 | if n > remain { 90 | return nil, ErrFetchReaderNotEnoughBuffer 91 | } 92 | 93 | newUsed := r.dataUsed + n 94 | data := r.data[r.dataUsed:newUsed] 95 | 96 | r.dataUsed = newUsed 97 | 98 | return data, nil 99 | } 100 | 101 | // Fetch fetchs data from the source 102 | // 103 | // Params: 104 | // - n: Max bytes to fetch (0 to n, n included) 105 | // 106 | // Returns: 107 | // - Fetched data 108 | // - Read error 109 | func (r *FetchReader) Fetch(n int) ([]byte, error) { 110 | remain := r.dataRemain() 111 | 112 | if remain <= 0 { 113 | data, dataFetchErr := r.source() 114 | 115 | if dataFetchErr != nil { 116 | return nil, dataFetchErr 117 | } 118 | 119 | r.data = data 120 | r.dataUsed = 0 121 | 122 | remain = r.dataRemain() 123 | } 124 | 125 | if n > remain { 126 | n = remain 127 | } 128 | 129 | newUsed := r.dataUsed + n 130 | data := r.data[r.dataUsed:newUsed] 131 | 132 | r.dataUsed = newUsed 133 | 134 | return data, nil 135 | } 136 | 137 | // Read implements io.Read 138 | func (r *FetchReader) Read(b []byte) (int, error) { 139 | d, dErr := r.Fetch(len(b)) 140 | 141 | if dErr != nil { 142 | return 0, dErr 143 | } 144 | 145 | return copy(b, d), nil 146 | } 147 | -------------------------------------------------------------------------------- /application/rw/fetch_test.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package rw 19 | 20 | import ( 21 | "bytes" 22 | "io" 23 | "testing" 24 | ) 25 | 26 | func testDummyFetchGen(data []byte) FetchReaderFetcher { 27 | current := 0 28 | 29 | return func() ([]byte, error) { 30 | if current >= len(data) { 31 | return nil, io.EOF 32 | } 33 | 34 | oldCurrent := current 35 | current = oldCurrent + 1 36 | 37 | return data[oldCurrent:current], nil 38 | } 39 | } 40 | 41 | func TestFetchReader(t *testing.T) { 42 | r := NewFetchReader(testDummyFetchGen([]byte("Hello World"))) 43 | b := make([]byte, 11) 44 | 45 | _, rErr := io.ReadFull(&r, b) 46 | 47 | if rErr != nil { 48 | t.Error("Failed to read due to error:", rErr) 49 | 50 | return 51 | } 52 | 53 | if !bytes.Equal(b, []byte("Hello World")) { 54 | t.Errorf("Expecting data to be %s, got %s instead", 55 | []byte("Hello World"), b) 56 | 57 | return 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /application/rw/limited.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package rw 19 | 20 | import ( 21 | "errors" 22 | "io" 23 | ) 24 | 25 | // Errors 26 | var ( 27 | ErrReadUntilCompletedBufferFull = errors.New( 28 | "cannot read more, not enough data buffer") 29 | ) 30 | 31 | // LimitedReader reads only n bytes of data 32 | type LimitedReader struct { 33 | r *FetchReader 34 | n int 35 | } 36 | 37 | // ReadUntilCompleted read until the reader is completed 38 | func ReadUntilCompleted(r *LimitedReader, b []byte) (int, error) { 39 | bCur := 0 40 | bLen := len(b) 41 | 42 | for !r.Completed() { 43 | if bCur >= bLen { 44 | return bCur, ErrReadUntilCompletedBufferFull 45 | } 46 | 47 | rLen, rErr := r.Read(b[bCur:]) 48 | 49 | if rErr != nil { 50 | return bCur + rLen, rErr 51 | } 52 | 53 | bCur += rLen 54 | } 55 | 56 | return bCur, nil 57 | } 58 | 59 | // NewLimitedReader creates a new LimitedReader 60 | func NewLimitedReader(r *FetchReader, n int) LimitedReader { 61 | return LimitedReader{ 62 | r: r, 63 | n: n, 64 | } 65 | } 66 | 67 | // Buffered exports the internal buffer 68 | func (l *LimitedReader) Buffered() ([]byte, error) { 69 | return l.Fetch(l.Remains()) 70 | } 71 | 72 | // Fetch fetchs max n bytes from buffer 73 | func (l *LimitedReader) Fetch(n int) ([]byte, error) { 74 | if l.Completed() { 75 | return nil, io.EOF 76 | } 77 | 78 | if n > l.Remains() { 79 | n = l.Remains() 80 | } 81 | 82 | exported, eErr := l.r.Fetch(n) 83 | 84 | l.n -= len(exported) 85 | 86 | return exported, eErr 87 | } 88 | 89 | // Read read from the LimitedReader 90 | func (l *LimitedReader) Read(b []byte) (int, error) { 91 | if l.Completed() { 92 | return 0, io.EOF 93 | } 94 | 95 | toRead := len(b) 96 | 97 | if toRead > l.Remains() { 98 | toRead = l.Remains() 99 | } 100 | 101 | rLen, rErr := l.r.Read(b[:toRead]) 102 | 103 | l.n -= rLen 104 | 105 | return rLen, rErr 106 | } 107 | 108 | // Ditch ditchs all remaining data. Data will be written and overwritten to 109 | // the given buf when ditching 110 | func (l *LimitedReader) Ditch(buf []byte) error { 111 | for !l.Completed() { 112 | _, rErr := l.Read(buf) 113 | 114 | if rErr != nil { 115 | return rErr 116 | } 117 | } 118 | 119 | return nil 120 | } 121 | 122 | // Remains returns how many bytes is waiting to be read 123 | func (l LimitedReader) Remains() int { 124 | return l.n 125 | } 126 | 127 | // Completed returns whether or not current reader is completed 128 | func (l LimitedReader) Completed() bool { 129 | return l.n <= 0 130 | } 131 | -------------------------------------------------------------------------------- /application/rw/rw.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package rw 19 | 20 | // ReaderFunc function of io.Reader 21 | type ReaderFunc func(b []byte) (int, error) 22 | 23 | // ReadFull Read until given b is fully loaded 24 | func ReadFull(r ReaderFunc, b []byte) (int, error) { 25 | bLen := len(b) 26 | readed := 0 27 | 28 | for { 29 | rLen, rErr := r(b[readed:]) 30 | 31 | readed += rLen 32 | 33 | if rErr != nil { 34 | return readed, rErr 35 | } 36 | 37 | if readed >= bLen { 38 | return readed, nil 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /application/server/conn.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package server 19 | 20 | import ( 21 | "net" 22 | "time" 23 | 24 | "github.com/nirui/sshwifty/application/network" 25 | ) 26 | 27 | type listener struct { 28 | *net.TCPListener 29 | 30 | readTimeout time.Duration 31 | writeTimeout time.Duration 32 | } 33 | 34 | func (l listener) Accept() (net.Conn, error) { 35 | acc, accErr := l.TCPListener.Accept() 36 | 37 | if accErr != nil { 38 | return nil, accErr 39 | } 40 | 41 | timeoutConn := network.NewTimeoutConn(acc, l.readTimeout, l.writeTimeout) 42 | 43 | return conn{ 44 | TimeoutConn: &timeoutConn, 45 | readTimeout: l.readTimeout, 46 | writeTimeout: l.writeTimeout, 47 | }, nil 48 | } 49 | 50 | // conn is a net.Conn hack, we use it prevent the upper to alter some important 51 | // configuration of the connection, mainly the timeouts. 52 | type conn struct { 53 | *network.TimeoutConn 54 | 55 | readTimeout time.Duration 56 | writeTimeout time.Duration 57 | } 58 | 59 | func (c conn) normalizeTimeout(t time.Time, m time.Duration) time.Time { 60 | max := time.Now().Add(m) 61 | 62 | // You cannot set timeout that is longer than the given m 63 | if t.After(max) { 64 | return max 65 | } 66 | 67 | return t 68 | } 69 | 70 | func (c conn) SetDeadline(dl time.Time) error { 71 | c.SetReadDeadline(dl) 72 | c.SetWriteDeadline(dl) 73 | 74 | return nil 75 | } 76 | 77 | func (c conn) SetReadDeadline(dl time.Time) error { 78 | return c.TimeoutConn.SetReadDeadline( 79 | c.normalizeTimeout(dl, c.readTimeout)) 80 | } 81 | 82 | func (c conn) SetWriteDeadline(dl time.Time) error { 83 | return c.TimeoutConn.SetWriteDeadline( 84 | c.normalizeTimeout(dl, c.writeTimeout)) 85 | } 86 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | module.exports = function(api) { 19 | api.cache(true); 20 | 21 | return { 22 | presets: ["@babel/preset-env"], 23 | plugins: [["@babel/plugin-transform-runtime"]], 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /docker-compose.example.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | sshwifty: 5 | image: sshwifty:dev 6 | container_name: sshwifty 7 | user: "nobody:nobody" 8 | build: ./ 9 | restart: unless-stopped 10 | # environment: 11 | # - SSHWIFTY_SHAREDKEY=WEB_ACCESS_PASSWORD 12 | ports: 13 | - "127.0.0.1:8182:8182/tcp" 14 | stdin_open: true 15 | tty: true 16 | # deploy: 17 | # replicas: 3 18 | # resources: 19 | # limits: 20 | # cpus: '0.3' 21 | # memory: 600M 22 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import js from "@eslint/js"; 5 | import { FlatCompat } from "@eslint/eslintrc"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | recommendedConfig: js.configs.recommended, 12 | allConfig: js.configs.all 13 | }); 14 | 15 | export default [...compat.extends( 16 | "plugin:vue/recommended", 17 | "eslint:recommended", 18 | "prettier", 19 | "plugin:prettier/recommended", 20 | ), { 21 | languageOptions: { 22 | globals: { 23 | ...globals.node, 24 | $nuxt: true, 25 | }, 26 | 27 | ecmaVersion: 13, 28 | sourceType: "module", 29 | 30 | parserOptions: { 31 | parser: "@babel/eslint-parser", 32 | }, 33 | }, 34 | 35 | rules: { 36 | "vue/component-name-in-template-casing": ["error", "PascalCase"], 37 | "vue/multi-word-component-names": "off", 38 | "no-console": "off", 39 | "no-debugger": "off", 40 | "no-unused-vars": "warn", 41 | }, 42 | }]; -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | module github.com/nirui/sshwifty 19 | 20 | go 1.24.3 21 | 22 | require ( 23 | github.com/gorilla/websocket v1.5.3 24 | golang.org/x/crypto v0.38.0 25 | golang.org/x/net v0.40.0 26 | ) 27 | 28 | require golang.org/x/sys v0.33.0 // indirect 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 2 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 3 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 4 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 5 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 6 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 7 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 8 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 9 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 10 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 11 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 12 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 13 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 14 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 15 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 16 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 17 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 18 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 19 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 20 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 21 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 22 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 23 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 24 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 25 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 26 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 27 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 28 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 29 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 30 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 31 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 32 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 33 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sshwifty-ui", 3 | "version": "0.0.0", 4 | "description": "Sshwifty Web Front-end Project", 5 | "main": "", 6 | "type": "module", 7 | "devDependencies": { 8 | "@azurity/pure-nerd-font": "^3.0.4", 9 | "@babel/core": "^7.27.1", 10 | "@babel/eslint-parser": "^7.27.1", 11 | "@babel/plugin-transform-runtime": "^7.27.1", 12 | "@babel/preset-env": "^7.27.2", 13 | "@babel/register": "^7.27.1", 14 | "@babel/runtime": "^7.27.1", 15 | "@xterm/addon-fit": "^0.10.0", 16 | "@xterm/addon-unicode11": "^0.8.0", 17 | "@xterm/addon-web-links": "^0.11.0", 18 | "@xterm/addon-webgl": "^0.18.0", 19 | "@xterm/xterm": "^5.5.0", 20 | "babel-loader": "^10.0.0", 21 | "buffer": "^6.0.3", 22 | "clean-webpack-plugin": "^4.0.0", 23 | "copy-webpack-plugin": "^13.0.0", 24 | "css-loader": "^7.1.2", 25 | "css-minimizer-webpack-plugin": "^7.0.2", 26 | "cwebp-bin": "^8.0.0", 27 | "eslint": "^9.26.0", 28 | "eslint-config-prettier": "^10.1.5", 29 | "eslint-plugin-prettier": "^5.4.0", 30 | "eslint-plugin-vue": "^9.32.0", 31 | "eslint-webpack-plugin": "^5.0.1", 32 | "favicons": "^7.2.0", 33 | "fontfaceobserver": "^2.3.0", 34 | "hack-font": "^3.3.0", 35 | "html-loader": "^5.1.0", 36 | "html-webpack-plugin": "^5.6.3", 37 | "iconv-lite": "^0.6.3", 38 | "image-minimizer-webpack-plugin": "^4.1.3", 39 | "mini-css-extract-plugin": "^2.9.2", 40 | "mocha": "^11.2.2", 41 | "normalize.css": "^8.0.1", 42 | "prettier": "^3.5.3", 43 | "roboto-fontface": "^0.10.0", 44 | "sharp": "^0.34.1", 45 | "style-loader": "^4.0.0", 46 | "svgo": "^3.3.2", 47 | "terser-webpack-plugin": "^5.3.14", 48 | "vue": "^2.6.14", 49 | "vue-loader": "^15.9.8", 50 | "webpack": "^5.99.8", 51 | "webpack-cli": "^6.0.1", 52 | "webpack-favicons": "^1.5.4" 53 | }, 54 | "scripts": { 55 | "dev": "CGO_ENABLED=0 NODE_ENV=development webpack --mode=development --config=webpack.config.js --watch", 56 | "clean": "rm .tmp/ -rf || true", 57 | "generate": "npm run clean && CGO_ENABLED=0 NODE_ENV=production webpack --mode=production --config=webpack.config.js", 58 | "build": "npm run generate && CGO_ENABLED=0 go build -ldflags \"-s -w -X github.com/nirui/sshwifty/application.version=$(git describe --always --dirty='*' --tag)\"", 59 | "lint": "eslint --ext .js,.vue ui", 60 | "testonly": "mocha --require @babel/register --recursive --timeout 3s ./ui/**/*_test.js && CGO_ENABLED=1 go test ./... -race -timeout 30s", 61 | "test": "npm run generate && npm run testonly" 62 | }, 63 | "author": "", 64 | "license": "AGPL-3.0-only" 65 | } 66 | -------------------------------------------------------------------------------- /preset.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Title": "Testing telnet server", 4 | "Type": "Telnet", 5 | "Host": "sshwifty-telnet-test:5555", 6 | "Meta": {} 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /sshwifty.conf.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "HostName": "", 3 | "SharedKey": "WEB_ACCESS_PASSWORD", 4 | "DialTimeout": 5, 5 | "Socks5": "", 6 | "Socks5User": "", 7 | "Socks5Password": "", 8 | "Hooks": { 9 | "before_connecting": [] 10 | }, 11 | "HookTimeout": 30, 12 | "Servers": [ 13 | { 14 | "ListenInterface": "127.0.0.1", 15 | "ListenPort": 8182, 16 | "InitialTimeout": 3, 17 | "ReadTimeout": 60, 18 | "WriteTimeout": 60, 19 | "HeartbeatTimeout": 20, 20 | "ReadDelay": 10, 21 | "WriteDelay": 10, 22 | "TLSCertificateFile": "", 23 | "TLSCertificateKeyFile": "", 24 | "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." 25 | } 26 | ], 27 | "Presets": [ 28 | { 29 | "Title": "SDF.org Unix Shell", 30 | "Type": "SSH", 31 | "Host": "sdf.org:22", 32 | "TabColor": "112233", 33 | "Meta": { 34 | "Encoding": "utf-8", 35 | "Authentication": "Password" 36 | } 37 | }, 38 | { 39 | "Title": "My own super secure server", 40 | "Type": "SSH", 41 | "Host": "localhost", 42 | "Meta": { 43 | "User": "root", 44 | "Encoding": "utf-8", 45 | "Private Key": "-----BEGIN RSA Will be sent to client-END RSA PRI...\n", 46 | "Authentication": "Private Key", 47 | "Fingerprint": "SHA256:bgO...." 48 | } 49 | }, 50 | { 51 | "Title": "My own super expensive router", 52 | "Type": "Telnet", 53 | "Host": "localhost", 54 | "Meta": { 55 | "Encoding": "ibm866" 56 | } 57 | } 58 | ], 59 | "OnlyAllowPresetRemotes": false 60 | } 61 | -------------------------------------------------------------------------------- /sshwifty.go: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2023 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | package main 19 | 20 | import ( 21 | "os" 22 | 23 | "github.com/nirui/sshwifty/application" 24 | "github.com/nirui/sshwifty/application/commands" 25 | "github.com/nirui/sshwifty/application/configuration" 26 | "github.com/nirui/sshwifty/application/controller" 27 | "github.com/nirui/sshwifty/application/log" 28 | ) 29 | 30 | func main() { 31 | configLoaders := make([]configuration.Loader, 0, 2) 32 | 33 | if len(os.Getenv("SSHWIFTY_CONFIG")) > 0 { 34 | configLoaders = append(configLoaders, 35 | configuration.File(os.Getenv("SSHWIFTY_CONFIG"))) 36 | } else { 37 | configLoaders = append(configLoaders, configuration.File("")) 38 | configLoaders = append(configLoaders, configuration.Enviro()) 39 | } 40 | 41 | e := application. 42 | New(os.Stderr, log.NewDebugOrNonDebugWriter( 43 | len(os.Getenv("SSHWIFTY_DEBUG")) > 0, application.Name, os.Stderr)). 44 | Run(configuration.Redundant(configLoaders...), 45 | application.DefaultProccessSignallerBuilder, 46 | commands.New(), 47 | controller.Builder, 48 | ) 49 | 50 | if e == nil { 51 | return 52 | } 53 | 54 | os.Exit(1) 55 | } 56 | -------------------------------------------------------------------------------- /ui/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | // Sshwifty - A Web SSH client 3 | // 4 | // Copyright (C) 2019-2025 Ni Rui 5 | // 6 | // This program is free software: you can redistribute it and/or modify 7 | // it under the terms of the GNU Affero General Public License as 8 | // published by the Free Software Foundation, either version 3 of the 9 | // License, or (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU Affero General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Affero General Public License 17 | // along with this program. If not, see . 18 | */ 19 | 20 | @charset "utf-8"; 21 | 22 | #app { 23 | padding: 0; 24 | margin: 0; 25 | width: 100%; 26 | height: 100%; 27 | position: relative; 28 | min-width: 320px; 29 | } 30 | 31 | body.app-error { 32 | background: #944; 33 | color: #fff; 34 | } 35 | 36 | body.app-error .app-error-message { 37 | white-space: pre-line; 38 | } 39 | 40 | #app-loading { 41 | width: 100%; 42 | min-height: 100%; 43 | display: flex; 44 | flex-direction: column; 45 | justify-items: center; 46 | justify-content: center; 47 | } 48 | 49 | #app-loading-frame { 50 | flex: 0 0; 51 | padding: 30px; 52 | text-align: center; 53 | } 54 | 55 | #app-loading-icon { 56 | background: url("./widgets/busy.svg") center center no-repeat; 57 | background-size: contain; 58 | width: 100%; 59 | height: 200px; 60 | } 61 | 62 | #app-loading-error { 63 | font-size: 5em; 64 | color: #fff; 65 | } 66 | 67 | #app-loading-title { 68 | color: #fab; 69 | font-size: 1.2em; 70 | font-weight: lighter; 71 | max-width: 500px; 72 | margin: 0 auto; 73 | } 74 | 75 | #app-loading-title.error { 76 | color: #fff; 77 | } 78 | -------------------------------------------------------------------------------- /ui/auth.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 71 | 72 | 127 | -------------------------------------------------------------------------------- /ui/commands/address_test.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | import assert from "assert"; 19 | import * as reader from "../stream/reader.js"; 20 | import * as address from "./address.js"; 21 | 22 | describe("Address", () => { 23 | it("Address Loopback", async () => { 24 | let addr = new address.Address(address.LOOPBACK, null, 8080), 25 | buf = addr.buffer(); 26 | 27 | let r = new reader.Reader(new reader.Multiple(), (data) => { 28 | return data; 29 | }); 30 | 31 | r.feed(buf); 32 | 33 | let addr2 = await address.Address.read(r); 34 | 35 | assert.strictEqual(addr2.type(), addr.type()); 36 | assert.deepStrictEqual(addr2.address(), addr.address()); 37 | assert.strictEqual(addr2.port(), addr.port()); 38 | }); 39 | 40 | it("Address IPv4", async () => { 41 | let addr = new address.Address( 42 | address.IPV4, 43 | new Uint8Array([127, 0, 0, 1]), 44 | 8080, 45 | ), 46 | buf = addr.buffer(); 47 | 48 | let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { 49 | return data; 50 | }); 51 | 52 | r.feed(buf); 53 | 54 | let addr2 = await address.Address.read(r); 55 | 56 | assert.strictEqual(addr2.type(), addr.type()); 57 | assert.deepStrictEqual(addr2.address(), addr.address()); 58 | assert.strictEqual(addr2.port(), addr.port()); 59 | }); 60 | 61 | it("Address IPv6", async () => { 62 | let addr = new address.Address( 63 | address.IPV6, 64 | new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]), 65 | 8080, 66 | ), 67 | buf = addr.buffer(); 68 | 69 | let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { 70 | return data; 71 | }); 72 | 73 | r.feed(buf); 74 | 75 | let addr2 = await address.Address.read(r); 76 | 77 | assert.strictEqual(addr2.type(), addr.type()); 78 | assert.deepStrictEqual(addr2.address(), addr.address()); 79 | assert.strictEqual(addr2.port(), addr.port()); 80 | }); 81 | 82 | it("Address HostName", async () => { 83 | let addr = new address.Address( 84 | address.HOSTNAME, 85 | new Uint8Array(["v", "a", "g", "u", "l", "1", "2", "3"]), 86 | 8080, 87 | ), 88 | buf = addr.buffer(); 89 | 90 | let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { 91 | return data; 92 | }); 93 | 94 | r.feed(buf); 95 | 96 | let addr2 = await address.Address.read(r); 97 | 98 | assert.strictEqual(addr2.type(), addr.type()); 99 | assert.deepStrictEqual(addr2.address(), addr.address()); 100 | assert.strictEqual(addr2.port(), addr.port()); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /ui/commands/controls.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | import Exception from "./exception.js"; 19 | 20 | export class Controls { 21 | /** 22 | * constructor 23 | * 24 | * @param {[]object} controls 25 | * 26 | * @throws {Exception} When control type already been defined 27 | * 28 | */ 29 | constructor(controls) { 30 | this.controls = {}; 31 | 32 | for (let i in controls) { 33 | let cType = controls[i].type(); 34 | 35 | if (typeof this.controls[cType] === "object") { 36 | throw new Exception('Control "' + cType + '" already been defined'); 37 | } 38 | 39 | this.controls[cType] = controls[i]; 40 | } 41 | } 42 | 43 | /** 44 | * Get a control 45 | * 46 | * @param {string} type Type of the control 47 | * 48 | * @returns {object} Control object 49 | * 50 | * @throws {Exception} When given control type is undefined 51 | * 52 | */ 53 | get(type) { 54 | if (typeof this.controls[type] !== "object") { 55 | throw new Exception('Control "' + type + '" was undefined'); 56 | } 57 | 58 | return this.controls[type]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ui/commands/events.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | import Exception from "./exception.js"; 19 | 20 | export class Events { 21 | /** 22 | * constructor 23 | * 24 | * @param {[]string} events required events 25 | * @param {object} callbacks Callbacks 26 | * 27 | * @throws {Exception} When event handler is not registered 28 | * 29 | */ 30 | constructor(events, callbacks) { 31 | this.events = {}; 32 | this.placeHolders = {}; 33 | 34 | for (let i in events) { 35 | if (typeof callbacks[events[i]] !== "function") { 36 | throw new Exception( 37 | 'Unknown event type for "' + 38 | events[i] + 39 | '". Expecting "function" got "' + 40 | typeof callbacks[events[i]] + 41 | '" instead.', 42 | ); 43 | } 44 | 45 | let name = events[i]; 46 | 47 | if (name.indexOf("@") === 0) { 48 | name = name.substring(1); 49 | 50 | this.placeHolders[name] = null; 51 | } 52 | 53 | this.events[name] = callbacks[events[i]]; 54 | } 55 | } 56 | 57 | /** 58 | * Place callbacks to pending placeholder events 59 | * 60 | * @param {string} type Event Type 61 | * @param {function} callback Callback function 62 | */ 63 | place(type, callback) { 64 | if (this.placeHolders[type] !== null) { 65 | throw new Exception( 66 | 'Event type "' + 67 | type + 68 | '" cannot be appended. It maybe ' + 69 | "unregistered or already been acquired", 70 | ); 71 | } 72 | 73 | if (typeof callback !== "function") { 74 | throw new Exception( 75 | 'Unknown event type for "' + 76 | type + 77 | '". Expecting "function" got "' + 78 | typeof callback + 79 | '" instead.', 80 | ); 81 | } 82 | 83 | delete this.placeHolders[type]; 84 | 85 | this.events[type] = callback; 86 | } 87 | 88 | /** 89 | * Fire an event 90 | * 91 | * @param {string} type Event type 92 | * @param {...any} data Event data 93 | * 94 | * @returns {any} The result of the event handler 95 | * 96 | * @throws {Exception} When event type is not registered 97 | * 98 | */ 99 | fire(type, ...data) { 100 | if (!this.events[type] && this.placeHolders[type] !== null) { 101 | throw new Exception("Unknown event type: " + type); 102 | } 103 | 104 | return this.events[type](...data); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /ui/commands/exception.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | export default class Exception extends Error { 19 | /** 20 | * constructor 21 | * 22 | * @param {string} message error message 23 | * 24 | */ 25 | constructor(message) { 26 | super(message); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ui/commands/integer.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | import * as reader from "../stream/reader.js"; 19 | import Exception from "./exception.js"; 20 | 21 | export const MAX = 0x3fff; 22 | export const MAX_BYTES = 2; 23 | 24 | const integerHasNextBit = 0x80; 25 | const integerValueCutter = 0x7f; 26 | 27 | export class Integer { 28 | /** 29 | * constructor 30 | * 31 | * @param {number} num Integer number 32 | * 33 | */ 34 | constructor(num) { 35 | this.num = num; 36 | } 37 | 38 | /** 39 | * Marshal integer to buffer 40 | * 41 | * @returns {Uint8Array} Integer buffer 42 | * 43 | * @throws {Exception} When number is too large 44 | * 45 | */ 46 | marshal() { 47 | if (this.num > MAX) { 48 | throw new Exception("Integer number cannot be greater than 0x3fff"); 49 | } 50 | 51 | if (this.num <= integerValueCutter) { 52 | return new Uint8Array([this.num & integerValueCutter]); 53 | } 54 | 55 | return new Uint8Array([ 56 | (this.num >> 7) | integerHasNextBit, 57 | this.num & integerValueCutter, 58 | ]); 59 | } 60 | 61 | /** 62 | * Parse the reader to build an Integer 63 | * 64 | * @param {reader.Reader} rd Data reader 65 | * 66 | */ 67 | async unmarshal(rd) { 68 | for (let i = 0; i < MAX_BYTES; i++) { 69 | let r = await reader.readOne(rd); 70 | 71 | this.num |= r[0] & integerValueCutter; 72 | 73 | if ((integerHasNextBit & r[0]) == 0) { 74 | return; 75 | } 76 | 77 | this.num <<= 7; 78 | } 79 | } 80 | 81 | /** 82 | * Return the value of the number 83 | * 84 | * @returns {number} The integer value 85 | * 86 | */ 87 | value() { 88 | return this.num; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ui/commands/integer_test.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | import assert from "assert"; 19 | import * as reader from "../stream/reader.js"; 20 | import * as integer from "./integer.js"; 21 | 22 | describe("Integer", () => { 23 | it("Integer 127", async () => { 24 | let i = new integer.Integer(127), 25 | marshalled = i.marshal(); 26 | 27 | let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { 28 | return data; 29 | }); 30 | 31 | assert.strictEqual(marshalled.length, 1); 32 | 33 | r.feed(marshalled); 34 | 35 | let i2 = new integer.Integer(0); 36 | 37 | await i2.unmarshal(r); 38 | 39 | assert.strictEqual(i.value(), i2.value()); 40 | }); 41 | 42 | it("Integer MAX", async () => { 43 | let i = new integer.Integer(integer.MAX), 44 | marshalled = i.marshal(); 45 | 46 | let r = new reader.Reader(new reader.Multiple(() => {}), (data) => { 47 | return data; 48 | }); 49 | 50 | assert.strictEqual(marshalled.length, 2); 51 | 52 | r.feed(marshalled); 53 | 54 | let i2 = new integer.Integer(0); 55 | 56 | await i2.unmarshal(r); 57 | 58 | assert.strictEqual(i.value(), i2.value()); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /ui/commands/string.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | import * as reader from "../stream/reader.js"; 19 | import * as integer from "./integer.js"; 20 | 21 | export class String { 22 | /** 23 | * Read String from given reader 24 | * 25 | * @param {reader.Reader} rd Source reader 26 | * 27 | * @returns {String} readed string 28 | * 29 | */ 30 | static async read(rd) { 31 | let l = new integer.Integer(0); 32 | 33 | await l.unmarshal(rd); 34 | 35 | return new String(await reader.readN(rd, l.value())); 36 | } 37 | 38 | /** 39 | * constructor 40 | * 41 | * @param {Uint8Array} str String data 42 | */ 43 | constructor(str) { 44 | this.str = str; 45 | } 46 | 47 | /** 48 | * Return the string 49 | * 50 | * @returns {Uint8Array} String data 51 | * 52 | */ 53 | data() { 54 | return this.str; 55 | } 56 | 57 | /** 58 | * Return serialized String as array 59 | * 60 | * @returns {Uint8Array} serialized String 61 | * 62 | */ 63 | buffer() { 64 | let lBytes = new integer.Integer(this.str.length).marshal(), 65 | buf = new Uint8Array(lBytes.length + this.str.length); 66 | 67 | buf.set(lBytes, 0); 68 | buf.set(this.str, lBytes.length); 69 | 70 | return buf; 71 | } 72 | } 73 | 74 | /** 75 | * Truncates a string to the maximum length 76 | * 77 | * @param {string} str Source string 78 | * @param {integer} maxLength Max length 79 | * @param {string} exceed Text appends the string if it was truncated 80 | * 81 | * @returns {string} truncated String 82 | * 83 | */ 84 | export function truncate(str, maxLength, exceed) { 85 | if (str.length <= maxLength) { 86 | return str; 87 | } 88 | return str.substring(0, maxLength) + exceed; 89 | } 90 | -------------------------------------------------------------------------------- /ui/control/ssh.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | import * as iconv from "iconv-lite"; 19 | import * as color from "../commands/color.js"; 20 | import * as common from "../commands/common.js"; 21 | import * as reader from "../stream/reader.js"; 22 | import * as subscribe from "../stream/subscribe.js"; 23 | 24 | class Control { 25 | constructor(data, color) { 26 | this.background = color; 27 | this.charset = data.charset; 28 | 29 | this.charsetDecoder = (d) => { 30 | return iconv.decode(d, this.charset); 31 | }; 32 | this.charsetEncoder = (dStr) => { 33 | return iconv.encode(dStr, this.charset); 34 | }; 35 | 36 | this.enable = false; 37 | this.sender = data.send; 38 | this.closer = data.close; 39 | this.resizer = data.resize; 40 | this.subs = new subscribe.Subscribe(); 41 | 42 | let self = this; 43 | 44 | data.events.place("stdout", async (rd) => { 45 | try { 46 | self.subs.resolve(self.charsetDecoder(await reader.readCompletely(rd))); 47 | } catch (e) { 48 | // Do nothing 49 | } 50 | }); 51 | 52 | data.events.place("stderr", async (rd) => { 53 | try { 54 | self.subs.resolve(self.charsetDecoder(await reader.readCompletely(rd))); 55 | } catch (e) { 56 | // Do nothing 57 | } 58 | }); 59 | 60 | data.events.place("completed", () => { 61 | self.closed = true; 62 | self.background.forget(); 63 | 64 | self.subs.reject("Remote connection has been terminated"); 65 | }); 66 | } 67 | 68 | echo() { 69 | return false; 70 | } 71 | 72 | resize(dim) { 73 | if (this.closed) { 74 | return; 75 | } 76 | 77 | this.resizer(dim.rows, dim.cols); 78 | } 79 | 80 | enabled() { 81 | this.enable = true; 82 | } 83 | 84 | disabled() { 85 | this.enable = false; 86 | } 87 | 88 | retap(isOn) {} 89 | 90 | receive() { 91 | return this.subs.subscribe(); 92 | } 93 | 94 | send(data) { 95 | if (this.closed) { 96 | return; 97 | } 98 | 99 | return this.sender(this.charsetEncoder(data)); 100 | } 101 | 102 | sendBinary(data) { 103 | if (this.closed) { 104 | return; 105 | } 106 | 107 | return this.sender(common.strToBinary(data)); 108 | } 109 | 110 | color() { 111 | return this.background.hex(); 112 | } 113 | 114 | close() { 115 | if (this.closer === null) { 116 | return; 117 | } 118 | 119 | let cc = this.closer; 120 | this.closer = null; 121 | 122 | return cc(); 123 | } 124 | } 125 | 126 | export class SSH { 127 | /** 128 | * constructor 129 | * 130 | * @param {color.Colors} c 131 | */ 132 | constructor(c) { 133 | this.colors = c; 134 | } 135 | 136 | type() { 137 | return "SSH"; 138 | } 139 | 140 | ui() { 141 | return "Console"; 142 | } 143 | 144 | build(data) { 145 | return new Control(data, this.colors.get(data.tabColor)); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /ui/crypto.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | /** 19 | * Generate HMAC 512 of given data 20 | * 21 | * @param {Uint8Array} secret Secret key 22 | * @param {Uint8Array} data Data to be HMAC'ed 23 | */ 24 | export async function hmac512(secret, data) { 25 | const key = await window.crypto.subtle.importKey( 26 | "raw", 27 | secret, 28 | { 29 | name: "HMAC", 30 | hash: { name: "SHA-512" }, 31 | }, 32 | false, 33 | ["sign", "verify"], 34 | ); 35 | 36 | return window.crypto.subtle.sign(key.algorithm, key, data); 37 | } 38 | 39 | export const GCMNonceSize = 12; 40 | export const GCMKeyBitLen = 128; 41 | 42 | /** 43 | * Build AES GCM Encryption/Decryption key 44 | * 45 | * @param {Uint8Array} keyData Key data 46 | */ 47 | export function buildGCMKey(keyData) { 48 | return window.crypto.subtle.importKey( 49 | "raw", 50 | keyData, 51 | { 52 | name: "AES-GCM", 53 | length: GCMKeyBitLen, 54 | }, 55 | false, 56 | ["encrypt", "decrypt"], 57 | ); 58 | } 59 | 60 | /** 61 | * Encrypt data 62 | * 63 | * @param {CryptoKey} key Key 64 | * @param {Uint8Array} iv Nonce 65 | * @param {Uint8Array} plaintext Data to be encrypted 66 | */ 67 | export function encryptGCM(key, iv, plaintext) { 68 | return window.crypto.subtle.encrypt( 69 | { name: "AES-GCM", iv: iv, tagLength: GCMKeyBitLen }, 70 | key, 71 | plaintext, 72 | ); 73 | } 74 | 75 | /** 76 | * Decrypt data 77 | * 78 | * @param {CryptoKey} key Key 79 | * @param {Uint8Array} iv Nonce 80 | * @param {Uint8Array} cipherText Data to be decrypted 81 | */ 82 | export function decryptGCM(key, iv, cipherText) { 83 | return window.crypto.subtle.decrypt( 84 | { name: "AES-GCM", iv: iv, tagLength: GCMKeyBitLen }, 85 | key, 86 | cipherText, 87 | ); 88 | } 89 | 90 | /** 91 | * generate Random nonce 92 | * 93 | */ 94 | export function generateNonce() { 95 | return window.crypto.getRandomValues(new Uint8Array(GCMNonceSize)); 96 | } 97 | 98 | /** 99 | * Increase nonce by one 100 | * 101 | * @param {Uint8Array} nonce Nonce data 102 | * 103 | * @returns {Uint8Array} New nonce 104 | * 105 | */ 106 | export function increaseNonce(nonce) { 107 | for (let i = nonce.length; i > 0; i--) { 108 | nonce[i - 1]++; 109 | 110 | if (nonce[i - 1] <= 0) { 111 | continue; 112 | } 113 | 114 | break; 115 | } 116 | 117 | return nonce; 118 | } 119 | -------------------------------------------------------------------------------- /ui/error.html: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | Error 24 | 25 | 26 | 27 |
28 |
29 |
×
30 | 31 |

32 | Server was unable to complete the request 33 |

34 |
35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /ui/history.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | export class Records { 19 | /** 20 | * constructor 21 | * 22 | * @param {array} data Data space 23 | */ 24 | constructor(data) { 25 | this.data = data; 26 | } 27 | 28 | /** 29 | * Insert new item into the history records 30 | * 31 | * @param {number} newData New value 32 | */ 33 | update(newData) { 34 | this.data.shift(); 35 | this.data.push({ data: newData, class: "" }); 36 | } 37 | 38 | /** 39 | * Set all existing data as expired 40 | */ 41 | expire() { 42 | for (let i = 0; i < this.data.length; i++) { 43 | this.data[i].class = "expired"; 44 | } 45 | } 46 | 47 | /** 48 | * Return data 49 | * 50 | */ 51 | get() { 52 | return this.data; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ui/home_historyctl.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | import { History } from "./commands/history.js"; 19 | 20 | export function build(ctx) { 21 | let rec = []; 22 | 23 | // This renames "knowns" to "sshwifty-knowns" 24 | // TODO: Remove this after some few years 25 | try { 26 | let oldStore = localStorage.getItem("knowns"); 27 | 28 | if (oldStore) { 29 | localStorage.setItem("sshwifty-knowns", oldStore); 30 | localStorage.removeItem("knowns"); 31 | } 32 | } catch (e) { 33 | // Do nothing 34 | } 35 | 36 | try { 37 | rec = JSON.parse(localStorage.getItem("sshwifty-knowns")); 38 | 39 | if (!rec) { 40 | rec = []; 41 | } 42 | } catch (e) { 43 | alert("Unable to load data of Known remotes: " + e); 44 | } 45 | 46 | return new History( 47 | rec, 48 | (h, d) => { 49 | try { 50 | localStorage.setItem("sshwifty-knowns", JSON.stringify(d)); 51 | ctx.connector.knowns = h.all(); 52 | } catch (e) { 53 | alert("Unable to save remote history due to error: " + e); 54 | } 55 | }, 56 | 64, 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 | 23 | Sshwifty Web SSH Client 24 | 25 | 26 | 27 |
28 |
29 | 30 | 31 |

Loading Sshwifty

32 | 33 |
34 |

35 | Client is currently being loaded. Should only take a few seconds, 36 | please wait 37 |

38 | 44 |

45 | Copyright © 2019-2025 Ni Rui <ranqus@gmail.com> 46 |

47 |

48 | 49 | Source code 50 | 51 | 52 | 53 | Third-party 54 | 55 | 56 | Readme 57 | 58 | License 59 |

60 |
61 |
62 |
63 | 64 | 65 | -------------------------------------------------------------------------------- /ui/landing.css: -------------------------------------------------------------------------------- 1 | /* 2 | // Sshwifty - A Web SSH client 3 | // 4 | // Copyright (C) 2019-2025 Ni Rui 5 | // 6 | // This program is free software: you can redistribute it and/or modify 7 | // it under the terms of the GNU Affero General Public License as 8 | // published by the Free Software Foundation, either version 3 of the 9 | // License, or (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU Affero General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Affero General Public License 17 | // along with this program. If not, see . 18 | */ 19 | 20 | @charset "utf-8"; 21 | 22 | body.landing { 23 | background: #945; 24 | } 25 | 26 | #landing { 27 | min-height: 100%; 28 | font-family: Arial, Helvetica, sans-serif; 29 | display: flex; 30 | flex-direction: column; 31 | justify-content: center; 32 | justify-items: center; 33 | color: #fff; 34 | text-align: center; 35 | } 36 | 37 | #landing-message { 38 | font-size: 0.9em; 39 | max-width: 500px; 40 | margin: 0 auto; 41 | padding: 50px; 42 | flex: 0 0; 43 | } 44 | 45 | #landing-message a { 46 | text-decoration: none; 47 | color: #fab; 48 | } 49 | 50 | #landing-message p { 51 | margin: 10px 0; 52 | } 53 | 54 | #landing-message p.copy { 55 | margin: 20px 0 10px 0; 56 | color: #fab; 57 | } 58 | 59 | #landing-message p.copy.copy-first { 60 | margin-top: 50px; 61 | } 62 | 63 | #landing-message p.copy a { 64 | border: 1px solid #fab; 65 | display: inline-block; 66 | padding: 3px 7px; 67 | margin: 5px; 68 | border-radius: 5px; 69 | line-height: initial; 70 | } 71 | 72 | #landing-message-logo { 73 | background: url("./widgets/busy.svg") center center no-repeat; 74 | background-size: contain; 75 | width: 100%; 76 | height: 200px; 77 | } 78 | 79 | #landing-message-info { 80 | margin-top: 50px; 81 | font-size: 0.9em; 82 | } 83 | 84 | #auth { 85 | width: 100%; 86 | min-height: 100%; 87 | padding: 0; 88 | margin: 0; 89 | display: flex; 90 | flex-direction: column; 91 | justify-content: center; 92 | } 93 | 94 | #auth-frame { 95 | flex: 0 0; 96 | padding: 10px; 97 | } 98 | 99 | #auth-content { 100 | background: #333; 101 | box-shadow: 0 0 3px #111; 102 | padding: 30px; 103 | max-width: 380px; 104 | margin: 0 auto; 105 | font-size: 1em; 106 | } 107 | 108 | #auth-content > h1 { 109 | margin-bottom: 20px; 110 | color: #fab; 111 | font-size: 1.2em; 112 | font-weight: bold; 113 | text-transform: uppercase; 114 | letter-spacing: 2px; 115 | } 116 | 117 | #auth-content > h1:after { 118 | content: "\2731"; 119 | margin-left: 5px; 120 | } 121 | 122 | #auth-content > form { 123 | font-size: 0.9em; 124 | } 125 | 126 | #auth-content > form .field:last-child { 127 | margin-top: 30px; 128 | } 129 | 130 | #auth-content > form > fieldset { 131 | margin-top: 30px; 132 | } 133 | -------------------------------------------------------------------------------- /ui/loading.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 35 | 36 | 46 | -------------------------------------------------------------------------------- /ui/robots.txt: -------------------------------------------------------------------------------- 1 | user-agent: * 2 | 3 | Disallow: / 4 | Allow: /$ -------------------------------------------------------------------------------- /ui/stream/common.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | /** 19 | * Get one unsafe random number 20 | * 21 | * @param {number} min Min value (included) 22 | * @param {number} max Max value (not included) 23 | * 24 | * @returns {number} Get random number 25 | * 26 | */ 27 | export function getRand(min, max) { 28 | return Math.floor(Math.random() * (max - min + 1) + min); 29 | } 30 | 31 | /** 32 | * Get a group of random number 33 | * 34 | * @param {number} n How many number to get 35 | * @param {number} min Min value (included) 36 | * @param {number} max Max value (not included) 37 | * 38 | * @returns {Array} A group of random number 39 | */ 40 | export function getRands(n, min, max) { 41 | let r = []; 42 | 43 | for (let i = 0; i < n; i++) { 44 | r.push(getRand(min, max)); 45 | } 46 | 47 | return r; 48 | } 49 | 50 | /** 51 | * Separate given buffer to multiple ones based on input max length 52 | * 53 | * @param {Uint8Array} buf Buffer to separate 54 | * @param {number} max Max length of each buffer 55 | * 56 | * @returns {Array} Separated buffers 57 | * 58 | */ 59 | export function separateBuffer(buf, max) { 60 | let start = 0, 61 | result = []; 62 | 63 | while (start < buf.length) { 64 | let remain = buf.length - start; 65 | 66 | if (remain <= max) { 67 | result.push(buf.slice(start, start + remain)); 68 | 69 | return result; 70 | } 71 | 72 | remain = max; 73 | 74 | result.push(buf.slice(start, start + remain)); 75 | start += remain; 76 | } 77 | } 78 | 79 | /** 80 | * Create an Uint8Array out of given binary string 81 | * 82 | * @param {string} str binary string 83 | * 84 | * @returns {Uint8Array} Separated buffers 85 | * 86 | */ 87 | export function buildBufferFromString(str) { 88 | let r = [], 89 | t = []; 90 | 91 | for (let i in str) { 92 | let c = str.charCodeAt(i); 93 | 94 | while (c > 0xff) { 95 | t.push(c & 0xff); 96 | c >>= 8; 97 | } 98 | 99 | r.push(c); 100 | 101 | for (let j = t.length; j > 0; j--) { 102 | r.push(t[j]); 103 | } 104 | 105 | t = []; 106 | } 107 | 108 | return new Uint8Array(r); 109 | } 110 | -------------------------------------------------------------------------------- /ui/stream/common_test.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | import assert from "assert"; 19 | import * as common from "./common.js"; 20 | 21 | describe("Common", () => { 22 | it("separateBuffer", async () => { 23 | let resultArr = []; 24 | const expected = new Uint8Array([ 25 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 26 | 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 27 | 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 28 | 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 29 | 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 30 | ]), 31 | sepSeg = common.separateBuffer(expected, 16); 32 | 33 | sepSeg.forEach((d) => { 34 | resultArr.push(...d); 35 | }); 36 | 37 | const result = new Uint8Array(resultArr); 38 | 39 | assert.deepStrictEqual(result, expected); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /ui/stream/exception.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | export default class Exception extends Error { 19 | /** 20 | * constructor 21 | * 22 | * @param {string} message error message 23 | * @param {boolean} temporary whether or not the error is temporary 24 | * 25 | */ 26 | constructor(message, temporary) { 27 | super(message); 28 | 29 | this.temporary = temporary; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/stream/header_test.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | import assert from "assert"; 19 | import * as header from "./header.js"; 20 | 21 | describe("Header", () => { 22 | it("Header", () => { 23 | let h = new header.Header(header.ECHO); 24 | 25 | h.set(63); 26 | 27 | let n = new header.Header(h.value()); 28 | 29 | assert.strictEqual(h.type(), n.type()); 30 | assert.strictEqual(h.data(), n.data()); 31 | assert.strictEqual(n.type(), header.CONTROL); 32 | assert.strictEqual(n.data(), 63); 33 | }); 34 | 35 | it("Stream", () => { 36 | let h = new header.Stream(0, 0); 37 | 38 | h.set(header.STREAM_MAX_MARKER, header.STREAM_MAX_LENGTH); 39 | 40 | assert.strictEqual(h.marker(), header.STREAM_MAX_MARKER); 41 | assert.strictEqual(h.length(), header.STREAM_MAX_LENGTH); 42 | 43 | assert.strictEqual(h.headerByte1, 0xff); 44 | assert.strictEqual(h.headerByte2, 0xff); 45 | }); 46 | 47 | it("InitialStream", () => { 48 | let h = new header.InitialStream(0, 0); 49 | 50 | h.set(15, 128, true); 51 | 52 | assert.strictEqual(h.command(), 15); 53 | assert.strictEqual(h.data(), 128); 54 | assert.strictEqual(h.success(), true); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /ui/stream/sender_test.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | import assert from "assert"; 19 | import * as sender from "./sender.js"; 20 | 21 | describe("Sender", () => { 22 | function generateTestData(size) { 23 | let d = new Uint8Array(size); 24 | 25 | for (let i = 0; i < d.length; i++) { 26 | d[i] = i % 256; 27 | } 28 | 29 | return d; 30 | } 31 | 32 | it("Send", async () => { 33 | const maxSegSize = 64; 34 | let result = []; 35 | let sd = new sender.Sender( 36 | (rawData) => { 37 | return new Promise((resolve) => { 38 | setTimeout(() => { 39 | for (let i in rawData) { 40 | result.push(rawData[i]); 41 | } 42 | 43 | resolve(); 44 | }, 5); 45 | }); 46 | }, 47 | maxSegSize, 48 | 300, 49 | 3, 50 | ); 51 | let expected = generateTestData(maxSegSize * 16); 52 | 53 | sd.send(expected); 54 | 55 | let sendCompleted = new Promise((resolve) => { 56 | let timer = setInterval(() => { 57 | if (result.length < expected.length) { 58 | return; 59 | } 60 | 61 | clearInterval(timer); 62 | timer = null; 63 | resolve(); 64 | }, 100); 65 | }); 66 | 67 | await sendCompleted; 68 | 69 | assert.deepStrictEqual(new Uint8Array(result), expected); 70 | }); 71 | 72 | it("Send (Multiple calls)", async () => { 73 | const maxSegSize = 64; 74 | let result = []; 75 | let sd = new sender.Sender( 76 | (rawData) => { 77 | return new Promise((resolve) => { 78 | setTimeout(() => { 79 | for (let i in rawData) { 80 | result.push(rawData[i]); 81 | } 82 | 83 | resolve(); 84 | }, 10); 85 | }); 86 | }, 87 | maxSegSize, 88 | 300, 89 | 100, 90 | ); 91 | let expectedSingle = generateTestData(maxSegSize * 2), 92 | expectedLen = expectedSingle.length * 16, 93 | expected = new Uint8Array(expectedLen); 94 | 95 | for (let i = 0; i < expectedLen; i += expectedSingle.length) { 96 | expected.set(expectedSingle, i); 97 | } 98 | 99 | for (let i = 0; i < expectedLen; i += expectedSingle.length) { 100 | setTimeout(() => { 101 | sd.send(expectedSingle); 102 | }, 100); 103 | } 104 | 105 | let sendCompleted = new Promise((resolve) => { 106 | let timer = setInterval(() => { 107 | if (result.length < expectedLen) { 108 | return; 109 | } 110 | 111 | clearInterval(timer); 112 | timer = null; 113 | resolve(); 114 | }, 100); 115 | }); 116 | 117 | await sendCompleted; 118 | 119 | assert.deepStrictEqual(new Uint8Array(result), expected); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /ui/stream/streams_test.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | describe("Streams", () => { 19 | it("Header", () => {}); 20 | }); 21 | -------------------------------------------------------------------------------- /ui/stream/subscribe.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | import Exception from "./exception.js"; 19 | 20 | const typeReject = 0; 21 | const typeResolve = 1; 22 | 23 | export class Subscribe { 24 | /** 25 | * constructor 26 | * 27 | */ 28 | constructor() { 29 | this.res = null; 30 | this.rej = null; 31 | this.pending = []; 32 | this.disabled = null; 33 | } 34 | 35 | /** 36 | * Returns how many resolve/reject in the pending 37 | */ 38 | pendings() { 39 | return ( 40 | this.pending.length + (this.rej !== null || this.res !== null ? 1 : 0) 41 | ); 42 | } 43 | 44 | /** 45 | * Resolve the subscribe waiter 46 | * 47 | * @param {any} d Resolve data which will be send to the subscriber 48 | */ 49 | resolve(d) { 50 | if (this.res !== null) { 51 | this.res(d); 52 | 53 | return; 54 | } 55 | 56 | this.pending.push([typeResolve, d]); 57 | } 58 | 59 | /** 60 | * Reject the subscribe waiter 61 | * 62 | * @param {any} e Error message that will be send to the subscriber 63 | * 64 | */ 65 | reject(e) { 66 | if (this.rej !== null) { 67 | this.rej(e); 68 | 69 | return; 70 | } 71 | 72 | this.pending.push([typeReject, e]); 73 | } 74 | 75 | /** 76 | * Waiting and receive subscribe data 77 | * 78 | * @returns {Promise} Data receiver 79 | * 80 | */ 81 | subscribe() { 82 | if (this.pending.length > 0) { 83 | let p = this.pending.shift(); 84 | 85 | switch (p[0]) { 86 | case typeReject: 87 | throw p[1]; 88 | 89 | case typeResolve: 90 | return p[1]; 91 | 92 | default: 93 | throw new Exception("Unknown pending type", false); 94 | } 95 | } 96 | 97 | if (this.disabled) { 98 | throw new Exception(this.disabled, false); 99 | } 100 | 101 | let self = this; 102 | 103 | return new Promise((resolve, reject) => { 104 | self.res = (d) => { 105 | self.res = null; 106 | self.rej = null; 107 | 108 | resolve(d); 109 | }; 110 | 111 | self.rej = (e) => { 112 | self.res = null; 113 | self.rej = null; 114 | 115 | reject(e); 116 | }; 117 | }); 118 | } 119 | 120 | /** 121 | * Disable current subscriber when all internal data is readed 122 | * 123 | * @param {string} reason Reason of the disable 124 | * 125 | */ 126 | disable(reason) { 127 | this.disabled = reason; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /ui/widgets/busy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ui/widgets/connect.css: -------------------------------------------------------------------------------- 1 | /* 2 | // Sshwifty - A Web SSH client 3 | // 4 | // Copyright (C) 2019-2025 Ni Rui 5 | // 6 | // This program is free software: you can redistribute it and/or modify 7 | // it under the terms of the GNU Affero General Public License as 8 | // published by the Free Software Foundation, either version 3 of the 9 | // License, or (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU Affero General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Affero General Public License 17 | // along with this program. If not, see . 18 | */ 19 | 20 | @charset "utf-8"; 21 | 22 | #connect { 23 | z-index: 999999; 24 | top: 40px; 25 | left: 159px; 26 | display: none; 27 | background: #333; 28 | width: 700px; 29 | } 30 | 31 | #connect-frame { 32 | z-index: 0; 33 | position: relative; 34 | } 35 | 36 | #connect .window-frame { 37 | max-height: calc(100vh - 40px); 38 | overflow: auto; 39 | } 40 | 41 | #connect:before { 42 | left: 30px; 43 | background: #333; 44 | } 45 | 46 | @media (max-width: 1024px) { 47 | #connect { 48 | left: 20px; 49 | right: 20px; 50 | width: auto; 51 | } 52 | 53 | #connect:before { 54 | left: 169px; 55 | } 56 | } 57 | 58 | @media (max-width: 768px) { 59 | #connect:before { 60 | left: 149px; 61 | } 62 | } 63 | 64 | #connect.display { 65 | display: block; 66 | } 67 | 68 | #connect h1 { 69 | padding: 15px 15px 0 15px; 70 | margin-bottom: 10px; 71 | color: #999; 72 | } 73 | 74 | #connect-close { 75 | cursor: pointer; 76 | color: #999; 77 | right: 10px; 78 | top: 20px; 79 | } 80 | 81 | #connect-busy-overlay { 82 | z-index: 2; 83 | background: #2229 url("busy.svg") center center no-repeat; 84 | top: 0; 85 | left: 0; 86 | bottom: 0; 87 | right: 0; 88 | position: absolute; 89 | backdrop-filter: blur(1px); 90 | } 91 | 92 | #connect-warning { 93 | padding: 20px; 94 | font-size: 0.85em; 95 | background: #b44; 96 | color: #fff; 97 | } 98 | 99 | #connect-warning-icon { 100 | float: left; 101 | display: block; 102 | margin: 5px 20px 5px 0; 103 | } 104 | 105 | #connect-warning-icon::after { 106 | background: #c55; 107 | } 108 | 109 | #connect-warning-msg { 110 | overflow: auto; 111 | } 112 | 113 | #connect-warning-msg p { 114 | margin: 0 0 5px 0; 115 | } 116 | 117 | #connect-warning-msg a { 118 | color: #faa; 119 | text-decoration: underline; 120 | } 121 | -------------------------------------------------------------------------------- /ui/widgets/connect_new.css: -------------------------------------------------------------------------------- 1 | /* 2 | // Sshwifty - A Web SSH client 3 | // 4 | // Copyright (C) 2019-2025 Ni Rui 5 | // 6 | // This program is free software: you can redistribute it and/or modify 7 | // it under the terms of the GNU Affero General Public License as 8 | // published by the Free Software Foundation, either version 3 of the 9 | // License, or (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU Affero General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Affero General Public License 17 | // along with this program. If not, see . 18 | */ 19 | 20 | @charset "utf-8"; 21 | 22 | #connect-new { 23 | min-height: 200px; 24 | background: #3a3a3a; 25 | font-size: 0.75em; 26 | padding: 15px; 27 | } 28 | 29 | #connect-new li .lst-wrap:hover { 30 | background: #544; 31 | } 32 | 33 | #connect-new li .lst-wrap:active { 34 | background: #444; 35 | } 36 | 37 | #connect-new li .lst-wrap { 38 | cursor: pointer; 39 | color: #aaa; 40 | padding: 15px; 41 | } 42 | 43 | #connect-new li h2 { 44 | color: #e9a; 45 | } 46 | 47 | #connect-new li h2::before { 48 | content: ">"; 49 | margin: 0 5px 0 0; 50 | color: #555; 51 | font-weight: normal; 52 | transition: ease 0.3s margin; 53 | } 54 | 55 | #connect-new li .lst-wrap:hover h2::before { 56 | content: ">"; 57 | margin: 0 3px 0 2px; 58 | } 59 | -------------------------------------------------------------------------------- /ui/widgets/connect_new.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 36 | 37 | 54 | -------------------------------------------------------------------------------- /ui/widgets/connect_switch.css: -------------------------------------------------------------------------------- 1 | /* 2 | // Sshwifty - A Web SSH client 3 | // 4 | // Copyright (C) 2019-2025 Ni Rui 5 | // 6 | // This program is free software: you can redistribute it and/or modify 7 | // it under the terms of the GNU Affero General Public License as 8 | // published by the Free Software Foundation, either version 3 of the 9 | // License, or (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU Affero General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Affero General Public License 17 | // along with this program. If not, see . 18 | */ 19 | 20 | @charset "utf-8"; 21 | 22 | #connect-switch { 23 | font-size: 0.88em; 24 | color: #aaa; 25 | clear: both; 26 | border-color: #555; 27 | } 28 | 29 | #connect-switch li .label { 30 | padding: 2px 7px; 31 | margin-left: 3px; 32 | font-size: 0.85em; 33 | background: #444; 34 | border-radius: 3px; 35 | } 36 | 37 | #connect-switch li.active { 38 | border-color: #555; 39 | background: #3a3a3a; 40 | } 41 | 42 | #connect-switch li.active .label { 43 | background: #888; 44 | } 45 | 46 | #connect-switch li.disabled { 47 | color: #666; 48 | } 49 | 50 | #connect-switch.red { 51 | border-color: #a56; 52 | } 53 | 54 | #connect-switch.red li.active { 55 | border-color: #a56; 56 | } 57 | -------------------------------------------------------------------------------- /ui/widgets/connect_switch.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | 32 | 53 | -------------------------------------------------------------------------------- /ui/widgets/connector.css: -------------------------------------------------------------------------------- 1 | /* 2 | // Sshwifty - A Web SSH client 3 | // 4 | // Copyright (C) 2019-2025 Ni Rui 5 | // 6 | // This program is free software: you can redistribute it and/or modify 7 | // it under the terms of the GNU Affero General Public License as 8 | // published by the Free Software Foundation, either version 3 of the 9 | // License, or (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU Affero General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Affero General Public License 17 | // along with this program. If not, see . 18 | */ 19 | 20 | @charset "utf-8"; 21 | 22 | #connector { 23 | padding: 0 20px 40px 20px; 24 | } 25 | 26 | #connector-cancel { 27 | text-decoration: none; 28 | color: #e9a; 29 | } 30 | 31 | #connector-cancel.disabled { 32 | color: #444; 33 | } 34 | 35 | #connector-cancel::before { 36 | content: "\000AB"; 37 | margin-right: 3px; 38 | } 39 | 40 | #connector-title { 41 | margin-top: 10px; 42 | text-align: center; 43 | font-size: 0.9em; 44 | color: #aaa; 45 | } 46 | 47 | #connector-title > h2 { 48 | color: #e9a; 49 | font-size: 1.3em; 50 | font-weight: bold; 51 | margin: 3px 0; 52 | } 53 | 54 | #connector-title.big { 55 | margin: 50px 0; 56 | } 57 | 58 | #connector-title.big > h2 { 59 | margin: 10px 0; 60 | } 61 | 62 | #connector-fields { 63 | margin-top: 10px; 64 | font-size: 0.9em; 65 | } 66 | 67 | #connector-continue { 68 | margin-top: 10px; 69 | font-size: 0.9em; 70 | } 71 | 72 | #connector-proccess { 73 | margin-top: 10px; 74 | text-align: center; 75 | font-size: 0.9em; 76 | color: #aaa; 77 | } 78 | 79 | #connector-proccess-message { 80 | margin: 30px 0; 81 | } 82 | 83 | #connector-proccess-message > h2 { 84 | font-weight: normal; 85 | margin: 10px 0; 86 | color: #e9a; 87 | font-size: 1.2em; 88 | } 89 | 90 | #connector-proccess-message > h2 > span { 91 | padding: 2px 10px; 92 | border: 2px solid transparent; 93 | display: inline-block; 94 | } 95 | 96 | @keyframes connector-proccess-message-alert { 97 | 0% { 98 | border-color: transparent; 99 | } 100 | 101 | 50% { 102 | outline: 2px solid #e9a; 103 | } 104 | 105 | 60% { 106 | border-color: #e9a; 107 | outline: none; 108 | } 109 | } 110 | 111 | #connector-proccess-message.alert > h2 > span { 112 | outline: 2px solid transparent; 113 | animation-name: connector-proccess-message-alert; 114 | animation-duration: 1.5s; 115 | animation-iteration-count: infinite; 116 | animation-direction: normal; 117 | animation-timing-function: steps(1, end); 118 | } 119 | 120 | #connector-proccess-indicater { 121 | width: 100%; 122 | margin: 20px auto; 123 | padding: 0; 124 | } 125 | -------------------------------------------------------------------------------- /ui/widgets/screens.css: -------------------------------------------------------------------------------- 1 | /* 2 | // Sshwifty - A Web SSH client 3 | // 4 | // Copyright (C) 2019-2025 Ni Rui 5 | // 6 | // This program is free software: you can redistribute it and/or modify 7 | // it under the terms of the GNU Affero General Public License as 8 | // published by the Free Software Foundation, either version 3 of the 9 | // License, or (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU Affero General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Affero General Public License 17 | // along with this program. If not, see . 18 | */ 19 | 20 | @charset "utf-8"; 21 | 22 | #home-content.active { 23 | min-height: 0; 24 | } 25 | 26 | #home-content > .screen { 27 | display: flex; 28 | justify-content: start; 29 | flex-direction: column; 30 | font-size: 1em; 31 | overflow: hidden; 32 | flex: auto; 33 | } 34 | 35 | #home-content > .screen.screen-inactive { 36 | flex: 0 0 0; 37 | } 38 | 39 | #home-content > .screen > .screen-error { 40 | display: block; 41 | padding: 10px; 42 | background: #b44; 43 | color: #fff; 44 | font-size: 0.75em; 45 | flex: 0 0; 46 | } 47 | 48 | #home-content > .screen > .screen-error.screen-error-level-error { 49 | background: #b44; 50 | } 51 | 52 | #home-content > .screen > .screen-error.screen-error-level-warning { 53 | background: #b82; 54 | } 55 | 56 | #home-content > .screen > .screen-error.screen-error-level-info { 57 | background: #28b; 58 | } 59 | 60 | #home-content > .screen > .screen-screen { 61 | flex: auto; 62 | padding: 0; 63 | margin: 0; 64 | position: relative; 65 | min-height: 0; 66 | } 67 | 68 | #home-content > .screen > .screen-screen > .screen-content { 69 | width: 100%; 70 | height: 100%; 71 | padding: 0; 72 | margin: 0; 73 | position: relative; 74 | overflow: hidden; 75 | } 76 | -------------------------------------------------------------------------------- /ui/widgets/screens.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 59 | 60 | 108 | -------------------------------------------------------------------------------- /ui/widgets/tab_list.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 50 | 51 | 114 | -------------------------------------------------------------------------------- /ui/widgets/tab_window.css: -------------------------------------------------------------------------------- 1 | /* 2 | // Sshwifty - A Web SSH client 3 | // 4 | // Copyright (C) 2019-2025 Ni Rui 5 | // 6 | // This program is free software: you can redistribute it and/or modify 7 | // it under the terms of the GNU Affero General Public License as 8 | // published by the Free Software Foundation, either version 3 of the 9 | // License, or (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU Affero General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Affero General Public License 17 | // along with this program. If not, see . 18 | */ 19 | 20 | @charset "utf-8"; 21 | 22 | #tab-window { 23 | z-index: 999999; 24 | top: 40px; 25 | right: 0px; 26 | display: none; 27 | width: 400px; 28 | background: #333; 29 | } 30 | 31 | #tab-window .window-frame { 32 | max-height: calc(100vh - 40px); 33 | overflow: auto; 34 | } 35 | 36 | #tab-window:before { 37 | right: 19px; 38 | background: #333; 39 | } 40 | 41 | @media (max-width: 768px) { 42 | #tab-window { 43 | width: 80%; 44 | } 45 | } 46 | 47 | #tab-window.display { 48 | display: block; 49 | } 50 | 51 | #tab-window-close { 52 | cursor: pointer; 53 | right: 10px; 54 | top: 20px; 55 | color: #999; 56 | } 57 | 58 | #tab-window h1 { 59 | padding: 15px 15px 0 15px; 60 | margin-bottom: 10px; 61 | color: #999; 62 | } 63 | 64 | #tab-window-list > li > .lst-wrap { 65 | padding: 10px 20px; 66 | cursor: pointer; 67 | } 68 | 69 | #tab-window-list > li { 70 | border-bottom: none; 71 | } 72 | 73 | #tab-window-tabs { 74 | flex: auto; 75 | overflow: hidden; 76 | } 77 | 78 | #tab-window-tabs > li { 79 | display: flex; 80 | position: relative; 81 | padding: 15px; 82 | opacity: 0.5; 83 | color: #999; 84 | cursor: pointer; 85 | } 86 | 87 | #tab-window-tabs > li::after { 88 | content: " "; 89 | display: block; 90 | position: absolute; 91 | top: 5px; 92 | bottom: 5px; 93 | left: 0; 94 | width: 0; 95 | transition: all 0.1s linear; 96 | transition-property: width, top, bottom; 97 | } 98 | 99 | #tab-window-tabs > li.active::after { 100 | top: 0; 101 | bottom: 0; 102 | } 103 | 104 | #tab-window-tabs > li.updated::after { 105 | background: #fff3; 106 | width: 5px; 107 | } 108 | 109 | #tab-window-tabs > li.error::after { 110 | background: #d55; 111 | width: 5px; 112 | } 113 | 114 | #tab-window-tabs > li > span.title { 115 | text-overflow: ellipsis; 116 | overflow: hidden; 117 | display: inline-block; 118 | } 119 | 120 | #tab-window-tabs > li > span.title > span.type { 121 | display: inline-block; 122 | font-size: 0.85em; 123 | font-weight: bold; 124 | margin-right: 3px; 125 | text-transform: uppercase; 126 | color: #fff; 127 | background: #222; 128 | padding: 1px 4px; 129 | border-radius: 2px; 130 | } 131 | 132 | #tab-window-tabs > li > .icon-close { 133 | display: block; 134 | position: absolute; 135 | top: 50%; 136 | right: 10px; 137 | margin-top: -5px; 138 | color: #fff6; 139 | } 140 | 141 | #tab-window-tabs > li.active { 142 | color: #fff; 143 | opacity: 1; 144 | } 145 | 146 | #tab-window-tabs > li.active > span.title { 147 | padding-right: 20px; 148 | } 149 | -------------------------------------------------------------------------------- /ui/widgets/tab_window.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 40 | 41 | 81 | -------------------------------------------------------------------------------- /ui/widgets/tabs.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 41 | 42 | 78 | -------------------------------------------------------------------------------- /ui/widgets/window.css: -------------------------------------------------------------------------------- 1 | /* 2 | // Sshwifty - A Web SSH client 3 | // 4 | // Copyright (C) 2019-2025 Ni Rui 5 | // 6 | // This program is free software: you can redistribute it and/or modify 7 | // it under the terms of the GNU Affero General Public License as 8 | // published by the Free Software Foundation, either version 3 of the 9 | // License, or (at your option) any later version. 10 | // 11 | // This program is distributed in the hope that it will be useful, 12 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | // GNU Affero General Public License for more details. 15 | // 16 | // You should have received a copy of the GNU Affero General Public License 17 | // along with this program. If not, see . 18 | */ 19 | 20 | @charset "utf-8"; 21 | -------------------------------------------------------------------------------- /ui/widgets/window.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 37 | 38 | 78 | -------------------------------------------------------------------------------- /ui/xhr.js: -------------------------------------------------------------------------------- 1 | // Sshwifty - A Web SSH client 2 | // 3 | // Copyright (C) 2019-2025 Ni Rui 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU Affero General Public License as 7 | // published by the Free Software Foundation, either version 3 of the 8 | // License, or (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU Affero General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU Affero General Public License 16 | // along with this program. If not, see . 17 | 18 | function send(method, url, headers) { 19 | return new Promise((res, rej) => { 20 | let authReq = new XMLHttpRequest(); 21 | 22 | authReq.addEventListener("readystatechange", () => { 23 | if (authReq.readyState !== authReq.DONE) { 24 | return; 25 | } 26 | 27 | res(authReq); 28 | }); 29 | 30 | authReq.addEventListener("error", (e) => { 31 | rej(e); 32 | }); 33 | 34 | authReq.addEventListener("timeout", (e) => { 35 | rej(e); 36 | }); 37 | 38 | authReq.open(method, url, true); 39 | 40 | for (let h in headers) { 41 | authReq.setRequestHeader(h, headers[h]); 42 | } 43 | 44 | authReq.send(); 45 | }); 46 | } 47 | 48 | export function get(url, headers) { 49 | return send("GET", url, headers); 50 | } 51 | 52 | export function options(url, headers) { 53 | return send("OPTIONS", url, headers); 54 | } 55 | --------------------------------------------------------------------------------