Implemented the host name auto suggestion, and added Preset feature
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user