Implemented the host name auto suggestion, and added Preset feature
This commit is contained in:
@@ -54,7 +54,9 @@ ENV SSHWIFTY_HOSTNAME= \
|
||||
SSHWIFTY_TLSCERTIFICATEFILE= \
|
||||
SSHWIFTY_TLSCERTIFICATEKEYFILE= \
|
||||
SSHWIFTY_DOCKER_TLSCERT= \
|
||||
SSHWIFTY_DOCKER_TLSCERTKEY=
|
||||
SSHWIFTY_DOCKER_TLSCERTKEY= \
|
||||
SSHWIFTY_PRESETS= \
|
||||
SSHWIFTY_ONLYALLOWPRESETREMOTES=
|
||||
COPY --from=builder /sshwifty /
|
||||
COPY . /sshwifty-src
|
||||
RUN set -ex && \
|
||||
|
||||
84
README.md
84
README.md
@@ -50,9 +50,10 @@ $ docker run --detach \
|
||||
```
|
||||
|
||||
The `domain.crt` and `domain.key` must be valid TLS certificate and key file
|
||||
located on the machine which the `docker run` command will be executed.
|
||||
located on the same machine which the `docker run` command will be executed
|
||||
upon.
|
||||
|
||||
[Docker]: https://www.docker.com
|
||||
[docker]: https://www.docker.com
|
||||
|
||||
### Compile from source code (Recommanded if you're a developer)
|
||||
|
||||
@@ -75,7 +76,7 @@ When done, you can found the newly generated `sshwifty` binary inside current
|
||||
working directory.
|
||||
|
||||
Notice: `Dockerfile` contains the entire build procedure of this software.
|
||||
Please refer to it when you encountered any compile/build related problem.
|
||||
Please refer to it when you encountered any compile/build related issue.
|
||||
|
||||
### Deploy on the cloud
|
||||
|
||||
@@ -99,8 +100,8 @@ Sshwifty can be configured through either file or environment variables. By
|
||||
default, the configuration loader will try to load file from default paths
|
||||
first, when failed, environment variables will be used.
|
||||
|
||||
You can also specify your own configuration file by setting `SSHWIFTY_CONFIG`
|
||||
environment variable. For example:
|
||||
You can also specify your own configuration file by setting up `SSHWIFTY_CONFIG`
|
||||
environment variable before start the software. For example:
|
||||
|
||||
```
|
||||
$ SSHWIFTY_CONFIG=./sshwifty.conf.json ./sshwifty
|
||||
@@ -188,7 +189,66 @@ Here is all the options of a configuration file:
|
||||
"InitialTimeout": 3,
|
||||
.....
|
||||
}
|
||||
]
|
||||
],
|
||||
|
||||
// Remote Presets, the operate can define few presets for user so the user
|
||||
// won't have to manually fill in all the form fields
|
||||
//
|
||||
// Presets will be displayed in the "Known remotes" table on the Connector
|
||||
// window
|
||||
"Presets": [
|
||||
{
|
||||
// Title of the preset
|
||||
"Title": "SDF.org Unix Shell",
|
||||
|
||||
// Preset Types, i.e. Telnet, and SSH
|
||||
"Type": "SSH",
|
||||
|
||||
// Target address and port
|
||||
"Host": "sdf.org:22",
|
||||
|
||||
// Form fields and values, you have to manually validate the correctness
|
||||
// of the field value
|
||||
"Meta": {
|
||||
// Data for predefined User field
|
||||
"User": "pre-defined-username",
|
||||
|
||||
// Data for predefined Encoding field. Valid data is those displayed on
|
||||
// the page
|
||||
"Encoding": "pre-defined-encoding",
|
||||
|
||||
// Data for predefined Password field
|
||||
"Password": "pre-defined-password",
|
||||
|
||||
// Data for predefined Private Key field
|
||||
"Private Key": "pre-defined-private-key",
|
||||
|
||||
// Data for predefined Authentication field. Valid values is what
|
||||
// displayed on the page (Password, Private Key, None)
|
||||
"Authentication": "Password",
|
||||
}
|
||||
},
|
||||
{
|
||||
"Title": "Endpoint Telnet",
|
||||
"Type": "Telnet",
|
||||
"Host": "endpoint.vaguly.com",
|
||||
"Meta": {
|
||||
// Data for predefined Encoding field. Valid data is those displayed on
|
||||
// the page
|
||||
"Encoding": "utf-8"
|
||||
....
|
||||
}
|
||||
},
|
||||
....
|
||||
],
|
||||
|
||||
// Allow the Preset Remotes only, and refuse to connect to any other remote
|
||||
// host
|
||||
//
|
||||
// NOTICE: You can only configure OnlyAllowPresetRemotes through a config
|
||||
// file. This option is not supported when you are configuring with
|
||||
// environment variables
|
||||
OnlyAllowPresetRemotes: false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -215,6 +275,8 @@ SSHWIFTY_WRITEELAY
|
||||
SSHWIFTY_LISTENINTERFACE
|
||||
SSHWIFTY_TLSCERTIFICATEFILE
|
||||
SSHWIFTY_TLSCERTIFICATEKEYFILE
|
||||
SSHWIFTY_PRESETS
|
||||
SSHWIFTY_ONLYALLOWPRESETREMOTES
|
||||
```
|
||||
|
||||
The option they represented is corresponded to their counterparts in the
|
||||
@@ -264,8 +326,8 @@ Code of this project is licensed under AGPL, see [LICENSE.md] for detail.
|
||||
Third-party components used by this project are licensed under their respective
|
||||
licenses. See [DEPENDENCIES.md] for dependencies used by this project.
|
||||
|
||||
[LICENSE.md]: LICENSE.md
|
||||
[DEPENDENCIES.md]: DEPENDENCIES.md
|
||||
[license.md]: LICENSE.md
|
||||
[dependencies.md]: DEPENDENCIES.md
|
||||
|
||||
## Contribute
|
||||
|
||||
@@ -274,10 +336,10 @@ Sorry.
|
||||
|
||||
Upon release (Which is then you're able to read this file), this project will
|
||||
enter _maintaining_ state, which includes doing bug fix and security updates.
|
||||
Adding new features however, is not a part of the state.
|
||||
_Adding new features however, is not a part of the state_.
|
||||
|
||||
Please do not send pull request. If you need new feature, fork it, and maintain
|
||||
it like one of your own project.
|
||||
Please do not send pull request. If you need new feature, fork it, add it by
|
||||
yourself, and maintain it like one of your own project.
|
||||
|
||||
(Notice: Typo, grammar error or invalid use of language in the source code and
|
||||
document is categorized as bug, please report them if you found any. Thank you!)
|
||||
|
||||
@@ -120,21 +120,33 @@ func (s Server) Verify() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Preset contains data of a static remote host
|
||||
type Preset struct {
|
||||
Title string
|
||||
Type string
|
||||
Host string
|
||||
Meta map[string]string
|
||||
}
|
||||
|
||||
// Configuration contains configuration of the application
|
||||
type Configuration struct {
|
||||
HostName string
|
||||
SharedKey string
|
||||
Dialer network.Dial
|
||||
DialTimeout time.Duration
|
||||
Servers []Server
|
||||
HostName string
|
||||
SharedKey string
|
||||
Dialer network.Dial
|
||||
DialTimeout time.Duration
|
||||
Servers []Server
|
||||
Presets []Preset
|
||||
OnlyAllowPresetRemotes bool
|
||||
}
|
||||
|
||||
// Common settings shared by mulitple servers
|
||||
type Common struct {
|
||||
HostName string
|
||||
SharedKey string
|
||||
Dialer network.Dial
|
||||
DialTimeout time.Duration
|
||||
HostName string
|
||||
SharedKey string
|
||||
Dialer network.Dial
|
||||
DialTimeout time.Duration
|
||||
Presets []Preset
|
||||
OnlyAllowPresetRemotes bool
|
||||
}
|
||||
|
||||
// Verify verifies current setting
|
||||
@@ -164,6 +176,16 @@ func (c Configuration) Common() Common {
|
||||
dialer = network.TCPDial()
|
||||
}
|
||||
|
||||
if c.OnlyAllowPresetRemotes {
|
||||
accessList := make(network.AllowedHosts, len(c.Presets))
|
||||
|
||||
for _, k := range c.Presets {
|
||||
accessList[k.Host] = struct{}{}
|
||||
}
|
||||
|
||||
dialer = network.AccessControlDial(accessList, dialer)
|
||||
}
|
||||
|
||||
dialTimeout := c.DialTimeout
|
||||
|
||||
if dialTimeout <= 1*time.Second {
|
||||
@@ -171,10 +193,12 @@ func (c Configuration) Common() Common {
|
||||
}
|
||||
|
||||
return Common{
|
||||
HostName: c.HostName,
|
||||
SharedKey: c.SharedKey,
|
||||
Dialer: c.Dialer,
|
||||
DialTimeout: c.DialTimeout,
|
||||
HostName: c.HostName,
|
||||
SharedKey: c.SharedKey,
|
||||
Dialer: dialer,
|
||||
DialTimeout: c.DialTimeout,
|
||||
Presets: c.Presets,
|
||||
OnlyAllowPresetRemotes: c.OnlyAllowPresetRemotes,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package configuration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -102,12 +103,27 @@ func Enviro() Loader {
|
||||
TLSCertificateKeyFile: parseEviro("SSHWIFTY_TLSCERTIFICATEKEYFILE"),
|
||||
}
|
||||
|
||||
presets := make([]Preset, 0, 16)
|
||||
presetStr := strings.TrimSpace(parseEviro("SSHWIFTY_PRESETS"))
|
||||
|
||||
if len(presetStr) > 0 {
|
||||
jErr := json.Unmarshal([]byte(presetStr), &presets)
|
||||
|
||||
if jErr != nil {
|
||||
return enviroTypeName, Configuration{}, fmt.Errorf(
|
||||
"Invalid \"SSHWIFTY_PRESETS\": %s", jErr)
|
||||
}
|
||||
}
|
||||
|
||||
return enviroTypeName, Configuration{
|
||||
HostName: cfg.HostName,
|
||||
SharedKey: cfg.SharedKey,
|
||||
Dialer: dialer,
|
||||
DialTimeout: time.Duration(cfg.DialTimeout) * time.Second,
|
||||
Servers: []Server{cfgSer.build()},
|
||||
Presets: presets,
|
||||
OnlyAllowPresetRemotes: len(
|
||||
parseEviro("SSHWIFTY_ONLYALLOWPRESETREMOTES")) > 0,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/niruix/sshwifty/application/network"
|
||||
|
||||
"github.com/niruix/sshwifty/application/log"
|
||||
"github.com/niruix/sshwifty/application/network"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -78,14 +77,49 @@ func (f *fileCfgServer) build() Server {
|
||||
}
|
||||
}
|
||||
|
||||
type fileCfgPreset struct {
|
||||
Title string
|
||||
Type string
|
||||
Host string
|
||||
Meta map[string]string
|
||||
}
|
||||
|
||||
func (f fileCfgPreset) build() Preset {
|
||||
return Preset{
|
||||
Title: f.Title,
|
||||
Type: strings.TrimSpace(f.Type),
|
||||
Host: f.Host,
|
||||
Meta: f.Meta,
|
||||
}
|
||||
}
|
||||
|
||||
type fileCfgCommon struct {
|
||||
HostName string // Host name
|
||||
SharedKey string // Shared key, empty to enable public access
|
||||
DialTimeout int // DialTimeout, min 5s
|
||||
Socks5 string // Socks5 server address, optional
|
||||
Socks5User string // Login user for socks5 server, optional
|
||||
Socks5Password string // Login pass for socks5 server, optional
|
||||
Servers []*fileCfgServer // Servers
|
||||
// Host name
|
||||
HostName string
|
||||
|
||||
// Shared key, empty to enable public access
|
||||
SharedKey string
|
||||
|
||||
// DialTimeout, min 5s
|
||||
DialTimeout int
|
||||
|
||||
// Socks5 server address, optional
|
||||
Socks5 string
|
||||
|
||||
// Login user for socks5 server, optional
|
||||
Socks5User string
|
||||
|
||||
// Login pass for socks5 server, optional
|
||||
Socks5Password string
|
||||
|
||||
// Servers
|
||||
Servers []*fileCfgServer
|
||||
|
||||
// Remotes
|
||||
Presets []*fileCfgPreset
|
||||
|
||||
// Allow predefined remotes only
|
||||
OnlyAllowPresetRemotes bool
|
||||
}
|
||||
|
||||
func (f fileCfgCommon) build() (fileCfgCommon, network.Dial, error) {
|
||||
@@ -111,13 +145,15 @@ func (f fileCfgCommon) build() (fileCfgCommon, network.Dial, error) {
|
||||
}
|
||||
|
||||
return fileCfgCommon{
|
||||
HostName: f.HostName,
|
||||
SharedKey: f.SharedKey,
|
||||
DialTimeout: dialTimeout,
|
||||
Socks5: f.Socks5,
|
||||
Socks5User: f.Socks5User,
|
||||
Socks5Password: f.Socks5Password,
|
||||
Servers: f.Servers,
|
||||
HostName: f.HostName,
|
||||
SharedKey: f.SharedKey,
|
||||
DialTimeout: dialTimeout,
|
||||
Socks5: f.Socks5,
|
||||
Socks5User: f.Socks5User,
|
||||
Socks5Password: f.Socks5Password,
|
||||
Servers: f.Servers,
|
||||
Presets: f.Presets,
|
||||
OnlyAllowPresetRemotes: f.OnlyAllowPresetRemotes,
|
||||
}, dialer, nil
|
||||
}
|
||||
|
||||
@@ -151,12 +187,21 @@ func loadFile(filePath string) (string, Configuration, error) {
|
||||
servers[i] = finalCfg.Servers[i].build()
|
||||
}
|
||||
|
||||
presets := make([]Preset, len(finalCfg.Presets))
|
||||
|
||||
for i := range presets {
|
||||
presets[i] = finalCfg.Presets[i].build()
|
||||
}
|
||||
|
||||
return fileTypeName, Configuration{
|
||||
HostName: finalCfg.HostName,
|
||||
SharedKey: finalCfg.SharedKey,
|
||||
Dialer: dialer,
|
||||
DialTimeout: time.Duration(finalCfg.DialTimeout) * time.Second,
|
||||
Servers: servers,
|
||||
HostName: finalCfg.HostName,
|
||||
SharedKey: finalCfg.SharedKey,
|
||||
Dialer: dialer,
|
||||
DialTimeout: time.Duration(finalCfg.DialTimeout) *
|
||||
time.Second,
|
||||
Servers: servers,
|
||||
Presets: presets,
|
||||
OnlyAllowPresetRemotes: cfg.OnlyAllowPresetRemotes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ type handler struct {
|
||||
logger log.Logger
|
||||
homeCtl home
|
||||
socketCtl socket
|
||||
socketVerifyCtl socketVerification
|
||||
}
|
||||
|
||||
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -80,6 +81,8 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
case "/sshwifty/socket":
|
||||
err = serveController(h.socketCtl, w, r, clientLogger)
|
||||
case "/sshwifty/socket/verify":
|
||||
err = serveController(h.socketVerifyCtl, w, r, clientLogger)
|
||||
|
||||
case "/robots.txt":
|
||||
err = serveStaticCacheData(
|
||||
@@ -155,12 +158,15 @@ func Builder(cmds command.Commands) server.HandlerBuilder {
|
||||
cfg configuration.Server,
|
||||
logger log.Logger,
|
||||
) http.Handler {
|
||||
socketCtl := newSocketCtl(commonCfg, cfg, cmds)
|
||||
|
||||
return handler{
|
||||
hostNameChecker: commonCfg.HostName + ":",
|
||||
commonCfg: commonCfg,
|
||||
logger: logger,
|
||||
homeCtl: home{},
|
||||
socketCtl: newSocketCtl(commonCfg, cfg, cmds),
|
||||
socketCtl: socketCtl,
|
||||
socketVerifyCtl: newSocketVerification(socketCtl, cfg, commonCfg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,54 +162,6 @@ func (s socket) Options(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s socket) setServerConfigHeader(hd *http.Header) {
|
||||
hd.Add("X-Heartbeat",
|
||||
strconv.FormatFloat(s.serverCfg.HeartbeatTimeout.Seconds(), 'g', 2, 64))
|
||||
hd.Add("X-Timeout",
|
||||
strconv.FormatFloat(s.serverCfg.ReadTimeout.Seconds(), 'g', 2, 64))
|
||||
}
|
||||
|
||||
func (s socket) Head(
|
||||
w http.ResponseWriter, r *http.Request, l log.Logger) error {
|
||||
key := r.Header.Get("X-Key")
|
||||
hd := w.Header()
|
||||
|
||||
if len(key) <= 0 {
|
||||
hd.Add("X-Key", s.randomKey)
|
||||
|
||||
if len(s.commonCfg.SharedKey) <= 0 {
|
||||
s.setServerConfigHeader(&hd)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrSocketAuthFailed
|
||||
}
|
||||
|
||||
if len(key) > 64 {
|
||||
return ErrSocketAuthFailed
|
||||
}
|
||||
|
||||
// Delay the brute force attack. Use it with connection limits (via
|
||||
// iptables or nginx etc)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
decodedKey, decodedKeyErr := base64.StdEncoding.DecodeString(key)
|
||||
|
||||
if decodedKeyErr != nil {
|
||||
return NewError(http.StatusBadRequest, decodedKeyErr.Error())
|
||||
}
|
||||
|
||||
if !hmac.Equal(s.authKey, decodedKey) {
|
||||
return ErrSocketAuthFailed
|
||||
}
|
||||
|
||||
hd.Add("X-Key", s.randomKey)
|
||||
s.setServerConfigHeader(&hd)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s socket) buildWSFetcher(c *websocket.Conn) rw.FetchReaderFetcher {
|
||||
return func() ([]byte, error) {
|
||||
for {
|
||||
|
||||
138
application/controller/socket_verify.go
Normal file
138
application/controller/socket_verify.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2020 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/niruix/sshwifty/application/configuration"
|
||||
"github.com/niruix/sshwifty/application/log"
|
||||
)
|
||||
|
||||
type socketVerification struct {
|
||||
socket
|
||||
|
||||
heartbeat string
|
||||
timeout string
|
||||
configRspBody []byte
|
||||
}
|
||||
|
||||
type socketRemotePreset struct {
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Meta map[string]string `json:"meta"`
|
||||
}
|
||||
|
||||
func buildAccessConfigRespondBody(remotes []configuration.Preset) []byte {
|
||||
presets := make([]socketRemotePreset, len(remotes))
|
||||
|
||||
for i := range presets {
|
||||
presets[i] = socketRemotePreset{
|
||||
Title: remotes[i].Title,
|
||||
Type: remotes[i].Type,
|
||||
Host: remotes[i].Host,
|
||||
Meta: remotes[i].Meta,
|
||||
}
|
||||
}
|
||||
|
||||
mData, mErr := json.Marshal(presets)
|
||||
|
||||
if mErr != nil {
|
||||
panic(fmt.Errorf("Unable to marshal remote data: %s", mErr))
|
||||
}
|
||||
|
||||
return mData
|
||||
}
|
||||
|
||||
func newSocketVerification(
|
||||
s socket,
|
||||
srvCfg configuration.Server,
|
||||
commCfg configuration.Common,
|
||||
) socketVerification {
|
||||
return socketVerification{
|
||||
socket: s,
|
||||
heartbeat: strconv.FormatFloat(
|
||||
srvCfg.HeartbeatTimeout.Seconds(), 'g', 2, 64),
|
||||
timeout: strconv.FormatFloat(
|
||||
srvCfg.ReadTimeout.Seconds(), 'g', 2, 64),
|
||||
configRspBody: buildAccessConfigRespondBody(commCfg.Presets),
|
||||
}
|
||||
}
|
||||
|
||||
func (s socketVerification) setServerConfigRespond(
|
||||
hd *http.Header, w http.ResponseWriter) {
|
||||
hd.Add("X-Heartbeat", s.heartbeat)
|
||||
hd.Add("X-Timeout", s.timeout)
|
||||
|
||||
if s.commonCfg.OnlyAllowPresetRemotes {
|
||||
hd.Add("X-OnlyAllowPresetRemotes", "yes")
|
||||
}
|
||||
|
||||
hd.Add("Content-Type", "text/json; charset=utf-8")
|
||||
|
||||
w.Write(s.configRspBody)
|
||||
}
|
||||
|
||||
func (s socketVerification) Get(
|
||||
w http.ResponseWriter, r *http.Request, l log.Logger) error {
|
||||
hd := w.Header()
|
||||
|
||||
key := r.Header.Get("X-Key")
|
||||
|
||||
if len(key) <= 0 {
|
||||
hd.Add("X-Key", s.randomKey)
|
||||
|
||||
if len(s.commonCfg.SharedKey) <= 0 {
|
||||
s.setServerConfigRespond(&hd, w)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrSocketAuthFailed
|
||||
}
|
||||
|
||||
if len(key) > 64 {
|
||||
return ErrSocketAuthFailed
|
||||
}
|
||||
|
||||
// Delay the brute force attack. Use it with connection limits (via
|
||||
// iptables or nginx etc)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
decodedKey, decodedKeyErr := base64.StdEncoding.DecodeString(key)
|
||||
|
||||
if decodedKeyErr != nil {
|
||||
return NewError(http.StatusBadRequest, decodedKeyErr.Error())
|
||||
}
|
||||
|
||||
if !hmac.Equal(s.authKey, decodedKey) {
|
||||
return ErrSocketAuthFailed
|
||||
}
|
||||
|
||||
hd.Add("X-Key", s.randomKey)
|
||||
s.setServerConfigRespond(&hd, w)
|
||||
|
||||
return nil
|
||||
}
|
||||
61
application/network/dial_ac.go
Normal file
61
application/network/dial_ac.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2020 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package network
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrAccessControlDialTargetHostNotAllowed = errors.New(
|
||||
"Refuse to connect to specified target host due to outgoing " +
|
||||
"restriction")
|
||||
)
|
||||
|
||||
// AllowedHosts contains a map of allowed remote hosts
|
||||
type AllowedHosts map[string]struct{}
|
||||
|
||||
// Allowed returns whether or not given host is allowed
|
||||
func (a AllowedHosts) Allowed(host string) bool {
|
||||
_, ok := a[host]
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// AllowedHost returns whether or not give host is allowed
|
||||
type AllowedHost interface {
|
||||
Allowed(host string) bool
|
||||
}
|
||||
|
||||
// AccessControlDial creates an access controlled Dial
|
||||
func AccessControlDial(allowed AllowedHost, dial Dial) Dial {
|
||||
return func(
|
||||
network string,
|
||||
address string,
|
||||
timeout time.Duration,
|
||||
) (net.Conn, error) {
|
||||
if !allowed.Allowed(address) {
|
||||
return nil, ErrAccessControlDialTargetHostNotAllowed
|
||||
}
|
||||
|
||||
return dial(network, address, timeout)
|
||||
}
|
||||
}
|
||||
@@ -18,5 +18,36 @@
|
||||
"TLSCertificateFile": "",
|
||||
"TLSCertificateKeyFile": ""
|
||||
}
|
||||
]
|
||||
],
|
||||
"Presets": [
|
||||
{
|
||||
"Title": "SDF.org Unix Shell",
|
||||
"Type": "SSH",
|
||||
"Host": "sdf.org:22",
|
||||
"Meta": {
|
||||
"Encoding": "utf-8",
|
||||
"Authentication": "Password"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Title": "My own super secure server",
|
||||
"Type": "SSH",
|
||||
"Host": "localhost",
|
||||
"Meta": {
|
||||
"User": "root",
|
||||
"Encoding": "utf-8",
|
||||
"Private Key": "--------- BEGIN RSA PRIVATE KEY ---------",
|
||||
"Authentication": "Private Key"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Title": "My own super expensive router",
|
||||
"Type": "Telnet",
|
||||
"Host": "10.0.0.1",
|
||||
"Meta": {
|
||||
"Encoding": "ibm866"
|
||||
}
|
||||
}
|
||||
],
|
||||
"OnlyAllowPresetRemotes": false
|
||||
}
|
||||
|
||||
116
ui/app.js
116
ui/app.js
@@ -15,28 +15,24 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import "./common.css";
|
||||
import "./app.css";
|
||||
import "./landing.css";
|
||||
|
||||
import { Socket } from "./socket.js";
|
||||
|
||||
import Vue from "vue";
|
||||
import Home from "./home.vue";
|
||||
import "./app.css";
|
||||
import Auth from "./auth.vue";
|
||||
import Loading from "./loading.vue";
|
||||
|
||||
import { Color as ControlColor } from "./commands/color.js";
|
||||
import { Commands } from "./commands/commands.js";
|
||||
import { Controls } from "./commands/controls.js";
|
||||
import { Presets } from "./commands/presets.js";
|
||||
import * as ssh from "./commands/ssh.js";
|
||||
import * as telnet from "./commands/telnet.js";
|
||||
|
||||
import { Controls } from "./commands/controls.js";
|
||||
import { Color as ControlColor } from "./commands/color.js";
|
||||
import * as telnetctl from "./control/telnet.js";
|
||||
import "./common.css";
|
||||
import * as sshctl from "./control/ssh.js";
|
||||
|
||||
import * as xhr from "./xhr.js";
|
||||
import * as telnetctl from "./control/telnet.js";
|
||||
import * as cipher from "./crypto.js";
|
||||
import Home from "./home.vue";
|
||||
import "./landing.css";
|
||||
import Loading from "./loading.vue";
|
||||
import { Socket } from "./socket.js";
|
||||
import * as xhr from "./xhr.js";
|
||||
|
||||
const backendQueryRetryDelay = 2000;
|
||||
|
||||
@@ -52,6 +48,8 @@ const mainTemplate = `
|
||||
:connection="socket"
|
||||
:controls="controls"
|
||||
:commands="commands"
|
||||
:preset-data="presetData.presets"
|
||||
:restricted-to-presets="presetData.restricted"
|
||||
@navigate-to="changeURLHash"
|
||||
@tab-opened="tabOpened"
|
||||
@tab-closed="tabClosed"
|
||||
@@ -66,6 +64,7 @@ const mainTemplate = `
|
||||
`.trim();
|
||||
|
||||
const socksInterface = "/sshwifty/socket";
|
||||
const socksVerificationInterface = socksInterface + "/verify";
|
||||
|
||||
function startApp(rootEl) {
|
||||
const pageTitle = document.title;
|
||||
@@ -93,6 +92,10 @@ function startApp(rootEl) {
|
||||
: "",
|
||||
page: "loading",
|
||||
key: "",
|
||||
presetData: {
|
||||
presets: new Presets([]),
|
||||
restricted: false
|
||||
},
|
||||
authErr: "",
|
||||
loadErr: "",
|
||||
socket: null,
|
||||
@@ -163,6 +166,18 @@ function startApp(rootEl) {
|
||||
heartbeatInterval * 1000
|
||||
);
|
||||
},
|
||||
executeHomeApp(authResult, key) {
|
||||
this.presetData = {
|
||||
presets: new Presets(JSON.parse(authResult.data)),
|
||||
restricted: authResult.onlyAllowPresetRemotes
|
||||
};
|
||||
this.socket = this.buildSocket(
|
||||
key,
|
||||
authResult.timeout,
|
||||
authResult.heartbeat
|
||||
);
|
||||
this.page = "app";
|
||||
},
|
||||
async tryInitialAuth() {
|
||||
try {
|
||||
let result = await this.doAuth("");
|
||||
@@ -188,35 +203,30 @@ function startApp(rootEl) {
|
||||
|
||||
switch (result.result) {
|
||||
case 200:
|
||||
this.socket = this.buildSocket(
|
||||
{
|
||||
data: result.key,
|
||||
async fetch() {
|
||||
if (this.data) {
|
||||
let dKey = this.data;
|
||||
this.executeHomeApp(result, {
|
||||
data: result.key,
|
||||
async fetch() {
|
||||
if (this.data) {
|
||||
let dKey = this.data;
|
||||
|
||||
this.data = null;
|
||||
this.data = null;
|
||||
|
||||
return dKey;
|
||||
}
|
||||
|
||||
let result = await self.doAuth("");
|
||||
|
||||
if (result.result !== 200) {
|
||||
throw new Error(
|
||||
"Unable to fetch key from remote, unexpected " +
|
||||
"error code: " +
|
||||
result.result
|
||||
);
|
||||
}
|
||||
|
||||
return result.key;
|
||||
return dKey;
|
||||
}
|
||||
},
|
||||
result.timeout,
|
||||
result.heartbeat
|
||||
);
|
||||
this.page = "app";
|
||||
|
||||
let result = await self.doAuth("");
|
||||
|
||||
if (result.result !== 200) {
|
||||
throw new Error(
|
||||
"Unable to fetch key from remote, unexpected " +
|
||||
"error code: " +
|
||||
result.result
|
||||
);
|
||||
}
|
||||
|
||||
return result.key;
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 403:
|
||||
@@ -251,7 +261,7 @@ function startApp(rootEl) {
|
||||
? null
|
||||
: await this.getSocketAuthKey(privateKey, this.key);
|
||||
|
||||
let h = await xhr.head(socksInterface, {
|
||||
let h = await xhr.get(socksVerificationInterface, {
|
||||
"X-Key": authKey ? btoa(String.fromCharCode.apply(null, authKey)) : ""
|
||||
});
|
||||
|
||||
@@ -262,7 +272,10 @@ function startApp(rootEl) {
|
||||
key: h.getResponseHeader("X-Key"),
|
||||
timeout: h.getResponseHeader("X-Timeout"),
|
||||
heartbeat: h.getResponseHeader("X-Heartbeat"),
|
||||
date: serverDate ? new Date(serverDate) : null
|
||||
date: serverDate ? new Date(serverDate) : null,
|
||||
data: h.responseText,
|
||||
onlyAllowPresetRemotes:
|
||||
h.getResponseHeader("X-OnlyAllowPresetRemotes") === "yes"
|
||||
};
|
||||
},
|
||||
async submitAuth(passphrase) {
|
||||
@@ -273,17 +286,12 @@ function startApp(rootEl) {
|
||||
|
||||
switch (result.result) {
|
||||
case 200:
|
||||
this.socket = this.buildSocket(
|
||||
{
|
||||
data: passphrase,
|
||||
fetch() {
|
||||
return this.data;
|
||||
}
|
||||
},
|
||||
result.timeout,
|
||||
result.heartbeat
|
||||
);
|
||||
this.page = "app";
|
||||
this.executeHomeApp(result, {
|
||||
data: passphrase,
|
||||
fetch() {
|
||||
return this.data;
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 403:
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import Exception from "./exception.js";
|
||||
import * as subscribe from "../stream/subscribe.js";
|
||||
import Exception from "./exception.js";
|
||||
import * as presets from "./presets.js";
|
||||
|
||||
export const NEXT_PROMPT = 1;
|
||||
export const NEXT_WAIT = 2;
|
||||
@@ -131,8 +132,12 @@ const defField = {
|
||||
type: "",
|
||||
value: "",
|
||||
example: "",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(v) {
|
||||
return "OK";
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -155,19 +160,21 @@ export function field(def, f) {
|
||||
}
|
||||
|
||||
for (let i in f) {
|
||||
if (typeof n[i] !== typeof f[i]) {
|
||||
throw new Exception(
|
||||
'Field data type for "' +
|
||||
i +
|
||||
'" was not unmatched. Expecting "' +
|
||||
typeof def[i] +
|
||||
'", got "' +
|
||||
typeof f[i] +
|
||||
'" instead'
|
||||
);
|
||||
if (typeof n[i] === typeof f[i]) {
|
||||
n[i] = f[i];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
n[i] = f[i];
|
||||
throw new Exception(
|
||||
'Field data type for "' +
|
||||
i +
|
||||
'" was unmatched. Expecting "' +
|
||||
typeof n[i] +
|
||||
'", got "' +
|
||||
typeof f[i] +
|
||||
'" instead'
|
||||
);
|
||||
}
|
||||
|
||||
if (!n["name"]) {
|
||||
@@ -206,6 +213,31 @@ export function fields(definitions, fs) {
|
||||
return fss;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build command fields with preset data
|
||||
*
|
||||
* @param {object} definitions Definition of a group of fields
|
||||
* @param {object} fieldsData field data object, formated like a `defField`
|
||||
* @param {presets.Preset} presetData Preset data
|
||||
*
|
||||
* @returns {object}
|
||||
*
|
||||
*/
|
||||
export function fieldsWithPreset(definitions, fieldsData, presetData) {
|
||||
let newFields = fields(definitions, fieldsData);
|
||||
|
||||
for (let i in newFields) {
|
||||
try {
|
||||
newFields[i].value = presetData.meta(newFields[i].name);
|
||||
newFields[i].readonly = true;
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
return newFields;
|
||||
}
|
||||
|
||||
class Prompt {
|
||||
/**
|
||||
* constructor
|
||||
@@ -457,7 +489,7 @@ class Wizard {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {function} builder Command builder
|
||||
* @param {object} built Command executer
|
||||
* @param {subscribe.Subscribe} subs Wizard step subscriber
|
||||
* @param {function} done Callback which will be called when the wizard
|
||||
* is done
|
||||
@@ -468,6 +500,8 @@ class Wizard {
|
||||
this.subs = subs;
|
||||
this.done = done;
|
||||
this.closed = false;
|
||||
|
||||
this.built.run();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -583,8 +617,14 @@ class Builder {
|
||||
*/
|
||||
constructor(command) {
|
||||
this.cid = command.id();
|
||||
this.builder = (n, i, r, u, y, x, l) => {
|
||||
return command.builder(n, i, r, u, y, x, l);
|
||||
this.represeter = n => {
|
||||
return command.represet(n);
|
||||
};
|
||||
this.wizarder = (n, i, r, u, y, x, l) => {
|
||||
return command.wizard(n, i, r, u, y, x, l);
|
||||
};
|
||||
this.executer = (n, i, r, u, y, x, l) => {
|
||||
return command.execute(n, i, r, u, y, x, l);
|
||||
};
|
||||
this.launchCmd = (n, i, r, u, y, x) => {
|
||||
return command.launch(n, i, r, u, y, x);
|
||||
@@ -638,7 +678,38 @@ class Builder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build command wizard
|
||||
* Execute an automatic command wizard
|
||||
*
|
||||
* @param {stream.Streams} streams
|
||||
* @param {controls.Controls} controls
|
||||
* @param {history.History} history
|
||||
* @param {presets.Preset} preset
|
||||
* @param {object} session
|
||||
* @param {function} done Callback which will be called when wizard is done
|
||||
*
|
||||
* @returns {Wizard} Command wizard
|
||||
*
|
||||
*/
|
||||
wizard(streams, controls, history, preset, session, done) {
|
||||
let subs = new subscribe.Subscribe();
|
||||
|
||||
return new Wizard(
|
||||
this.wizarder(
|
||||
new Info(this),
|
||||
preset,
|
||||
session,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history
|
||||
),
|
||||
subs,
|
||||
done
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an automatic command wizard
|
||||
*
|
||||
* @param {stream.Streams} streams
|
||||
* @param {controls.Controls} controls
|
||||
@@ -650,11 +721,11 @@ class Builder {
|
||||
* @returns {Wizard} Command wizard
|
||||
*
|
||||
*/
|
||||
build(streams, controls, history, config, session, done) {
|
||||
execute(streams, controls, history, config, session, done) {
|
||||
let subs = new subscribe.Subscribe();
|
||||
|
||||
return new Wizard(
|
||||
this.builder(
|
||||
this.executer(
|
||||
new Info(this),
|
||||
config,
|
||||
session,
|
||||
@@ -707,19 +778,44 @@ class Builder {
|
||||
launcher(config) {
|
||||
return this.name() + ":" + encodeURI(this.launcherCmd(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconfigure the preset data for the command wizard
|
||||
*
|
||||
* @param {presets.Preset} n preset
|
||||
*
|
||||
* @return {presets.Preset} modified new preset
|
||||
*/
|
||||
represet(n) {
|
||||
return this.represeter(n);
|
||||
}
|
||||
}
|
||||
|
||||
export class Preset {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {presets.Preset} preset preset
|
||||
* @param {Builder} command executor
|
||||
*
|
||||
*/
|
||||
constructor(preset, command) {
|
||||
this.preset = preset;
|
||||
this.command = command;
|
||||
}
|
||||
}
|
||||
|
||||
export class Commands {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {[]object} commands Command array
|
||||
* @param {Array<object>} commands Command array
|
||||
*
|
||||
*/
|
||||
constructor(commands) {
|
||||
this.commands = [];
|
||||
|
||||
for (let i in commands) {
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
this.commands.push(new Builder(commands[i]));
|
||||
}
|
||||
}
|
||||
@@ -727,7 +823,7 @@ export class Commands {
|
||||
/**
|
||||
* Return all commands
|
||||
*
|
||||
* @returns {[]Builder} A group of command
|
||||
* @returns {Array<Builder>} A group of command
|
||||
*
|
||||
*/
|
||||
all() {
|
||||
@@ -745,4 +841,28 @@ export class Commands {
|
||||
select(id) {
|
||||
return this.commands[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns presets with merged command
|
||||
*
|
||||
* @param {presets.Presets} ps
|
||||
*
|
||||
* @returns {Array<Preset>}
|
||||
*
|
||||
*/
|
||||
mergePresets(ps) {
|
||||
let pp = [];
|
||||
|
||||
for (let i = 0; i < this.commands.length; i++) {
|
||||
const fetched = ps.fetch(this.commands[i].name());
|
||||
|
||||
for (let j = 0; j < fetched.length; j++) {
|
||||
pp.push(
|
||||
new Preset(this.commands[i].represet(fetched[j]), this.commands[i])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return pp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,16 @@
|
||||
|
||||
import * as command from "./commands.js";
|
||||
|
||||
function metaContains(data, metaName, valContains) {
|
||||
switch (typeof data[metaName]) {
|
||||
case "string":
|
||||
return data[metaName].indexOf(valContains) >= 0;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class History {
|
||||
/**
|
||||
* constructor
|
||||
@@ -206,4 +216,39 @@ export class History {
|
||||
|
||||
this.store();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for partly matched results
|
||||
*
|
||||
* @param {string} type of the history record
|
||||
* @param {string} metaName name of the meta data
|
||||
* @param {string} keyword keyword to search
|
||||
* @param {number} max max results
|
||||
*/
|
||||
search(type, metaName, keyword, max) {
|
||||
let maxResults = max > this.records.length ? this.records.length : max;
|
||||
let s = [];
|
||||
|
||||
if (maxResults < 0) {
|
||||
maxResults = this.records.length;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.records.length && s.length < maxResults; i++) {
|
||||
if (this.records[i].type !== type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this.records[i].data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!metaContains(this.records[i].data, metaName, keyword)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
s.push(this.records[i]);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
292
ui/commands/presets.js
Normal file
292
ui/commands/presets.js
Normal file
@@ -0,0 +1,292 @@
|
||||
// Sshwifty - A Web SSH client
|
||||
//
|
||||
// Copyright (C) 2019-2020 Rui NI <nirui@gmx.com>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import Exception from "./exception.js";
|
||||
|
||||
/**
|
||||
* Default preset item, contains data of a default preset
|
||||
*
|
||||
*/
|
||||
const presetItem = {
|
||||
title: "",
|
||||
type: "",
|
||||
host: "",
|
||||
meta: {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify Preset Item Meta
|
||||
*
|
||||
* @param {object} preset
|
||||
*
|
||||
*/
|
||||
function verifyPresetItemMeta(preset) {
|
||||
for (let i in preset.meta) {
|
||||
if (typeof preset.meta[i] === "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
'The data type of meta field "' +
|
||||
i +
|
||||
'" was "' +
|
||||
typeof preset.meta[i] +
|
||||
'" instead of expected "string"'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and verify the given preset, return a valid preset
|
||||
*
|
||||
* @param {object} item
|
||||
*
|
||||
* @throws {Exception} when invalid data is given
|
||||
*
|
||||
* @return {object}
|
||||
*
|
||||
*/
|
||||
function parsePresetItem(item) {
|
||||
let preset = {};
|
||||
|
||||
for (let i in presetItem) {
|
||||
preset[i] = presetItem[i];
|
||||
}
|
||||
|
||||
for (let i in presetItem) {
|
||||
if (typeof presetItem[i] === typeof item[i]) {
|
||||
preset[i] = item[i];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
'Expecting the data type of "' +
|
||||
i +
|
||||
'" is "' +
|
||||
typeof presetItem[i] +
|
||||
'", given "' +
|
||||
typeof item[i] +
|
||||
'" instead'
|
||||
);
|
||||
}
|
||||
|
||||
verifyPresetItemMeta(preset.meta);
|
||||
|
||||
return preset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preset data
|
||||
*
|
||||
*/
|
||||
export class Preset {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {object} preset preset data
|
||||
*
|
||||
*/
|
||||
constructor(preset) {
|
||||
this.preset = parsePresetItem(preset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the title of the preset
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
title() {
|
||||
return this.preset.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type of the preset
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
type() {
|
||||
return this.preset.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the host of the preset
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
host() {
|
||||
return this.preset.host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the given meta of current preset
|
||||
*
|
||||
* @param {string} name name of the meta data
|
||||
*
|
||||
* @throws {Exception} when invalid data is given
|
||||
*
|
||||
* @returns {string}
|
||||
*
|
||||
*/
|
||||
meta(name) {
|
||||
if (typeof this.preset.meta[name] !== "string") {
|
||||
throw new Exception('Meta "' + name + '" was undefined');
|
||||
}
|
||||
|
||||
return this.preset.meta[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new meta item
|
||||
*
|
||||
* @param {string} name name of the meta data
|
||||
* @param {string} data data of the meta data
|
||||
*
|
||||
* @throws {Exception} when invalid data is given
|
||||
*
|
||||
*/
|
||||
insertMeta(name, data) {
|
||||
if (typeof this.preset.meta[name] !== "undefined") {
|
||||
throw new Exception('Meta "' + name + '" has already been defined');
|
||||
}
|
||||
|
||||
this.preset.meta[name] = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an empty preset
|
||||
*
|
||||
* @returns {Preset}
|
||||
*
|
||||
*/
|
||||
export function emptyPreset() {
|
||||
return new Preset({
|
||||
title: "Default",
|
||||
type: "Default",
|
||||
host: "",
|
||||
meta: {}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Command Preset manager
|
||||
*
|
||||
*/
|
||||
export class Presets {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {Array<object>} presets Array of preset data
|
||||
*
|
||||
*/
|
||||
constructor(presets) {
|
||||
this.presets = [];
|
||||
|
||||
for (let i = 0; i < presets.length; i++) {
|
||||
this.presets.push(new Preset(presets[i]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all presets of a type
|
||||
*
|
||||
* @param {string} type type of the presets data
|
||||
*
|
||||
* @returns {Array<Preset>}
|
||||
*
|
||||
*/
|
||||
fetch(type) {
|
||||
let presets = [];
|
||||
|
||||
for (let i = 0; i < this.presets.length; i++) {
|
||||
if (this.presets[i].type() !== type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
presets.push(this.presets[i]);
|
||||
}
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return presets with matched type and meta data
|
||||
*
|
||||
* @param {string} type type of the presets data
|
||||
* @param {string} metaName name of the meta data
|
||||
* @param {string} metaVal value of the meta data
|
||||
*
|
||||
* @returns {Array<Preset>}
|
||||
*
|
||||
*/
|
||||
meta(type, metaName, metaVal) {
|
||||
let presets = [];
|
||||
|
||||
for (let i = 0; i < this.presets.length; i++) {
|
||||
if (this.presets[i].type() !== type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.presets[i].meta(metaName) !== metaVal) {
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!(e instanceof Exception)) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
presets.push(this.presets[i]);
|
||||
}
|
||||
|
||||
return presets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return presets with matched type and host
|
||||
*
|
||||
* @param {string} type type of the presets
|
||||
* @param {string} host host of the presets
|
||||
*
|
||||
* @returns {Array<Preset>}
|
||||
*
|
||||
*/
|
||||
hosts(type, host) {
|
||||
let presets = [];
|
||||
|
||||
for (let i = 0; i < this.presets.length; i++) {
|
||||
if (this.presets[i].type() !== type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.presets[i].host() !== host) {
|
||||
continue;
|
||||
}
|
||||
|
||||
presets.push(this.presets[i]);
|
||||
}
|
||||
|
||||
return presets;
|
||||
}
|
||||
}
|
||||
@@ -15,17 +15,18 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as header from "../stream/header.js";
|
||||
import * as reader from "../stream/reader.js";
|
||||
import * as stream from "../stream/stream.js";
|
||||
import * as address from "./address.js";
|
||||
import * as command from "./commands.js";
|
||||
import * as common from "./common.js";
|
||||
import * as event from "./events.js";
|
||||
import * as reader from "../stream/reader.js";
|
||||
import * as stream from "../stream/stream.js";
|
||||
import * as controls from "./controls.js";
|
||||
import * as header from "../stream/header.js";
|
||||
import * as history from "./history.js";
|
||||
import * as strings from "./string.js";
|
||||
import * as event from "./events.js";
|
||||
import Exception from "./exception.js";
|
||||
import * as history from "./history.js";
|
||||
import * as presets from "./presets.js";
|
||||
import * as strings from "./string.js";
|
||||
|
||||
const AUTHMETHOD_NONE = 0x00;
|
||||
const AUTHMETHOD_PASSPHRASE = 0x01;
|
||||
@@ -57,6 +58,8 @@ const FingerprintPromptVerifyPassed = 0x00;
|
||||
const FingerprintPromptVerifyNoRecord = 0x01;
|
||||
const FingerprintPromptVerifyMismatch = 0x02;
|
||||
|
||||
const HostMaxSearchResults = 3;
|
||||
|
||||
class SSH {
|
||||
/**
|
||||
* constructor
|
||||
@@ -245,6 +248,10 @@ const initialFieldDef = {
|
||||
type: "text",
|
||||
value: "",
|
||||
example: "guest",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
if (d.length <= 0) {
|
||||
throw new Error("Username must be specified");
|
||||
@@ -265,6 +272,10 @@ const initialFieldDef = {
|
||||
type: "text",
|
||||
value: "",
|
||||
example: "ssh.vaguly.com:22",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
if (d.length <= 0) {
|
||||
throw new Error("Hostname must be specified");
|
||||
@@ -295,6 +306,10 @@ const initialFieldDef = {
|
||||
type: "select",
|
||||
value: "utf-8",
|
||||
example: common.charsetPresets.join(","),
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
for (let i in common.charsetPresets) {
|
||||
if (common.charsetPresets[i] !== d) {
|
||||
@@ -315,6 +330,10 @@ const initialFieldDef = {
|
||||
"SSH session is handled by the backend. Traffic will be decrypted " +
|
||||
"on the backend server and then transmit back to your client.",
|
||||
example: "",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
return "";
|
||||
}
|
||||
@@ -325,6 +344,10 @@ const initialFieldDef = {
|
||||
type: "password",
|
||||
value: "",
|
||||
example: "----------",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
if (d.length <= 0) {
|
||||
throw new Error("Password must be specified");
|
||||
@@ -356,6 +379,10 @@ const initialFieldDef = {
|
||||
type: "textfile",
|
||||
value: "",
|
||||
example: "",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
if (d.length <= 0) {
|
||||
throw new Error("Private Key must be specified");
|
||||
@@ -410,6 +437,10 @@ const initialFieldDef = {
|
||||
type: "radio",
|
||||
value: "",
|
||||
example: "Password,Private Key,None",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
switch (d) {
|
||||
case "Password":
|
||||
@@ -431,6 +462,10 @@ const initialFieldDef = {
|
||||
type: "textdata",
|
||||
value: "",
|
||||
example: "",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
return "";
|
||||
}
|
||||
@@ -468,7 +503,7 @@ class Wizard {
|
||||
* constructor
|
||||
*
|
||||
* @param {command.Info} info
|
||||
* @param {object} config
|
||||
* @param {presets.Preset} preset
|
||||
* @param {object} session
|
||||
* @param {streams.Streams} streams
|
||||
* @param {subscribe.Subscribe} subs
|
||||
@@ -476,11 +511,11 @@ class Wizard {
|
||||
* @param {history.History} history
|
||||
*
|
||||
*/
|
||||
constructor(info, config, session, streams, subs, controls, history) {
|
||||
constructor(info, preset, session, streams, subs, controls, history) {
|
||||
this.info = info;
|
||||
this.preset = preset;
|
||||
this.hasStarted = false;
|
||||
this.streams = streams;
|
||||
this.config = config;
|
||||
this.session = session
|
||||
? session
|
||||
: {
|
||||
@@ -489,7 +524,9 @@ class Wizard {
|
||||
this.step = subs;
|
||||
this.controls = controls.get("SSH");
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
run() {
|
||||
this.step.resolve(this.stepInitialPrompt());
|
||||
}
|
||||
|
||||
@@ -680,26 +717,6 @@ class Wizard {
|
||||
stepInitialPrompt() {
|
||||
let self = this;
|
||||
|
||||
if (this.config) {
|
||||
self.hasStarted = true;
|
||||
|
||||
self.streams.request(COMMAND_ID, sd => {
|
||||
return self.buildCommand(
|
||||
sd,
|
||||
{
|
||||
user: this.config.user,
|
||||
authentication: this.config.authentication,
|
||||
host: this.config.host,
|
||||
charset: this.config.charset ? this.config.charset : "utf-8",
|
||||
fingerprint: this.config.fingerprint
|
||||
},
|
||||
this.session
|
||||
);
|
||||
});
|
||||
|
||||
return self.stepWaitForAcceptWait();
|
||||
}
|
||||
|
||||
return command.prompt(
|
||||
"SSH",
|
||||
"Secure Shell Host",
|
||||
@@ -717,26 +734,57 @@ class Wizard {
|
||||
charset: r.encoding,
|
||||
fingerprint: ""
|
||||
},
|
||||
this.session
|
||||
self.session
|
||||
);
|
||||
});
|
||||
|
||||
self.step.resolve(self.stepWaitForAcceptWait());
|
||||
},
|
||||
() => {},
|
||||
command.fields(initialFieldDef, [
|
||||
{ name: "User" },
|
||||
{ name: "Host" },
|
||||
{ name: "Authentication" },
|
||||
{ name: "Encoding" },
|
||||
{ name: "Notice" }
|
||||
])
|
||||
command.fieldsWithPreset(
|
||||
initialFieldDef,
|
||||
[
|
||||
{ name: "User" },
|
||||
{
|
||||
name: "Host",
|
||||
suggestions(input) {
|
||||
const hosts = self.history.search(
|
||||
"SSH",
|
||||
"host",
|
||||
input,
|
||||
HostMaxSearchResults
|
||||
);
|
||||
|
||||
let sugg = [];
|
||||
|
||||
for (let i = 0; i < hosts.length; i++) {
|
||||
sugg.push({
|
||||
title: hosts[i].title,
|
||||
value: hosts[i].data.host,
|
||||
meta: {
|
||||
User: hosts[i].data.user,
|
||||
Authentication: hosts[i].data.authentication,
|
||||
Encoding: hosts[i].data.charset
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return sugg;
|
||||
}
|
||||
},
|
||||
{ name: "Authentication" },
|
||||
{ name: "Encoding" },
|
||||
{ name: "Notice" }
|
||||
],
|
||||
self.preset
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async stepFingerprintPrompt(rd, sd, verify, newFingerprint) {
|
||||
let self = this,
|
||||
fingerprintData = new TextDecoder("utf-8").decode(
|
||||
const self = this;
|
||||
|
||||
let fingerprintData = new TextDecoder("utf-8").decode(
|
||||
await reader.readCompletely(rd)
|
||||
),
|
||||
fingerprintChanged = false;
|
||||
@@ -745,7 +793,7 @@ class Wizard {
|
||||
case FingerprintPromptVerifyPassed:
|
||||
sd.send(CLIENT_CONNECT_RESPOND_FINGERPRINT, new Uint8Array([0]));
|
||||
|
||||
return this.stepContinueWaitForEstablishWait();
|
||||
return self.stepContinueWaitForEstablishWait();
|
||||
|
||||
case FingerprintPromptVerifyMismatch:
|
||||
fingerprintChanged = true;
|
||||
@@ -783,8 +831,9 @@ class Wizard {
|
||||
}
|
||||
|
||||
async stepCredentialPrompt(rd, sd, config, newCredential) {
|
||||
let self = this,
|
||||
fields = [];
|
||||
const self = this;
|
||||
|
||||
let fields = [];
|
||||
|
||||
if (config.credential.length > 0) {
|
||||
sd.send(
|
||||
@@ -792,7 +841,7 @@ class Wizard {
|
||||
new TextEncoder().encode(config.credential)
|
||||
);
|
||||
|
||||
return this.stepContinueWaitForEstablishWait();
|
||||
return self.stepContinueWaitForEstablishWait();
|
||||
}
|
||||
|
||||
switch (config.auth) {
|
||||
@@ -836,11 +885,61 @@ class Wizard {
|
||||
)
|
||||
);
|
||||
},
|
||||
command.fields(initialFieldDef, fields)
|
||||
command.fieldsWithPreset(initialFieldDef, fields, self.preset)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Executer extends Wizard {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {command.Info} info
|
||||
* @param {config} config
|
||||
* @param {object} session
|
||||
* @param {streams.Streams} streams
|
||||
* @param {subscribe.Subscribe} subs
|
||||
* @param {controls.Controls} controls
|
||||
* @param {history.History} history
|
||||
*
|
||||
*/
|
||||
constructor(info, config, session, streams, subs, controls, history) {
|
||||
super(
|
||||
info,
|
||||
presets.emptyPreset(),
|
||||
session,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history
|
||||
);
|
||||
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
stepInitialPrompt() {
|
||||
const self = this;
|
||||
|
||||
self.hasStarted = true;
|
||||
|
||||
self.streams.request(COMMAND_ID, sd => {
|
||||
return self.buildCommand(
|
||||
sd,
|
||||
{
|
||||
user: self.config.user,
|
||||
authentication: self.config.authentication,
|
||||
host: self.config.host,
|
||||
charset: self.config.charset ? self.config.charset : "utf-8",
|
||||
fingerprint: self.config.fingerprint
|
||||
},
|
||||
self.session
|
||||
);
|
||||
});
|
||||
|
||||
return self.stepWaitForAcceptWait();
|
||||
}
|
||||
}
|
||||
|
||||
export class Command {
|
||||
constructor() {}
|
||||
|
||||
@@ -860,8 +959,20 @@ export class Command {
|
||||
return "#3c8";
|
||||
}
|
||||
|
||||
builder(info, config, session, streams, subs, controls, history) {
|
||||
return new Wizard(info, config, session, streams, subs, controls, history);
|
||||
wizard(info, preset, session, streams, subs, controls, history) {
|
||||
return new Wizard(info, preset, session, streams, subs, controls, history);
|
||||
}
|
||||
|
||||
execute(info, config, session, streams, subs, controls, history) {
|
||||
return new Executer(
|
||||
info,
|
||||
config,
|
||||
session,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history
|
||||
);
|
||||
}
|
||||
|
||||
launch(info, launcher, streams, subs, controls, history) {
|
||||
@@ -893,7 +1004,7 @@ export class Command {
|
||||
);
|
||||
}
|
||||
|
||||
return this.builder(
|
||||
return this.execute(
|
||||
info,
|
||||
{
|
||||
user: user,
|
||||
@@ -920,4 +1031,14 @@ export class Command {
|
||||
(config.charset ? config.charset : "utf-8")
|
||||
);
|
||||
}
|
||||
|
||||
represet(preset) {
|
||||
const host = preset.host();
|
||||
|
||||
if (host.length > 0) {
|
||||
preset.insertMeta("Host", host);
|
||||
}
|
||||
|
||||
return preset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,16 +15,17 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import * as header from "../stream/header.js";
|
||||
import * as reader from "../stream/reader.js";
|
||||
import * as stream from "../stream/stream.js";
|
||||
import * as address from "./address.js";
|
||||
import * as command from "./commands.js";
|
||||
import * as common from "./common.js";
|
||||
import * as event from "./events.js";
|
||||
import * as reader from "../stream/reader.js";
|
||||
import * as stream from "../stream/stream.js";
|
||||
import * as controls from "./controls.js";
|
||||
import * as history from "./history.js";
|
||||
import * as header from "../stream/header.js";
|
||||
import * as event from "./events.js";
|
||||
import Exception from "./exception.js";
|
||||
import * as history from "./history.js";
|
||||
import * as presets from "./presets.js";
|
||||
|
||||
const COMMAND_ID = 0x00;
|
||||
|
||||
@@ -36,6 +37,8 @@ const SERVER_DIAL_CONNECTED = 0x02;
|
||||
|
||||
const DEFAULT_PORT = 23;
|
||||
|
||||
const HostMaxSearchResults = 3;
|
||||
|
||||
class Telnet {
|
||||
/**
|
||||
* constructor
|
||||
@@ -185,6 +188,10 @@ const initialFieldDef = {
|
||||
type: "text",
|
||||
value: "",
|
||||
example: "telnet.vaguly.com:23",
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
if (d.length <= 0) {
|
||||
throw new Error("Hostname must be specified");
|
||||
@@ -215,6 +222,10 @@ const initialFieldDef = {
|
||||
type: "select",
|
||||
value: "utf-8",
|
||||
example: common.charsetPresets.join(","),
|
||||
readonly: false,
|
||||
suggestions(input) {
|
||||
return [];
|
||||
},
|
||||
verify(d) {
|
||||
for (let i in common.charsetPresets) {
|
||||
if (common.charsetPresets[i] !== d) {
|
||||
@@ -234,7 +245,7 @@ class Wizard {
|
||||
* constructor
|
||||
*
|
||||
* @param {command.Info} info
|
||||
* @param {object} config
|
||||
* @param {presets.Preset} preset
|
||||
* @param {object} session
|
||||
* @param {streams.Streams} streams
|
||||
* @param {subscribe.Subscribe} subs
|
||||
@@ -242,16 +253,18 @@ class Wizard {
|
||||
* @param {history.History} history
|
||||
*
|
||||
*/
|
||||
constructor(info, config, session, streams, subs, controls, history) {
|
||||
constructor(info, preset, session, streams, subs, controls, history) {
|
||||
this.info = info;
|
||||
this.preset = preset;
|
||||
this.hasStarted = false;
|
||||
this.streams = streams;
|
||||
this.config = config;
|
||||
this.session = session;
|
||||
this.step = subs;
|
||||
this.controls = controls.get("Telnet");
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
run() {
|
||||
this.step.resolve(this.stepInitialPrompt());
|
||||
}
|
||||
|
||||
@@ -378,24 +391,7 @@ class Wizard {
|
||||
}
|
||||
|
||||
stepInitialPrompt() {
|
||||
let self = this;
|
||||
|
||||
if (this.config) {
|
||||
self.hasStarted = true;
|
||||
|
||||
self.streams.request(COMMAND_ID, sd => {
|
||||
return self.buildCommand(
|
||||
sd,
|
||||
{
|
||||
host: this.config.host,
|
||||
charset: this.config.charset ? this.config.charset : "utf-8"
|
||||
},
|
||||
this.session
|
||||
);
|
||||
});
|
||||
|
||||
return self.stepWaitForAcceptWait();
|
||||
}
|
||||
const self = this;
|
||||
|
||||
return command.prompt(
|
||||
"Telnet",
|
||||
@@ -411,18 +407,96 @@ class Wizard {
|
||||
host: r.host,
|
||||
charset: r.encoding
|
||||
},
|
||||
this.session
|
||||
self.session
|
||||
);
|
||||
});
|
||||
|
||||
self.step.resolve(self.stepWaitForAcceptWait());
|
||||
},
|
||||
() => {},
|
||||
command.fields(initialFieldDef, [{ name: "Host" }, { name: "Encoding" }])
|
||||
command.fieldsWithPreset(
|
||||
initialFieldDef,
|
||||
[
|
||||
{
|
||||
name: "Host",
|
||||
suggestions(input) {
|
||||
const hosts = self.history.search(
|
||||
"Telnet",
|
||||
"host",
|
||||
input,
|
||||
HostMaxSearchResults
|
||||
);
|
||||
|
||||
let sugg = [];
|
||||
|
||||
for (let i = 0; i < hosts.length; i++) {
|
||||
sugg.push({
|
||||
title: hosts[i].title,
|
||||
value: hosts[i].data.host,
|
||||
meta: {
|
||||
Encoding: hosts[i].data.charset
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return sugg;
|
||||
}
|
||||
},
|
||||
{ name: "Encoding" }
|
||||
],
|
||||
self.preset
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Executor extends Wizard {
|
||||
/**
|
||||
* constructor
|
||||
*
|
||||
* @param {command.Info} info
|
||||
* @param {object} config
|
||||
* @param {object} session
|
||||
* @param {streams.Streams} streams
|
||||
* @param {subscribe.Subscribe} subs
|
||||
* @param {controls.Controls} controls
|
||||
* @param {history.History} history
|
||||
*
|
||||
*/
|
||||
constructor(info, config, session, streams, subs, controls, history) {
|
||||
super(
|
||||
info,
|
||||
presets.emptyPreset(),
|
||||
session,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history
|
||||
);
|
||||
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
stepInitialPrompt() {
|
||||
const self = this;
|
||||
|
||||
self.hasStarted = true;
|
||||
|
||||
self.streams.request(COMMAND_ID, sd => {
|
||||
return self.buildCommand(
|
||||
sd,
|
||||
{
|
||||
host: self.config.host,
|
||||
charset: self.config.charset ? self.config.charset : "utf-8"
|
||||
},
|
||||
self.session
|
||||
);
|
||||
});
|
||||
|
||||
return self.stepWaitForAcceptWait();
|
||||
}
|
||||
}
|
||||
|
||||
export class Command {
|
||||
constructor() {}
|
||||
|
||||
@@ -442,8 +516,20 @@ export class Command {
|
||||
return "#6ac";
|
||||
}
|
||||
|
||||
builder(info, config, session, streams, subs, controls, history) {
|
||||
return new Wizard(info, config, session, streams, subs, controls, history);
|
||||
wizard(info, preset, session, streams, subs, controls, history) {
|
||||
return new Wizard(info, preset, session, streams, subs, controls, history);
|
||||
}
|
||||
|
||||
execute(info, config, session, streams, subs, controls, history) {
|
||||
return new Executor(
|
||||
info,
|
||||
config,
|
||||
session,
|
||||
streams,
|
||||
subs,
|
||||
controls,
|
||||
history
|
||||
);
|
||||
}
|
||||
|
||||
launch(info, launcher, streams, subs, controls, history) {
|
||||
@@ -476,7 +562,7 @@ export class Command {
|
||||
}
|
||||
}
|
||||
|
||||
return this.builder(
|
||||
return this.execute(
|
||||
info,
|
||||
{
|
||||
host: d[0],
|
||||
@@ -493,4 +579,14 @@ export class Command {
|
||||
launcher(config) {
|
||||
return config.host + "|" + (config.charset ? config.charset : "utf-8");
|
||||
}
|
||||
|
||||
represet(preset) {
|
||||
const host = preset.host();
|
||||
|
||||
if (host.length > 0) {
|
||||
preset.insertMeta("Host", host);
|
||||
}
|
||||
|
||||
return preset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +216,28 @@ body {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.hlst.lstcl2 {
|
||||
list-style: none;
|
||||
list-style-position: inside;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.hlst.lstcl2 > li {
|
||||
width: 33%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hlst.lstcl2 > li .lst-wrap {
|
||||
padding: 10px;
|
||||
margin: 5px;
|
||||
background: #333;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Icon */
|
||||
.icon {
|
||||
line-height: 1;
|
||||
|
||||
80
ui/home.vue
80
ui/home.vue
@@ -110,6 +110,8 @@
|
||||
:inputting="connector.inputting"
|
||||
:display="windows.connect"
|
||||
:connectors="connector.connectors"
|
||||
:presets="presets"
|
||||
:restricted-to-presets="restrictedToPresets"
|
||||
:knowns="connector.knowns"
|
||||
:knowns-launcher-builder="buildknownLauncher"
|
||||
:knowns-export="exportKnowns"
|
||||
@@ -119,6 +121,7 @@
|
||||
@connector-select="connectNew"
|
||||
@known-select="connectKnown"
|
||||
@known-remove="removeKnown"
|
||||
@preset-select="connectPreset"
|
||||
@known-clear-session="clearSessionKnown"
|
||||
>
|
||||
<connector
|
||||
@@ -143,8 +146,9 @@
|
||||
@current="switchTab"
|
||||
@retap="retapTab"
|
||||
@close="closeTab"
|
||||
></tab-window></div
|
||||
></template>
|
||||
></tab-window>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import "./home.css";
|
||||
@@ -159,6 +163,8 @@ import Screens from "./widgets/screens.vue";
|
||||
import * as home_socket from "./home_socketctl.js";
|
||||
import * as home_history from "./home_historyctl.js";
|
||||
|
||||
import * as presets from "./commands/presets.js";
|
||||
|
||||
const BACKEND_CONNECT_ERROR =
|
||||
"Unable to connect to the Sshwifty backend server: ";
|
||||
const BACKEND_REQUEST_ERROR = "Unable to perform request: ";
|
||||
@@ -198,6 +204,18 @@ export default {
|
||||
default: () => {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
presetData: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return new presets.Presets([]);
|
||||
}
|
||||
},
|
||||
restrictedToPresets: {
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -220,6 +238,7 @@ export default {
|
||||
busy: false,
|
||||
knowns: history.all()
|
||||
},
|
||||
presets: this.commands.mergePresets(this.presetData),
|
||||
tab: {
|
||||
current: -1,
|
||||
lastID: 0,
|
||||
@@ -317,22 +336,45 @@ export default {
|
||||
);
|
||||
},
|
||||
connectNew(connector) {
|
||||
this.runConnect(stream => {
|
||||
this.connector.connector = {
|
||||
const self = this;
|
||||
|
||||
self.runConnect(stream => {
|
||||
self.connector.connector = {
|
||||
id: connector.id(),
|
||||
name: connector.name(),
|
||||
description: connector.description(),
|
||||
wizard: connector.build(
|
||||
wizard: connector.wizard(
|
||||
stream,
|
||||
this.controls,
|
||||
this.connector.historyRec,
|
||||
null,
|
||||
self.controls,
|
||||
self.connector.historyRec,
|
||||
presets.emptyPreset(),
|
||||
null,
|
||||
() => {}
|
||||
)
|
||||
};
|
||||
|
||||
this.connector.inputting = true;
|
||||
self.connector.inputting = true;
|
||||
});
|
||||
},
|
||||
connectPreset(preset) {
|
||||
const self = this;
|
||||
|
||||
self.runConnect(stream => {
|
||||
self.connector.connector = {
|
||||
id: preset.command.id(),
|
||||
name: preset.command.name(),
|
||||
description: preset.command.description(),
|
||||
wizard: preset.command.wizard(
|
||||
stream,
|
||||
self.controls,
|
||||
self.connector.historyRec,
|
||||
preset.preset,
|
||||
null,
|
||||
() => {}
|
||||
)
|
||||
};
|
||||
|
||||
self.connector.inputting = true;
|
||||
});
|
||||
},
|
||||
getConnectorByType(type) {
|
||||
@@ -349,27 +391,27 @@ export default {
|
||||
return connector;
|
||||
},
|
||||
connectKnown(known) {
|
||||
this.runConnect(stream => {
|
||||
let connector = this.getConnectorByType(known.type);
|
||||
const self = this;
|
||||
|
||||
self.runConnect(stream => {
|
||||
let connector = self.getConnectorByType(known.type);
|
||||
|
||||
if (!connector) {
|
||||
alert("Unknown connector: " + known.type);
|
||||
|
||||
this.connector.inputting = false;
|
||||
self.connector.inputting = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
this.connector.connector = {
|
||||
self.connector.connector = {
|
||||
id: connector.id(),
|
||||
name: connector.name(),
|
||||
description: connector.description(),
|
||||
wizard: connector.build(
|
||||
wizard: connector.execute(
|
||||
stream,
|
||||
this.controls,
|
||||
this.connector.historyRec,
|
||||
self.controls,
|
||||
self.connector.historyRec,
|
||||
known.data,
|
||||
known.session,
|
||||
() => {
|
||||
@@ -378,7 +420,7 @@ export default {
|
||||
)
|
||||
};
|
||||
|
||||
this.connector.inputting = true;
|
||||
self.connector.inputting = true;
|
||||
});
|
||||
},
|
||||
parseConnectLauncher(ll) {
|
||||
|
||||
@@ -44,11 +44,14 @@
|
||||
|
||||
<connect-known
|
||||
v-if="tab === 'known' && !inputting"
|
||||
:presets="presets"
|
||||
:restricted-to-presets="restrictedToPresets"
|
||||
:knowns="knowns"
|
||||
:launcher-builder="knownsLauncherBuilder"
|
||||
:knowns-export="knownsExport"
|
||||
:knowns-import="knownsImport"
|
||||
@select="selectKnown"
|
||||
@select-preset="selectPreset"
|
||||
@remove="removeKnown"
|
||||
@clear-session="clearSessionKnown"
|
||||
></connect-known>
|
||||
@@ -100,6 +103,14 @@ export default {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
presets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
restrictedToPresets: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
},
|
||||
knowns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -160,6 +171,13 @@ export default {
|
||||
|
||||
this.$emit("known-remove", uid);
|
||||
},
|
||||
selectPreset(preset) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("preset-select", preset);
|
||||
},
|
||||
clearSessionKnown(uid) {
|
||||
if (this.inputting) {
|
||||
return;
|
||||
|
||||
@@ -22,13 +22,20 @@
|
||||
#connect-known-list {
|
||||
min-height: 200px;
|
||||
font-size: 0.75em;
|
||||
padding: 15px;
|
||||
background: #3a3a3a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#connect-known-list h3 {
|
||||
font-size: 1.1em;
|
||||
color: #999;
|
||||
margin: 5px 0 15px 0;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#connect-known-list.reloaded {
|
||||
}
|
||||
|
||||
@@ -62,10 +69,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#connect-known-list-import {
|
||||
margin: 15px 0 10px 0;
|
||||
margin: 15px 0;
|
||||
color: #aaa;
|
||||
font-size: 1.1em;
|
||||
text-align: center;
|
||||
@@ -76,26 +84,30 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#connect-known-list li {
|
||||
#connect-known-list-list {
|
||||
padding: 15px 20px 20px 20px;
|
||||
}
|
||||
|
||||
#connect-known-list-list li {
|
||||
width: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#connect-known-list li {
|
||||
#connect-known-list-list li {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#connect-known-list li .lst-wrap {
|
||||
#connect-known-list-list li > .lst-wrap {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#connect-known-list li .lst-wrap:hover {
|
||||
#connect-known-list-list li > .lst-wrap:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels {
|
||||
#connect-known-list-list li > .labels {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -104,14 +116,14 @@
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .type {
|
||||
#connect-known-list-list li > .labels > .type {
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
background: #a56;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt {
|
||||
#connect-known-list-list li > .labels > .opt {
|
||||
display: none;
|
||||
padding: 3px;
|
||||
background: #a56;
|
||||
@@ -121,43 +133,44 @@
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#connect-known-list li .labels > .opt {
|
||||
#connect-known-list-list li > .labels > .opt {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt.link {
|
||||
#connect-known-list-list li > .labels > .opt.link {
|
||||
background: #287;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt.link:after {
|
||||
#connect-known-list-list li > .labels > .opt.link:after {
|
||||
content: "\02936";
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt.del {
|
||||
#connect-known-list-list li > .labels > .opt.del {
|
||||
background: #a56;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list li .labels > .opt.clr {
|
||||
#connect-known-list-list li > .labels > .opt.clr {
|
||||
background: #b71;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#connect-known-list li:hover .labels > .opt,
|
||||
#connect-known-list li:focus .labels > .opt {
|
||||
#connect-known-list-list li:hover > .labels > .opt,
|
||||
#connect-known-list-list li:focus > .labels > .opt {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#connect-known-list li h2 {
|
||||
#connect-known-list-list li > .lst-wrap > h4 {
|
||||
font-size: 1.5em;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#connect-known-list li h2::before {
|
||||
#connect-known-list-list li > .lst-wrap > h4::before {
|
||||
content: ">_";
|
||||
color: #555;
|
||||
font-size: 0.8em;
|
||||
@@ -167,7 +180,70 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#connect-known-list li h2.highlight::before {
|
||||
#connect-known-list-list li > .lst-wrap > h4.highlight::before {
|
||||
color: #eee;
|
||||
background: #555;
|
||||
}
|
||||
|
||||
#connect-known-list-presets {
|
||||
margin-top: 10px;
|
||||
padding: 15px 20px 20px 20px;
|
||||
}
|
||||
|
||||
#connect-known-list-presets.last-planel {
|
||||
background: #3f3f3f;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li {
|
||||
width: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#connect-known-list-presets li {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#connect-known-list-presets li > .lst-wrap {
|
||||
cursor: pointer;
|
||||
border-radius: 0 3px 3px 3px;
|
||||
margin: 12px 10px 10px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li > .lst-wrap > .labels {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.85em;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li > .lst-wrap > .labels > .type {
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
background: #a56;
|
||||
color: #fff;
|
||||
border-radius: 3px 3px 3px 0;
|
||||
}
|
||||
|
||||
#connect-known-list-presets li > .lst-wrap > h4 {
|
||||
font-size: 1.3em;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#connect-known-list-presets-alert {
|
||||
font-size: 1.15em;
|
||||
color: #fff;
|
||||
background: #c73;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -19,53 +19,104 @@
|
||||
|
||||
<template>
|
||||
<div id="connect-known-list" :class="{ reloaded: reloaded }">
|
||||
<div v-if="knownList.length <= 0" id="connect-known-list-empty">
|
||||
<div
|
||||
v-if="knownList.length <= 0 && presets <= 0"
|
||||
id="connect-known-list-empty"
|
||||
>
|
||||
No known remote available
|
||||
</div>
|
||||
<ul v-else id="connect-known-list-list" class="hlst lstcl1">
|
||||
<li v-for="(known, kk) in knownList" :key="kk">
|
||||
<div class="labels">
|
||||
<span class="type" :style="'background-color: ' + known.data.color">
|
||||
{{ known.data.type }}
|
||||
</span>
|
||||
<div v-else>
|
||||
<div v-if="knownList.length > 0" id="connect-known-list-list">
|
||||
<h3>Connected before</h3>
|
||||
|
||||
<a
|
||||
class="opt link"
|
||||
href="javascript:;"
|
||||
@click="launcher(known, $event)"
|
||||
>
|
||||
{{ known.copyStatus }}
|
||||
</a>
|
||||
<ul class="hlst lstcl1">
|
||||
<li v-for="(known, kk) in knownList" :key="kk">
|
||||
<div class="labels">
|
||||
<span
|
||||
class="type"
|
||||
:style="'background-color: ' + known.data.color"
|
||||
>
|
||||
{{ known.data.type }}
|
||||
</span>
|
||||
|
||||
<a
|
||||
v-if="!known.data.session"
|
||||
class="opt del"
|
||||
href="javascript:;"
|
||||
@click="remove(known.data.uid)"
|
||||
<a
|
||||
class="opt link"
|
||||
href="javascript:;"
|
||||
@click="launcher(known, $event)"
|
||||
>
|
||||
{{ known.copyStatus }}
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="!known.data.session"
|
||||
class="opt del"
|
||||
href="javascript:;"
|
||||
@click="remove(known.data.uid)"
|
||||
>
|
||||
Remove
|
||||
</a>
|
||||
<a
|
||||
v-else
|
||||
class="opt clr"
|
||||
href="javascript:;"
|
||||
title="Clear session data"
|
||||
@click="clearSession(known.data.uid)"
|
||||
>
|
||||
Clear
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="lst-wrap" @click="select(known.data)">
|
||||
<h4
|
||||
:title="known.data.title"
|
||||
:class="{ highlight: known.data.session }"
|
||||
>
|
||||
{{ known.data.title }}
|
||||
</h4>
|
||||
Last: {{ known.data.last.toLocaleString() }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="presets.length > 0"
|
||||
id="connect-known-list-presets"
|
||||
:class="{
|
||||
'last-planel': knownList.length > 0
|
||||
}"
|
||||
>
|
||||
<h3>Presets</h3>
|
||||
|
||||
<ul class="hlst lstcl2">
|
||||
<li
|
||||
v-for="(preset, pk) in presets"
|
||||
:key="pk"
|
||||
:class="{ disabled: presetDisabled(preset) }"
|
||||
>
|
||||
Remove
|
||||
</a>
|
||||
<a
|
||||
v-else
|
||||
class="opt clr"
|
||||
href="javascript:;"
|
||||
title="Clear session data"
|
||||
@click="clearSession(known.data.uid)"
|
||||
>
|
||||
Clear
|
||||
</a>
|
||||
<div class="lst-wrap" @click="selectPreset(preset)">
|
||||
<div class="labels">
|
||||
<span
|
||||
class="type"
|
||||
:style="'background-color: ' + preset.command.color()"
|
||||
>
|
||||
{{ preset.command.name() }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h4 :title="preset.preset.title()">
|
||||
{{ preset.preset.title() }}
|
||||
</h4>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="restrictedToPresets" id="connect-known-list-presets-alert">
|
||||
The operator has disabled outgoing access to all remote hosts except
|
||||
those been defined as preset.
|
||||
</div>
|
||||
<div class="lst-wrap" @click="select(known.data)">
|
||||
<h2
|
||||
:title="known.data.title"
|
||||
:class="{ highlight: known.data.session }"
|
||||
>
|
||||
{{ known.data.title }}
|
||||
</h2>
|
||||
Last: {{ known.data.last.toLocaleString() }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="connect-known-list-import">
|
||||
Tip: You can
|
||||
@@ -81,6 +132,14 @@ import "./connect_known.css";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
presets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
restrictedToPresets: {
|
||||
type: Boolean,
|
||||
default: () => false
|
||||
},
|
||||
knowns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
@@ -147,6 +206,20 @@ export default {
|
||||
|
||||
this.$emit("select", known);
|
||||
},
|
||||
presetDisabled(preset) {
|
||||
if (!this.restrictedToPresets || preset.preset.host().length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
selectPreset(preset) {
|
||||
if (this.busy || this.presetDisabled(preset)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit("select-preset", preset);
|
||||
},
|
||||
async launcher(known, ev) {
|
||||
if (known.copying || this.busy) {
|
||||
return;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
export function head(url, headers) {
|
||||
export function get(url, headers) {
|
||||
return new Promise((res, rej) => {
|
||||
let authReq = new XMLHttpRequest();
|
||||
|
||||
@@ -35,7 +35,7 @@ export function head(url, headers) {
|
||||
rej(e);
|
||||
};
|
||||
|
||||
authReq.open("HEAD", url, true);
|
||||
authReq.open("GET", url, true);
|
||||
|
||||
for (let h in headers) {
|
||||
authReq.setRequestHeader(h, headers[h]);
|
||||
|
||||
Reference in New Issue
Block a user