mirror of
https://github.com/go-gost/gost-plus.git
synced 2024-08-11 17:43:16 +00:00
initial commit
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
*.syso
|
||||
*.apk
|
||||
*.idsig
|
||||
gost-plus
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 GOST
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,16 @@
|
||||
.PHONY: linux
|
||||
|
||||
linux:
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build
|
||||
|
||||
win:
|
||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-H windowsgui"
|
||||
|
||||
arm:
|
||||
GOOS=linux GOARCH=arm64 CGO_ENABLED=1 go build
|
||||
|
||||
android:
|
||||
gogio -x -work -target android -minsdk 22 -version 1 -appid gost.plus github.com/go-gost/gost-plus
|
||||
|
||||
clean:
|
||||
rm gost-plus.exe gost-plus gost-plus.apk
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
@@ -0,0 +1,136 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"gioui.org/app"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/x/config"
|
||||
xconfig "github.com/go-gost/x/config"
|
||||
logger_parser "github.com/go-gost/x/config/parsing/logger"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
configFile = "config.yml"
|
||||
)
|
||||
|
||||
var (
|
||||
configDir string
|
||||
)
|
||||
|
||||
func Init() {
|
||||
log.SetFlags(log.Lshortfile | log.LstdFlags | log.Lmicroseconds)
|
||||
|
||||
dir, err := app.DataDir()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if dir == "" {
|
||||
dir, _ = os.Getwd()
|
||||
}
|
||||
configDir = filepath.Join(dir, "gost.plus")
|
||||
os.MkdirAll(configDir, 0755)
|
||||
|
||||
log.Println("config dir:", configDir)
|
||||
|
||||
if err := global.Load(); err != nil {
|
||||
log.Println(err)
|
||||
if _, ok := err.(*os.PathError); ok {
|
||||
global.Write()
|
||||
}
|
||||
}
|
||||
if global.Log == nil {
|
||||
logDir := filepath.Join(configDir, "logs")
|
||||
os.MkdirAll(logDir, 0755)
|
||||
log.Println("log dir:", logDir)
|
||||
|
||||
global.Log = &xconfig.LogConfig{
|
||||
Output: filepath.Join(logDir, "gost-plus.log"),
|
||||
Level: string(logger.InfoLevel),
|
||||
Format: string(logger.JSONFormat),
|
||||
Rotation: &xconfig.LogRotationConfig{
|
||||
MaxSize: 10,
|
||||
MaxAge: 7,
|
||||
MaxBackups: 10,
|
||||
LocalTime: true,
|
||||
Compress: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
logger.SetDefault(logger_parser.ParseLogger(&xconfig.LoggerConfig{Log: global.Log}))
|
||||
}
|
||||
|
||||
var (
|
||||
global = &Config{}
|
||||
globalMux sync.RWMutex
|
||||
)
|
||||
|
||||
func Global() *Config {
|
||||
globalMux.RLock()
|
||||
defer globalMux.RUnlock()
|
||||
|
||||
cfg := &Config{}
|
||||
*cfg = *global
|
||||
return cfg
|
||||
}
|
||||
|
||||
func Set(c *Config) {
|
||||
globalMux.Lock()
|
||||
defer globalMux.Unlock()
|
||||
|
||||
global = c
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
Lang string
|
||||
Theme string
|
||||
}
|
||||
|
||||
type Tunnel struct {
|
||||
ID string
|
||||
Name string
|
||||
Type string
|
||||
Endpoint string
|
||||
Hostname string `yaml:",omitempty"`
|
||||
Username string `yaml:",omitempty"`
|
||||
Password string `yaml:",omitempty"`
|
||||
EnableTLS bool `yaml:"enableTLS,omitempty"`
|
||||
|
||||
Favorite bool
|
||||
Closed bool
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Settings *Settings
|
||||
Tunnels []*Tunnel
|
||||
Log *config.LogConfig
|
||||
}
|
||||
|
||||
func (c *Config) Load() error {
|
||||
f, err := os.Open(filepath.Join(configDir, configFile))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return yaml.NewDecoder(f).Decode(c)
|
||||
}
|
||||
|
||||
func (c *Config) Write() error {
|
||||
var buf bytes.Buffer
|
||||
enc := yaml.NewEncoder(&buf)
|
||||
defer enc.Close()
|
||||
|
||||
enc.SetIndent(2)
|
||||
if err := enc.Encode(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filepath.Join(configDir, configFile), buf.Bytes(), 0644)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
module github.com/go-gost/gost-plus
|
||||
|
||||
go 1.21.1
|
||||
|
||||
require (
|
||||
gioui.org v0.4.1
|
||||
gioui.org/x v0.4.0
|
||||
github.com/go-gost/core v0.0.0-20231119081403-abc73f2ca2b7
|
||||
github.com/go-gost/x v0.0.0-20231130113937-b1390dda1cc8
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/spf13/viper v1.18.1
|
||||
golang.org/x/exp/shiny v0.0.0-20231206192017-f3f8817b8deb
|
||||
)
|
||||
|
||||
require (
|
||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect
|
||||
gioui.org/shader v1.0.8 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-gost/gosocks5 v0.4.0 // indirect
|
||||
github.com/go-gost/plugin v0.0.0-20231119084331-d49a1cb23b3b // indirect
|
||||
github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7 // indirect
|
||||
github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.1 // indirect
|
||||
github.com/rs/xid v1.3.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/xtaci/smux v1.5.24 // indirect
|
||||
github.com/yl2chen/cidranger v1.0.2 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect
|
||||
golang.org/x/image v0.7.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,197 @@
|
||||
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
|
||||
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
|
||||
gioui.org v0.4.1 h1:sCTw5Fexg0xg9CxYmbrkKtHXcobf0JMbl5XpF2TC/zc=
|
||||
gioui.org v0.4.1/go.mod h1:2atiYR4upH71/6ehnh6XsUELa7JZOrOHHNMDxGBZF0Q=
|
||||
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
|
||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
|
||||
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
||||
gioui.org/x v0.4.0 h1:H6DofC86KoG51wgzeeA4ujZDDfcIa8vbL+jD9SpF/D8=
|
||||
gioui.org/x v0.4.0/go.mod h1:YAoFl2lbeARk4LopDXHK1N7fBQJupPYDSm9maf6tFlM=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-gost/core v0.0.0-20231119081403-abc73f2ca2b7 h1:fxVUlZANqPApygO7lT8bYySyajiCFA62bDiNorral1w=
|
||||
github.com/go-gost/core v0.0.0-20231119081403-abc73f2ca2b7/go.mod h1:ndkgWVYRLwupVaFFWv8ML1Nr8tD3xhHK245PLpUDg4E=
|
||||
github.com/go-gost/gosocks5 v0.4.0 h1:EIrOEkpJez4gwHrMa33frA+hHXJyevjp47thpMQsJzI=
|
||||
github.com/go-gost/gosocks5 v0.4.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4=
|
||||
github.com/go-gost/plugin v0.0.0-20231119084331-d49a1cb23b3b h1:ZmnYutflq+KOZK+Px5RDckorDSxTYlkT4aQbjTC8/C4=
|
||||
github.com/go-gost/plugin v0.0.0-20231119084331-d49a1cb23b3b/go.mod h1:qXr2Zm9Ex2ATqnWuNUzVZqySPMnuIihvblYZt4MlZLw=
|
||||
github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7 h1:qAG1OyjvdA5h221CfFSS3J359V3d2E7dJWyP29QoDSI=
|
||||
github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8=
|
||||
github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451 h1:xj8gUZGYO3nb5+6Bjw9+tsFkA9sYynrOvDvvC4uDV2I=
|
||||
github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs=
|
||||
github.com/go-gost/x v0.0.0-20231130113937-b1390dda1cc8 h1:aM2tAfTg4+oBiB161GrYbRBhUHQM1CYNiDNYsqoVAlE=
|
||||
github.com/go-gost/x v0.0.0-20231130113937-b1390dda1cc8/go.mod h1:YaeMQsu+I8Q3bxPFeo5MbnmLsAdbGUxxG3A4mvUt2YE=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
|
||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.1 h1:rmuU42rScKWlhhJDyXZRKJQHXFX02chSVW1IvkPGiVM=
|
||||
github.com/spf13/viper v1.18.1/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/xtaci/smux v1.5.24 h1:77emW9dtnOxxOQ5ltR+8BbsX1kzcOxQ5gB+aaV9hXOY=
|
||||
github.com/xtaci/smux v1.5.24/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
|
||||
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
|
||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8=
|
||||
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/exp/shiny v0.0.0-20231206192017-f3f8817b8deb h1:t3SA1mKG2eyhChDjOL0n0VLKZQmqU4J8UlCoQ0+nqSk=
|
||||
golang.org/x/exp/shiny v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
|
||||
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
|
||||
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,49 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
_ "net"
|
||||
"os"
|
||||
|
||||
"gioui.org/app"
|
||||
_ "gioui.org/app/permission/storage"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"github.com/go-gost/gost-plus/config"
|
||||
"github.com/go-gost/gost-plus/tunnel"
|
||||
"github.com/go-gost/gost-plus/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config.Init()
|
||||
tunnel.LoadConfig()
|
||||
|
||||
go func() {
|
||||
w := app.NewWindow(
|
||||
app.Title("GOST.PLUS"),
|
||||
app.MinSize(800, 600),
|
||||
)
|
||||
err := run(w)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
app.Main()
|
||||
}
|
||||
|
||||
func run(w *app.Window) error {
|
||||
ui := ui.NewUI()
|
||||
var ops op.Ops
|
||||
for {
|
||||
switch e := w.NextEvent().(type) {
|
||||
case system.DestroyEvent:
|
||||
return e.Err
|
||||
case system.FrameEvent:
|
||||
gtx := layout.NewContext(&ops, e)
|
||||
ui.Layout(gtx)
|
||||
e.Frame(gtx.Ops)
|
||||
}
|
||||
}
|
||||
}
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/go-gost/core/auth"
|
||||
"github.com/go-gost/core/chain"
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/service"
|
||||
xauth "github.com/go-gost/x/auth"
|
||||
"github.com/go-gost/x/config"
|
||||
chain_parser "github.com/go-gost/x/config/parsing/chain"
|
||||
"github.com/go-gost/x/handler/file"
|
||||
"github.com/go-gost/x/handler/forward/remote"
|
||||
"github.com/go-gost/x/hop"
|
||||
"github.com/go-gost/x/listener/rtcp"
|
||||
"github.com/go-gost/x/listener/tcp"
|
||||
mdx "github.com/go-gost/x/metadata"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type fileTunnel struct {
|
||||
endpoint string
|
||||
opts Options
|
||||
config *config.Config
|
||||
file service.Service
|
||||
forward service.Service
|
||||
favorite atomic.Bool
|
||||
|
||||
cclose chan struct{}
|
||||
}
|
||||
|
||||
func NewFileTunnel(opts ...Option) Tunnel {
|
||||
var options Options
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
if options.ID == "" {
|
||||
options.ID = uuid.NewString()
|
||||
}
|
||||
|
||||
v := md5.Sum([]byte(options.ID))
|
||||
endpoint := hex.EncodeToString(v[:8])
|
||||
|
||||
if options.Endpoint == "" {
|
||||
options.Endpoint, _ = os.Getwd()
|
||||
}
|
||||
|
||||
if options.Name == "" {
|
||||
options.Name = fmt.Sprintf("FILE-%s", endpoint)
|
||||
}
|
||||
|
||||
s := &fileTunnel{
|
||||
endpoint: endpoint,
|
||||
opts: options,
|
||||
cclose: make(chan struct{}),
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *fileTunnel) ID() string {
|
||||
return s.opts.ID
|
||||
}
|
||||
|
||||
func (s *fileTunnel) Type() string {
|
||||
return FileTunnel
|
||||
}
|
||||
|
||||
func (s *fileTunnel) Name() string {
|
||||
return s.opts.Name
|
||||
}
|
||||
|
||||
func (s *fileTunnel) Endpoint() string {
|
||||
return s.opts.Endpoint
|
||||
}
|
||||
|
||||
func (f *fileTunnel) Entrypoint() string {
|
||||
return fmt.Sprintf("https://%s.%s", f.endpoint, endpointAddr)
|
||||
}
|
||||
|
||||
func (s *fileTunnel) Options() Options {
|
||||
return s.opts
|
||||
}
|
||||
|
||||
func (s *fileTunnel) Favorite(b bool) {
|
||||
s.favorite.Store(b)
|
||||
}
|
||||
|
||||
func (s *fileTunnel) IsFavorite() bool {
|
||||
return s.favorite.Load()
|
||||
}
|
||||
|
||||
func (s *fileTunnel) init() error {
|
||||
file := &config.ServiceConfig{
|
||||
Name: s.opts.Name,
|
||||
Addr: ":0",
|
||||
Handler: &config.HandlerConfig{
|
||||
Type: "file",
|
||||
Metadata: map[string]any{"file.dir": s.opts.Endpoint},
|
||||
},
|
||||
Listener: &config.ListenerConfig{
|
||||
Type: "tcp",
|
||||
},
|
||||
}
|
||||
if s.opts.Username != "" {
|
||||
file.Handler.Auth = &config.AuthConfig{
|
||||
Username: s.opts.Username,
|
||||
Password: s.opts.Password,
|
||||
}
|
||||
}
|
||||
|
||||
rtcp := &config.ServiceConfig{
|
||||
Name: s.opts.Name,
|
||||
Addr: s.opts.Hostname,
|
||||
Handler: &config.HandlerConfig{
|
||||
Type: "rtcp",
|
||||
},
|
||||
Listener: &config.ListenerConfig{
|
||||
Type: "rtcp",
|
||||
Chain: s.opts.Name,
|
||||
},
|
||||
}
|
||||
|
||||
s.config = &config.Config{
|
||||
Services: []*config.ServiceConfig{file, rtcp},
|
||||
Chains: []*config.ChainConfig{chainConfig(s.opts.ID, s.opts.Name)},
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fileTunnel) Run() error {
|
||||
if s.IsClosed() {
|
||||
return ErrTunnelClosed
|
||||
}
|
||||
|
||||
if err := s.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log := logger.Default().WithFields(map[string]any{
|
||||
"kind": "service",
|
||||
"service": s.opts.Name,
|
||||
})
|
||||
|
||||
{
|
||||
cfg := s.config.Services[0]
|
||||
ln := tcp.NewListener(
|
||||
listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "tcp"})),
|
||||
)
|
||||
if err := ln.Init(nil); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("listen on %s", ln.Addr())
|
||||
|
||||
var auther auth.Authenticator
|
||||
if auth := cfg.Handler.Auth; auth != nil {
|
||||
auther = xauth.NewAuthenticator(xauth.AuthsOption(map[string]string{auth.Username: auth.Password}))
|
||||
}
|
||||
h := file.NewHandler(
|
||||
handler.LoggerOption(log.WithFields(map[string]any{"kind": "handler", "handler": "file"})),
|
||||
handler.AutherOption(auther),
|
||||
)
|
||||
if err := h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil {
|
||||
return err
|
||||
}
|
||||
s.file = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log))
|
||||
}
|
||||
|
||||
{
|
||||
ch, err := chain_parser.ParseChain(s.config.Chains[0], log)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := s.config.Services[1]
|
||||
ln := rtcp.NewListener(
|
||||
listener.AddrOption(cfg.Addr),
|
||||
listener.ChainOption(ch),
|
||||
listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "rtcp"})),
|
||||
)
|
||||
if err := ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h := remote.NewHandler(
|
||||
handler.LoggerOption(log.WithFields(map[string]any{"kind": "handler", "handler": "rtcp"})),
|
||||
)
|
||||
if err := h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil {
|
||||
return err
|
||||
}
|
||||
if forwarder, ok := h.(handler.Forwarder); ok {
|
||||
forwarder.Forward(hop.NewHop(
|
||||
hop.NodeOption(chain.NewNode(s.opts.Name, s.file.Addr().String())),
|
||||
hop.LoggerOption(log.WithFields(map[string]any{"kind": "hop"})),
|
||||
))
|
||||
}
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log))
|
||||
log.Infof("service listen on %s", s.file.Addr())
|
||||
}
|
||||
|
||||
go s.file.Serve()
|
||||
go s.forward.Serve()
|
||||
|
||||
log.Infof("file service run at %s", s.file.Addr())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fileTunnel) Close() error {
|
||||
defer func() {
|
||||
select {
|
||||
case <-s.cclose:
|
||||
default:
|
||||
close(s.cclose)
|
||||
}
|
||||
}()
|
||||
|
||||
if s.forward != nil {
|
||||
s.forward.Close()
|
||||
}
|
||||
if s.file != nil {
|
||||
return s.file.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fileTunnel) IsClosed() bool {
|
||||
select {
|
||||
case <-s.cclose:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/go-gost/core/chain"
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/service"
|
||||
xauth "github.com/go-gost/x/auth"
|
||||
"github.com/go-gost/x/config"
|
||||
chain_parser "github.com/go-gost/x/config/parsing/chain"
|
||||
"github.com/go-gost/x/handler/forward/remote"
|
||||
"github.com/go-gost/x/hop"
|
||||
"github.com/go-gost/x/listener/rtcp"
|
||||
mdx "github.com/go-gost/x/metadata"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type httpTunnel struct {
|
||||
endpoint string
|
||||
opts Options
|
||||
config *config.Config
|
||||
forward service.Service
|
||||
favorite atomic.Bool
|
||||
|
||||
cclose chan struct{}
|
||||
}
|
||||
|
||||
func NewHTTPTunnel(opts ...Option) Tunnel {
|
||||
var options Options
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
if options.ID == "" {
|
||||
options.ID = uuid.NewString()
|
||||
}
|
||||
|
||||
v := md5.Sum([]byte(options.ID))
|
||||
endpoint := hex.EncodeToString(v[:8])
|
||||
|
||||
if options.Endpoint == "" {
|
||||
options.Endpoint = "localhost:8080"
|
||||
}
|
||||
|
||||
if options.Name == "" {
|
||||
options.Name = fmt.Sprintf("HTTP-%s", endpoint)
|
||||
}
|
||||
|
||||
s := &httpTunnel{
|
||||
endpoint: endpoint,
|
||||
opts: options,
|
||||
cclose: make(chan struct{}),
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *httpTunnel) ID() string {
|
||||
return s.opts.ID
|
||||
}
|
||||
|
||||
func (s *httpTunnel) Type() string {
|
||||
return HTTPTunnel
|
||||
}
|
||||
|
||||
func (s *httpTunnel) Name() string {
|
||||
return s.opts.Name
|
||||
}
|
||||
|
||||
func (s *httpTunnel) Endpoint() string {
|
||||
return s.opts.Endpoint
|
||||
}
|
||||
|
||||
func (f *httpTunnel) Entrypoint() string {
|
||||
return fmt.Sprintf("https://%s.%s", f.endpoint, endpointAddr)
|
||||
}
|
||||
|
||||
func (s *httpTunnel) Options() Options {
|
||||
return s.opts
|
||||
}
|
||||
|
||||
func (s *httpTunnel) Favorite(b bool) {
|
||||
s.favorite.Store(b)
|
||||
}
|
||||
|
||||
func (s *httpTunnel) IsFavorite() bool {
|
||||
return s.favorite.Load()
|
||||
}
|
||||
|
||||
func (s *httpTunnel) init() error {
|
||||
node := &config.ForwardNodeConfig{
|
||||
Name: s.opts.Name,
|
||||
Addr: s.opts.Endpoint,
|
||||
}
|
||||
if s.opts.Username != "" {
|
||||
node.Auth = &config.AuthConfig{
|
||||
Username: s.opts.Username,
|
||||
Password: s.opts.Password,
|
||||
}
|
||||
}
|
||||
if s.opts.Hostname != "" {
|
||||
node.HTTP = &config.HTTPNodeConfig{
|
||||
Host: s.opts.Hostname,
|
||||
}
|
||||
}
|
||||
if s.opts.EnableTLS {
|
||||
node.TLS = &config.TLSNodeConfig{}
|
||||
}
|
||||
|
||||
rtcp := &config.ServiceConfig{
|
||||
Name: s.opts.Name,
|
||||
Addr: "",
|
||||
Handler: &config.HandlerConfig{
|
||||
Type: "rtcp",
|
||||
Metadata: map[string]any{
|
||||
"sniffing": true,
|
||||
},
|
||||
},
|
||||
Listener: &config.ListenerConfig{
|
||||
Type: "rtcp",
|
||||
Chain: s.opts.Name,
|
||||
},
|
||||
Forwarder: &config.ForwarderConfig{
|
||||
Nodes: []*config.ForwardNodeConfig{node},
|
||||
},
|
||||
}
|
||||
|
||||
s.config = &config.Config{
|
||||
Services: []*config.ServiceConfig{rtcp},
|
||||
Chains: []*config.ChainConfig{chainConfig(s.opts.ID, s.opts.Name)},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *httpTunnel) Run() error {
|
||||
if s.IsClosed() {
|
||||
return ErrTunnelClosed
|
||||
}
|
||||
|
||||
if err := s.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log := logger.Default().WithFields(map[string]any{
|
||||
"kind": "service",
|
||||
"service": s.opts.Name,
|
||||
})
|
||||
|
||||
{
|
||||
ch, err := chain_parser.ParseChain(s.config.Chains[0], log)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := s.config.Services[0]
|
||||
ln := rtcp.NewListener(
|
||||
listener.AddrOption(cfg.Addr),
|
||||
listener.ChainOption(ch),
|
||||
listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "rtcp"})),
|
||||
)
|
||||
if err := ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h := remote.NewHandler(
|
||||
handler.LoggerOption(log.WithFields(map[string]any{"kind": "handler", "handler": "rtcp"})),
|
||||
)
|
||||
if err := h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
node := cfg.Forwarder.Nodes[0]
|
||||
var nodeOpts []chain.NodeOption
|
||||
if node.Auth != nil {
|
||||
auther := xauth.NewAuthenticator(xauth.AuthsOption(map[string]string{node.Auth.Username: node.Auth.Password}))
|
||||
nodeOpts = append(nodeOpts, chain.AutherNodeOption(auther))
|
||||
}
|
||||
if node.HTTP != nil {
|
||||
nodeOpts = append(nodeOpts, chain.HTTPNodeOption(&chain.HTTPNodeSettings{
|
||||
Host: node.HTTP.Host,
|
||||
Header: node.HTTP.Header,
|
||||
}))
|
||||
}
|
||||
if node.TLS != nil {
|
||||
nodeOpts = append(nodeOpts, chain.TLSNodeOption(&chain.TLSNodeSettings{
|
||||
ServerName: node.TLS.ServerName,
|
||||
Secure: node.TLS.Secure,
|
||||
}))
|
||||
}
|
||||
if forwarder, ok := h.(handler.Forwarder); ok {
|
||||
forwarder.Forward(hop.NewHop(hop.NodeOption(chain.NewNode(node.Name, node.Addr, nodeOpts...)),
|
||||
hop.LoggerOption(log.WithFields(map[string]any{"kind": "hop"})),
|
||||
))
|
||||
}
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log))
|
||||
}
|
||||
|
||||
go s.forward.Serve()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *httpTunnel) Close() error {
|
||||
defer func() {
|
||||
select {
|
||||
case <-s.cclose:
|
||||
default:
|
||||
close(s.cclose)
|
||||
}
|
||||
}()
|
||||
|
||||
if s.forward != nil {
|
||||
return s.forward.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *httpTunnel) IsClosed() bool {
|
||||
select {
|
||||
case <-s.cclose:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/go-gost/core/chain"
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/service"
|
||||
"github.com/go-gost/x/config"
|
||||
chain_parser "github.com/go-gost/x/config/parsing/chain"
|
||||
"github.com/go-gost/x/handler/forward/remote"
|
||||
"github.com/go-gost/x/hop"
|
||||
"github.com/go-gost/x/listener/rtcp"
|
||||
mdx "github.com/go-gost/x/metadata"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type tcpTunnel struct {
|
||||
endpoint string
|
||||
opts Options
|
||||
config *config.Config
|
||||
forward service.Service
|
||||
favorite atomic.Bool
|
||||
|
||||
cclose chan struct{}
|
||||
}
|
||||
|
||||
func NewTCPTunnel(opts ...Option) Tunnel {
|
||||
var options Options
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
if options.ID == "" {
|
||||
options.ID = uuid.NewString()
|
||||
}
|
||||
|
||||
v := md5.Sum([]byte(options.ID))
|
||||
endpoint := hex.EncodeToString(v[:8])
|
||||
|
||||
if options.Endpoint == "" {
|
||||
options.Endpoint = "localhost:8080"
|
||||
}
|
||||
|
||||
if options.Name == "" {
|
||||
options.Name = fmt.Sprintf("TCP-%s", endpoint)
|
||||
}
|
||||
|
||||
s := &tcpTunnel{
|
||||
endpoint: endpoint,
|
||||
opts: options,
|
||||
cclose: make(chan struct{}),
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *tcpTunnel) ID() string {
|
||||
return s.opts.ID
|
||||
}
|
||||
|
||||
func (s *tcpTunnel) Type() string {
|
||||
return TCPTunnel
|
||||
}
|
||||
|
||||
func (s *tcpTunnel) Name() string {
|
||||
return s.opts.Name
|
||||
}
|
||||
|
||||
func (s *tcpTunnel) Endpoint() string {
|
||||
return s.opts.Endpoint
|
||||
}
|
||||
|
||||
func (f *tcpTunnel) Entrypoint() string {
|
||||
return fmt.Sprintf("%s.%s", f.endpoint, endpointAddr)
|
||||
}
|
||||
|
||||
func (s *tcpTunnel) Options() Options {
|
||||
return s.opts
|
||||
}
|
||||
|
||||
func (s *tcpTunnel) Favorite(b bool) {
|
||||
s.favorite.Store(b)
|
||||
}
|
||||
|
||||
func (s *tcpTunnel) IsFavorite() bool {
|
||||
return s.favorite.Load()
|
||||
}
|
||||
|
||||
func (s *tcpTunnel) init() error {
|
||||
rtcp := &config.ServiceConfig{
|
||||
Name: s.opts.Name,
|
||||
Addr: s.opts.Hostname,
|
||||
Handler: &config.HandlerConfig{
|
||||
Type: "rtcp",
|
||||
},
|
||||
Listener: &config.ListenerConfig{
|
||||
Type: "rtcp",
|
||||
Chain: s.opts.Name,
|
||||
},
|
||||
Forwarder: &config.ForwarderConfig{
|
||||
Nodes: []*config.ForwardNodeConfig{
|
||||
{
|
||||
Name: s.opts.Name,
|
||||
Addr: s.opts.Endpoint,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s.config = &config.Config{
|
||||
Services: []*config.ServiceConfig{rtcp},
|
||||
Chains: []*config.ChainConfig{chainConfig(s.opts.ID, s.opts.Name)},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tcpTunnel) Run() error {
|
||||
if s.IsClosed() {
|
||||
return ErrTunnelClosed
|
||||
}
|
||||
|
||||
if err := s.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log := logger.Default().WithFields(map[string]any{
|
||||
"kind": "service",
|
||||
"service": s.opts.Name,
|
||||
})
|
||||
|
||||
{
|
||||
ch, err := chain_parser.ParseChain(s.config.Chains[0], log)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := s.config.Services[0]
|
||||
ln := rtcp.NewListener(
|
||||
listener.AddrOption(cfg.Addr),
|
||||
listener.ChainOption(ch),
|
||||
listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "rtcp"})),
|
||||
)
|
||||
if err := ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h := remote.NewHandler(
|
||||
handler.LoggerOption(log.WithFields(map[string]any{"kind": "handler", "handler": "rtcp"})),
|
||||
)
|
||||
if err := h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
node := cfg.Forwarder.Nodes[0]
|
||||
if forwarder, ok := h.(handler.Forwarder); ok {
|
||||
forwarder.Forward(hop.NewHop(
|
||||
hop.NodeOption(chain.NewNode(node.Name, node.Addr)),
|
||||
hop.LoggerOption(log.WithFields(map[string]any{"kind": "hop"})),
|
||||
))
|
||||
}
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log))
|
||||
}
|
||||
|
||||
go s.forward.Serve()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tcpTunnel) Close() error {
|
||||
defer func() {
|
||||
select {
|
||||
case <-s.cclose:
|
||||
default:
|
||||
close(s.cclose)
|
||||
}
|
||||
}()
|
||||
|
||||
if s.forward != nil {
|
||||
return s.forward.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tcpTunnel) IsClosed() bool {
|
||||
select {
|
||||
case <-s.cclose:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/gost-plus/config"
|
||||
xconfig "github.com/go-gost/x/config"
|
||||
_ "github.com/go-gost/x/connector/tunnel"
|
||||
_ "github.com/go-gost/x/dialer/ws"
|
||||
)
|
||||
|
||||
const (
|
||||
endpointAddr = "gost.plus"
|
||||
serverName = "tunnel.gost.plus"
|
||||
serverAddr = serverName + ":443"
|
||||
)
|
||||
|
||||
const (
|
||||
FileTunnel = "file"
|
||||
HTTPTunnel = "http"
|
||||
TCPTunnel = "tcp"
|
||||
UDPTunnel = "udp"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTunnelClosed = errors.New("tunnel closed")
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
ID string
|
||||
Name string
|
||||
Endpoint string
|
||||
Hostname string
|
||||
Username string
|
||||
Password string
|
||||
EnableTLS bool
|
||||
}
|
||||
|
||||
type Option func(opts *Options)
|
||||
|
||||
func IDOption(id string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.ID = id
|
||||
}
|
||||
}
|
||||
|
||||
func NameOption(name string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
func EndpointOption(endpoint string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Endpoint = endpoint
|
||||
}
|
||||
}
|
||||
|
||||
func HostnameOption(hostname string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Hostname = hostname
|
||||
}
|
||||
}
|
||||
|
||||
func UsernameOption(username string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Username = username
|
||||
}
|
||||
}
|
||||
|
||||
func PasswordOption(password string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Password = password
|
||||
}
|
||||
}
|
||||
|
||||
func EnableTLSOption(b bool) Option {
|
||||
return func(opts *Options) {
|
||||
opts.EnableTLS = b
|
||||
}
|
||||
}
|
||||
|
||||
type Tunnel interface {
|
||||
ID() string
|
||||
Type() string
|
||||
Name() string
|
||||
Endpoint() string
|
||||
Entrypoint() string
|
||||
Options() Options
|
||||
Run() error
|
||||
Favorite(b bool)
|
||||
IsFavorite() bool
|
||||
Close() error
|
||||
IsClosed() bool
|
||||
}
|
||||
|
||||
type tunnelList struct {
|
||||
tunnels []Tunnel
|
||||
mux sync.RWMutex
|
||||
}
|
||||
|
||||
func (sl *tunnelList) Count() int {
|
||||
sl.mux.RLock()
|
||||
defer sl.mux.RUnlock()
|
||||
return len(sl.tunnels)
|
||||
}
|
||||
|
||||
func (sl *tunnelList) Add(s Tunnel) {
|
||||
sl.mux.Lock()
|
||||
defer sl.mux.Unlock()
|
||||
sl.tunnels = append(sl.tunnels, s)
|
||||
}
|
||||
|
||||
func (sl *tunnelList) Set(s Tunnel) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
sl.mux.Lock()
|
||||
defer sl.mux.Unlock()
|
||||
|
||||
for i, sv := range sl.tunnels {
|
||||
if sv != nil && sv.ID() == s.ID() {
|
||||
sl.tunnels[i] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sl *tunnelList) Get(index int) Tunnel {
|
||||
sl.mux.RLock()
|
||||
defer sl.mux.RUnlock()
|
||||
if index < 0 || index >= len(sl.tunnels) {
|
||||
return nil
|
||||
}
|
||||
return sl.tunnels[index]
|
||||
}
|
||||
|
||||
func (sl *tunnelList) GetID(id string) Tunnel {
|
||||
sl.mux.RLock()
|
||||
defer sl.mux.RUnlock()
|
||||
|
||||
for _, s := range sl.tunnels {
|
||||
if s != nil && s.ID() == id {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sl *tunnelList) DeleteID(id string) {
|
||||
sl.mux.Lock()
|
||||
defer sl.mux.Unlock()
|
||||
|
||||
for i, s := range sl.tunnels {
|
||||
if s != nil && s.ID() == id {
|
||||
s.Close()
|
||||
sl.tunnels[i] = nil
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
tunnels tunnelList
|
||||
)
|
||||
|
||||
func TunnelCount() int {
|
||||
return tunnels.Count()
|
||||
}
|
||||
|
||||
func AddTunnel(s Tunnel) {
|
||||
tunnels.Add(s)
|
||||
}
|
||||
|
||||
func SetTunnel(s Tunnel) {
|
||||
t := tunnels.GetID(s.ID())
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
s.Favorite(t.IsFavorite())
|
||||
tunnels.Set(s)
|
||||
}
|
||||
|
||||
func GetTunnel(index int) Tunnel {
|
||||
return tunnels.Get(index)
|
||||
}
|
||||
|
||||
func GetTunnelID(id string) Tunnel {
|
||||
return tunnels.GetID(id)
|
||||
}
|
||||
|
||||
func DeleteTunnel(id string) {
|
||||
tunnels.DeleteID(id)
|
||||
}
|
||||
|
||||
func chainConfig(id string, name string) *xconfig.ChainConfig {
|
||||
return &xconfig.ChainConfig{
|
||||
Name: name,
|
||||
Hops: []*xconfig.HopConfig{
|
||||
{
|
||||
Name: name,
|
||||
Nodes: []*xconfig.NodeConfig{
|
||||
{
|
||||
Name: name,
|
||||
Addr: serverAddr,
|
||||
Connector: &xconfig.ConnectorConfig{
|
||||
Type: "tunnel",
|
||||
Metadata: map[string]any{"tunnel.id": id},
|
||||
},
|
||||
Dialer: &xconfig.DialerConfig{
|
||||
Type: "wss",
|
||||
TLS: &xconfig.TLSConfig{
|
||||
Secure: true,
|
||||
ServerName: serverName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func LoadConfig() {
|
||||
for _, tun := range config.Global().Tunnels {
|
||||
if tun == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
s := createTunnel(tun.Type, Options{
|
||||
ID: tun.ID,
|
||||
Name: tun.Name,
|
||||
Endpoint: tun.Endpoint,
|
||||
Hostname: tun.Hostname,
|
||||
Username: tun.Username,
|
||||
Password: tun.Password,
|
||||
EnableTLS: tun.EnableTLS,
|
||||
})
|
||||
if s == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if tun.Closed {
|
||||
s.Close()
|
||||
} else {
|
||||
s.Run()
|
||||
}
|
||||
|
||||
s.Favorite(tun.Favorite)
|
||||
tunnels.Add(s)
|
||||
}
|
||||
}
|
||||
|
||||
func SaveTunnel() error {
|
||||
cfg := config.Global()
|
||||
cfg.Tunnels = nil
|
||||
|
||||
for i := 0; i < tunnels.Count(); i++ {
|
||||
tun := tunnels.Get(i)
|
||||
if tun == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
opts := tun.Options()
|
||||
|
||||
cfg.Tunnels = append(cfg.Tunnels, &config.Tunnel{
|
||||
ID: tun.ID(),
|
||||
Name: tun.Name(),
|
||||
Type: tun.Type(),
|
||||
Endpoint: tun.Endpoint(),
|
||||
Hostname: opts.Hostname,
|
||||
Username: opts.Username,
|
||||
Password: opts.Password,
|
||||
EnableTLS: opts.EnableTLS,
|
||||
Favorite: tun.IsFavorite(),
|
||||
Closed: tun.IsClosed(),
|
||||
})
|
||||
}
|
||||
|
||||
config.Set(cfg)
|
||||
|
||||
if err := cfg.Write(); err != nil {
|
||||
logger.Default().Error(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTunnel(st string, opts Options) Tunnel {
|
||||
options := []Option{
|
||||
IDOption(opts.ID),
|
||||
NameOption(opts.Name),
|
||||
EndpointOption(opts.Endpoint),
|
||||
HostnameOption(opts.Hostname),
|
||||
UsernameOption(opts.Username),
|
||||
PasswordOption(opts.Password),
|
||||
EnableTLSOption(opts.EnableTLS),
|
||||
}
|
||||
switch st {
|
||||
case FileTunnel:
|
||||
return NewFileTunnel(options...)
|
||||
case HTTPTunnel:
|
||||
return NewHTTPTunnel(options...)
|
||||
case TCPTunnel:
|
||||
return NewTCPTunnel(options...)
|
||||
case UDPTunnel:
|
||||
return NewUDPTunnel(options...)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/go-gost/core/chain"
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/service"
|
||||
"github.com/go-gost/x/config"
|
||||
chain_parser "github.com/go-gost/x/config/parsing/chain"
|
||||
"github.com/go-gost/x/handler/forward/remote"
|
||||
"github.com/go-gost/x/hop"
|
||||
"github.com/go-gost/x/listener/rudp"
|
||||
mdx "github.com/go-gost/x/metadata"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type udpTunnel struct {
|
||||
endpoint string
|
||||
opts Options
|
||||
config *config.Config
|
||||
forward service.Service
|
||||
favorite atomic.Bool
|
||||
|
||||
cclose chan struct{}
|
||||
}
|
||||
|
||||
func NewUDPTunnel(opts ...Option) Tunnel {
|
||||
var options Options
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
if options.ID == "" {
|
||||
options.ID = uuid.NewString()
|
||||
}
|
||||
|
||||
v := md5.Sum([]byte(options.ID))
|
||||
endpoint := hex.EncodeToString(v[:8])
|
||||
|
||||
if options.Endpoint == "" {
|
||||
options.Endpoint = "localhost:8080"
|
||||
}
|
||||
|
||||
if options.Name == "" {
|
||||
options.Name = fmt.Sprintf("UDP-%s", endpoint)
|
||||
}
|
||||
|
||||
s := &udpTunnel{
|
||||
endpoint: endpoint,
|
||||
opts: options,
|
||||
cclose: make(chan struct{}),
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *udpTunnel) ID() string {
|
||||
return s.opts.ID
|
||||
}
|
||||
|
||||
func (s *udpTunnel) Type() string {
|
||||
return UDPTunnel
|
||||
}
|
||||
|
||||
func (s *udpTunnel) Name() string {
|
||||
return s.opts.Name
|
||||
}
|
||||
|
||||
func (s *udpTunnel) Endpoint() string {
|
||||
return s.opts.Endpoint
|
||||
}
|
||||
|
||||
func (f *udpTunnel) Entrypoint() string {
|
||||
return fmt.Sprintf("%s.%s", f.endpoint, endpointAddr)
|
||||
}
|
||||
|
||||
func (s *udpTunnel) Options() Options {
|
||||
return s.opts
|
||||
}
|
||||
|
||||
func (s *udpTunnel) Favorite(b bool) {
|
||||
s.favorite.Store(b)
|
||||
}
|
||||
|
||||
func (s *udpTunnel) IsFavorite() bool {
|
||||
return s.favorite.Load()
|
||||
}
|
||||
|
||||
func (s *udpTunnel) init() error {
|
||||
rudp := &config.ServiceConfig{
|
||||
Name: s.opts.Name,
|
||||
Addr: s.opts.Hostname,
|
||||
Handler: &config.HandlerConfig{
|
||||
Type: "rudp",
|
||||
},
|
||||
Listener: &config.ListenerConfig{
|
||||
Type: "rudp",
|
||||
Chain: s.opts.Name,
|
||||
},
|
||||
Forwarder: &config.ForwarderConfig{
|
||||
Nodes: []*config.ForwardNodeConfig{
|
||||
{
|
||||
Name: s.opts.Name,
|
||||
Addr: s.opts.Endpoint,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s.config = &config.Config{
|
||||
Services: []*config.ServiceConfig{rudp},
|
||||
Chains: []*config.ChainConfig{chainConfig(s.opts.ID, s.opts.Name)},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *udpTunnel) Run() error {
|
||||
if s.IsClosed() {
|
||||
return ErrTunnelClosed
|
||||
}
|
||||
|
||||
if err := s.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log := logger.Default().WithFields(map[string]any{
|
||||
"kind": "service",
|
||||
"service": s.opts.Name,
|
||||
})
|
||||
|
||||
{
|
||||
ch, err := chain_parser.ParseChain(s.config.Chains[0], log)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := s.config.Services[0]
|
||||
ln := rudp.NewListener(
|
||||
listener.AddrOption(cfg.Addr),
|
||||
listener.ChainOption(ch),
|
||||
listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "rudp"})),
|
||||
)
|
||||
if err := ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h := remote.NewHandler(
|
||||
handler.LoggerOption(log.WithFields(map[string]any{"kind": "handler", "handler": "rudp"})),
|
||||
)
|
||||
if err := h.Init(mdx.NewMetadata(cfg.Handler.Metadata)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
node := cfg.Forwarder.Nodes[0]
|
||||
if forwarder, ok := h.(handler.Forwarder); ok {
|
||||
forwarder.Forward(hop.NewHop(
|
||||
hop.NodeOption(chain.NewNode(node.Name, node.Addr)),
|
||||
hop.LoggerOption(log.WithFields(map[string]any{"kind": "hop"})),
|
||||
))
|
||||
}
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log))
|
||||
}
|
||||
|
||||
go s.forward.Serve()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *udpTunnel) Close() error {
|
||||
defer func() {
|
||||
select {
|
||||
case <-s.cclose:
|
||||
default:
|
||||
close(s.cclose)
|
||||
}
|
||||
}()
|
||||
|
||||
if s.forward != nil {
|
||||
return s.forward.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *udpTunnel) IsClosed() bool {
|
||||
select {
|
||||
case <-s.cclose:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,56 @@
|
||||
package icons
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"image"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"log"
|
||||
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/widget"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
//go:embed icon.png
|
||||
var iconAppData []byte
|
||||
|
||||
func init() {
|
||||
img, _, err := image.Decode(bytes.NewReader(iconAppData))
|
||||
if err != nil {
|
||||
log.Println("err")
|
||||
return
|
||||
}
|
||||
|
||||
IconApp = &widget.Image{
|
||||
Src: paint.NewImageOp(img),
|
||||
Fit: widget.Unscaled,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
IconApp *widget.Image
|
||||
|
||||
IconHome = mustIcon(icons.ActionHome)
|
||||
IconFavorite = mustIcon(icons.ActionFavorite)
|
||||
IconAdd = mustIcon(icons.ContentAdd)
|
||||
IconSettings = mustIcon(icons.ActionSettings)
|
||||
IconDone = mustIcon(icons.ActionDone)
|
||||
IconTunnelState = mustIcon(icons.ToggleRadioButtonChecked)
|
||||
IconForward = mustIcon(icons.NavigationChevronRight)
|
||||
IconEdit = mustIcon(icons.EditorModeEdit)
|
||||
IconDelete = mustIcon(icons.ActionDelete)
|
||||
IconStart = mustIcon(icons.AVPlayArrow)
|
||||
IconStop = mustIcon(icons.AVStop)
|
||||
IconBack = mustIcon(icons.NavigationArrowBack)
|
||||
IconClose = mustIcon(icons.ContentClear)
|
||||
)
|
||||
|
||||
func mustIcon(data []byte) *widget.Icon {
|
||||
icon, err := widget.NewIcon(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return icon
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"gioui.org/font"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/go-gost/gost-plus/ui/icons"
|
||||
"github.com/go-gost/gost-plus/version"
|
||||
)
|
||||
|
||||
type aboutPage struct {
|
||||
router *Router
|
||||
|
||||
list layout.List
|
||||
}
|
||||
|
||||
func NewAboutPage(r *Router) Page {
|
||||
return &aboutPage{
|
||||
router: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *aboutPage) Init(opts ...PageOption) {
|
||||
p.router.bar.SetActions(nil, nil)
|
||||
p.router.bar.Title = "About"
|
||||
p.router.bar.NavigationIcon = icons.IconBack
|
||||
}
|
||||
|
||||
func (p *aboutPage) Layout(gtx C, th *material.Theme) D {
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
Alignment: layout.Middle,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if icons.IconApp == nil {
|
||||
return D{}
|
||||
}
|
||||
return icons.IconApp.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
label := material.H6(th, "GOST.PLUS")
|
||||
label.Font.Weight = font.Bold
|
||||
return label.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, version.Version).Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
+467
@@ -0,0 +1,467 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/go-gost/gost-plus/tunnel"
|
||||
"github.com/go-gost/gost-plus/ui/icons"
|
||||
"golang.org/x/exp/shiny/materialdesign/colornames"
|
||||
)
|
||||
|
||||
type fileAddPage struct {
|
||||
router *Router
|
||||
|
||||
list layout.List
|
||||
wgDone widget.Clickable
|
||||
|
||||
name component.TextField
|
||||
path component.TextField
|
||||
cbBasicAuth widget.Bool
|
||||
username component.TextField
|
||||
password component.TextField
|
||||
}
|
||||
|
||||
func NewFileAddPage(r *Router) Page {
|
||||
return &fileAddPage{
|
||||
router: r,
|
||||
list: layout.List{
|
||||
Axis: layout.Vertical,
|
||||
Alignment: layout.Middle,
|
||||
},
|
||||
name: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
path: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
username: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
password: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *fileAddPage) Init(opts ...PageOption) {
|
||||
p.name.SetText("")
|
||||
p.path.SetText("")
|
||||
p.cbBasicAuth.Value = false
|
||||
p.username.SetText("")
|
||||
p.password.SetText("")
|
||||
|
||||
p.router.bar.SetActions(
|
||||
[]component.AppBarAction{
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Create",
|
||||
Tag: &p.wgDone,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgDone.Clicked(gtx) {
|
||||
defer p.router.SwitchTo(Route{Path: PageHome})
|
||||
p.createTunnel()
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
p.router.bar.Title = "File"
|
||||
p.router.bar.NavigationIcon = icons.IconClose
|
||||
}
|
||||
|
||||
func (p *fileAddPage) Layout(gtx C, th *material.Theme) D {
|
||||
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return component.Surface(th).Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return p.layout(gtx, th)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *fileAddPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(material.Body1(th, "Expose local files to public network.").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 15}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Service name").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.name.Layout(gtx, th, "Name")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Root directory, default to the current working directory").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
dir := strings.TrimSpace(p.path.Text())
|
||||
if dir == "" {
|
||||
return nil
|
||||
}
|
||||
f, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fs, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fs.IsDir() {
|
||||
return fmt.Errorf("%s is not a directory", dir)
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.path.SetError(err.Error())
|
||||
} else {
|
||||
p.path.ClearError()
|
||||
}
|
||||
|
||||
return p.path.Layout(gtx, th, "Path")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Inset{Top: 10, Bottom: 10}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body1(th, "Use basic auth").Layout),
|
||||
layout.Rigid(material.Switch(th, &p.cbBasicAuth, "use basic auth").Layout),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !p.cbBasicAuth.Value {
|
||||
p.username.SetText("")
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return p.username.Layout(gtx, th, "Username")
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !p.cbBasicAuth.Value {
|
||||
p.password.SetText("")
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return p.password.Layout(gtx, th, "Password")
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *fileAddPage) createTunnel() error {
|
||||
var username, password string
|
||||
if p.cbBasicAuth.Value {
|
||||
username = strings.TrimSpace(p.username.Text())
|
||||
password = strings.TrimSpace(p.password.Text())
|
||||
}
|
||||
tun := tunnel.NewFileTunnel(
|
||||
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
|
||||
tunnel.EndpointOption(strings.TrimSpace(p.path.Text())),
|
||||
tunnel.UsernameOption(username),
|
||||
tunnel.PasswordOption(password),
|
||||
)
|
||||
|
||||
if err := tun.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tunnel.AddTunnel(tun)
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileEditPage struct {
|
||||
router *Router
|
||||
|
||||
id string
|
||||
|
||||
list layout.List
|
||||
wgFavorite widget.Clickable
|
||||
wgState widget.Clickable
|
||||
wgDelete widget.Clickable
|
||||
wgDone widget.Clickable
|
||||
|
||||
wgID widget.Clickable
|
||||
wgEntrypoint widget.Clickable
|
||||
|
||||
name component.TextField
|
||||
path component.TextField
|
||||
cbBasicAuth widget.Bool
|
||||
username component.TextField
|
||||
password component.TextField
|
||||
}
|
||||
|
||||
func NewFileEditPage(r *Router) Page {
|
||||
return &fileEditPage{
|
||||
router: r,
|
||||
list: layout.List{
|
||||
Axis: layout.Vertical,
|
||||
Alignment: layout.Middle,
|
||||
},
|
||||
name: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
path: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
username: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
password: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *fileEditPage) Init(opts ...PageOption) {
|
||||
var options PageOptions
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
p.id = options.ID
|
||||
s := tunnel.GetTunnelID(p.id)
|
||||
if s != nil {
|
||||
sopts := s.Options()
|
||||
p.name.SetText(sopts.Name)
|
||||
p.path.SetText(sopts.Endpoint)
|
||||
if sopts.Username != "" {
|
||||
p.cbBasicAuth.Value = true
|
||||
p.username.SetText(sopts.Username)
|
||||
p.password.SetText(sopts.Password)
|
||||
}
|
||||
}
|
||||
|
||||
actions := []component.AppBarAction{
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Favorite",
|
||||
Tag: &p.wgFavorite,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
s := tunnel.GetTunnelID(p.id)
|
||||
if s == nil {
|
||||
return D{}
|
||||
}
|
||||
|
||||
if p.wgFavorite.Clicked(gtx) {
|
||||
s.Favorite(!s.IsFavorite())
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
|
||||
btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
|
||||
if s.IsFavorite() {
|
||||
btn.Color = color.NRGBA(colornames.Red500)
|
||||
} else {
|
||||
btn.Color = fg
|
||||
}
|
||||
return btn.Layout(gtx)
|
||||
},
|
||||
},
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Start/Stop",
|
||||
Tag: &p.wgState,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
s := tunnel.GetTunnelID(p.id)
|
||||
if s == nil {
|
||||
return D{}
|
||||
}
|
||||
|
||||
if p.wgState.Clicked(gtx) {
|
||||
if s.IsClosed() {
|
||||
s = p.createTunnel()
|
||||
} else {
|
||||
s.Close()
|
||||
}
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
|
||||
if s != nil && !s.IsClosed() {
|
||||
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStop).Layout(gtx)
|
||||
} else {
|
||||
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStart).Layout(gtx)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Delete",
|
||||
Tag: &p.wgDelete,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgDelete.Clicked(gtx) {
|
||||
tunnel.DeleteTunnel(p.id)
|
||||
tunnel.SaveTunnel()
|
||||
p.router.SwitchTo(Route{Path: PageHome})
|
||||
}
|
||||
return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx)
|
||||
},
|
||||
},
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Save",
|
||||
Tag: &p.wgDone,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgDone.Clicked(gtx) {
|
||||
defer p.router.SwitchTo(Route{Path: PageHome})
|
||||
|
||||
if s := tunnel.GetTunnelID(p.id); s != nil {
|
||||
s.Close()
|
||||
p.createTunnel()
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
}
|
||||
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
|
||||
},
|
||||
},
|
||||
}
|
||||
p.router.bar.SetActions(actions, nil)
|
||||
p.router.bar.Title = "File"
|
||||
p.router.bar.NavigationIcon = icons.IconClose
|
||||
}
|
||||
|
||||
func (p *fileEditPage) createTunnel() tunnel.Tunnel {
|
||||
var username, password string
|
||||
if p.cbBasicAuth.Value {
|
||||
username = strings.TrimSpace(p.username.Text())
|
||||
password = strings.TrimSpace(p.password.Text())
|
||||
}
|
||||
s := tunnel.NewFileTunnel(
|
||||
tunnel.IDOption(p.id),
|
||||
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
|
||||
tunnel.EndpointOption(strings.TrimSpace(p.path.Text())),
|
||||
tunnel.UsernameOption(username),
|
||||
tunnel.PasswordOption(password),
|
||||
)
|
||||
|
||||
if err := s.Run(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
tunnel.SetTunnel(s)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *fileEditPage) Layout(gtx C, th *material.Theme) D {
|
||||
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return component.Surface(th).Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return p.layout(gtx, th)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *fileEditPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layoutHeader(gtx, th, tunnel.GetTunnelID(p.id), &p.wgID, &p.wgEntrypoint)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
div := component.Divider(th)
|
||||
return div.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Tunnel name").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.name.Layout(gtx, th, "Name")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Root directory, default to the current working directory").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
dir := strings.TrimSpace(p.path.Text())
|
||||
if dir == "" {
|
||||
return nil
|
||||
}
|
||||
f, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fs, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fs.IsDir() {
|
||||
return fmt.Errorf("%s is not a directory", dir)
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.path.SetError(err.Error())
|
||||
} else {
|
||||
p.path.ClearError()
|
||||
}
|
||||
|
||||
return p.path.Layout(gtx, th, "Path")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body1(th, "Use basic auth").Layout),
|
||||
layout.Rigid(material.Switch(th, &p.cbBasicAuth, "use basic auth").Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !p.cbBasicAuth.Value {
|
||||
p.username.SetText("")
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return p.username.Layout(gtx, th, "Username")
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !p.cbBasicAuth.Value {
|
||||
p.password.SetText("")
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return p.password.Layout(gtx, th, "Password")
|
||||
}),
|
||||
)
|
||||
}
|
||||
+534
@@ -0,0 +1,534 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/go-gost/gost-plus/tunnel"
|
||||
"github.com/go-gost/gost-plus/ui/icons"
|
||||
"golang.org/x/exp/shiny/materialdesign/colornames"
|
||||
)
|
||||
|
||||
type httpAddPage struct {
|
||||
router *Router
|
||||
|
||||
list layout.List
|
||||
wgDone widget.Clickable
|
||||
|
||||
name component.TextField
|
||||
addr component.TextField
|
||||
|
||||
bHost widget.Bool
|
||||
hostname component.TextField
|
||||
|
||||
bBasicAuth widget.Bool
|
||||
username component.TextField
|
||||
password component.TextField
|
||||
|
||||
bTLS widget.Bool
|
||||
}
|
||||
|
||||
func NewHTTPAddPage(r *Router) Page {
|
||||
return &httpAddPage{
|
||||
router: r,
|
||||
list: layout.List{
|
||||
Axis: layout.Vertical,
|
||||
Alignment: layout.Middle,
|
||||
},
|
||||
name: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
addr: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
hostname: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
username: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
password: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *httpAddPage) Init(opts ...PageOption) {
|
||||
p.name.SetText("")
|
||||
p.addr.SetText("")
|
||||
p.hostname.SetText("")
|
||||
p.bBasicAuth.Value = false
|
||||
p.username.SetText("")
|
||||
p.password.SetText("")
|
||||
p.bTLS.Value = false
|
||||
|
||||
p.router.bar.SetActions(
|
||||
[]component.AppBarAction{
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Create",
|
||||
Tag: &p.wgDone,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgDone.Clicked(gtx) {
|
||||
defer p.router.SwitchTo(Route{Path: PageHome})
|
||||
p.createTunnel()
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
p.router.bar.Title = "HTTP"
|
||||
p.router.bar.NavigationIcon = icons.IconClose
|
||||
}
|
||||
|
||||
func (p *httpAddPage) Layout(gtx C, th *material.Theme) D {
|
||||
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return component.Surface(th).Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return p.layout(gtx, th)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *httpAddPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(material.Body1(th, "Expose local http tunnel to public network.").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Tunnel name").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.name.Layout(gtx, th, "Name")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Endpoint address").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
addr := strings.TrimSpace(p.addr.Text())
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.addr.SetError(err.Error())
|
||||
} else {
|
||||
p.addr.ClearError()
|
||||
}
|
||||
|
||||
return p.addr.Layout(gtx, th, "Address")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body1(th, "Use custom hostname (rewrite HTTP Host header)").Layout),
|
||||
layout.Rigid(material.Switch(th, &p.bHost, "custom hostname").Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !p.bHost.Value {
|
||||
p.hostname.SetText("")
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return p.hostname.Layout(gtx, th, "Hostname")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body1(th, "Use basic auth").Layout),
|
||||
layout.Rigid(material.Switch(th, &p.bBasicAuth, "use basic auth").Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !p.bBasicAuth.Value {
|
||||
p.username.SetText("")
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return p.username.Layout(gtx, th, "Username")
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !p.bBasicAuth.Value {
|
||||
p.password.SetText("")
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return p.password.Layout(gtx, th, "Password")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body1(th, "Enable TLS").Layout),
|
||||
layout.Rigid(material.Switch(th, &p.bTLS, "enable TLS").Layout),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *httpAddPage) createTunnel() error {
|
||||
var username, password string
|
||||
if p.bBasicAuth.Value {
|
||||
username = strings.TrimSpace(p.username.Text())
|
||||
password = strings.TrimSpace(p.password.Text())
|
||||
}
|
||||
var hostname string
|
||||
if p.bHost.Value {
|
||||
hostname = strings.TrimSpace(p.hostname.Text())
|
||||
}
|
||||
srv := tunnel.NewHTTPTunnel(
|
||||
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
|
||||
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
|
||||
tunnel.UsernameOption(username),
|
||||
tunnel.PasswordOption(password),
|
||||
tunnel.HostnameOption(hostname),
|
||||
tunnel.EnableTLSOption(p.bTLS.Value),
|
||||
)
|
||||
|
||||
if err := srv.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tunnel.AddTunnel(srv)
|
||||
return nil
|
||||
}
|
||||
|
||||
type httpEditPage struct {
|
||||
router *Router
|
||||
|
||||
id string
|
||||
|
||||
list layout.List
|
||||
wgFavorite widget.Clickable
|
||||
wgState widget.Clickable
|
||||
wgDelete widget.Clickable
|
||||
wgDone widget.Clickable
|
||||
|
||||
name component.TextField
|
||||
addr component.TextField
|
||||
|
||||
bHost widget.Bool
|
||||
hostname component.TextField
|
||||
|
||||
cbBasicAuth widget.Bool
|
||||
username component.TextField
|
||||
password component.TextField
|
||||
|
||||
bTLS widget.Bool
|
||||
|
||||
wgID widget.Clickable
|
||||
wgEntrypoint widget.Clickable
|
||||
}
|
||||
|
||||
func NewHTTPEditPage(r *Router) Page {
|
||||
return &httpEditPage{
|
||||
router: r,
|
||||
list: layout.List{
|
||||
Axis: layout.Vertical,
|
||||
Alignment: layout.Middle,
|
||||
},
|
||||
name: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
addr: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
hostname: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
username: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
password: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *httpEditPage) Init(opts ...PageOption) {
|
||||
var options PageOptions
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
p.id = options.ID
|
||||
s := tunnel.GetTunnelID(p.id)
|
||||
if s != nil {
|
||||
sopts := s.Options()
|
||||
p.name.SetText(sopts.Name)
|
||||
p.addr.SetText(sopts.Endpoint)
|
||||
if sopts.Hostname != "" {
|
||||
p.bHost.Value = true
|
||||
p.hostname.SetText(sopts.Hostname)
|
||||
}
|
||||
if sopts.Username != "" {
|
||||
p.cbBasicAuth.Value = true
|
||||
p.username.SetText(sopts.Username)
|
||||
p.password.SetText(sopts.Password)
|
||||
}
|
||||
p.bTLS.Value = sopts.EnableTLS
|
||||
}
|
||||
|
||||
actions := []component.AppBarAction{
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Favorite",
|
||||
Tag: &p.wgFavorite,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
s := tunnel.GetTunnelID(p.id)
|
||||
if s == nil {
|
||||
return D{}
|
||||
}
|
||||
|
||||
if p.wgFavorite.Clicked(gtx) {
|
||||
s.Favorite(!s.IsFavorite())
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
|
||||
btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
|
||||
if s.IsFavorite() {
|
||||
btn.Color = color.NRGBA(colornames.Red500)
|
||||
} else {
|
||||
btn.Color = fg
|
||||
}
|
||||
return btn.Layout(gtx)
|
||||
},
|
||||
},
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Start/Stop",
|
||||
Tag: &p.wgState,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
s := tunnel.GetTunnelID(p.id)
|
||||
if p.wgState.Clicked(gtx) && s != nil {
|
||||
if s.IsClosed() {
|
||||
s = p.createTunnel()
|
||||
} else {
|
||||
s.Close()
|
||||
}
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
|
||||
if s != nil && !s.IsClosed() {
|
||||
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStop).Layout(gtx)
|
||||
} else {
|
||||
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStart).Layout(gtx)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Delete",
|
||||
Tag: &p.wgDelete,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgDelete.Clicked(gtx) {
|
||||
tunnel.DeleteTunnel(p.id)
|
||||
tunnel.SaveTunnel()
|
||||
p.router.SwitchTo(Route{Path: PageHome})
|
||||
}
|
||||
return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx)
|
||||
},
|
||||
},
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Save",
|
||||
Tag: &p.wgDone,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgDone.Clicked(gtx) {
|
||||
defer p.router.SwitchTo(Route{Path: PageHome})
|
||||
|
||||
if s := tunnel.GetTunnelID(p.id); s != nil {
|
||||
s.Close()
|
||||
p.createTunnel()
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
}
|
||||
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
|
||||
},
|
||||
},
|
||||
}
|
||||
p.router.bar.SetActions(actions, nil)
|
||||
p.router.bar.Title = "HTTP"
|
||||
p.router.bar.NavigationIcon = icons.IconClose
|
||||
}
|
||||
|
||||
func (p *httpEditPage) createTunnel() tunnel.Tunnel {
|
||||
var username, password string
|
||||
if p.cbBasicAuth.Value {
|
||||
username = strings.TrimSpace(p.username.Text())
|
||||
password = strings.TrimSpace(p.password.Text())
|
||||
}
|
||||
var hostname string
|
||||
if p.bHost.Value {
|
||||
hostname = strings.TrimSpace(p.hostname.Text())
|
||||
}
|
||||
s := tunnel.NewHTTPTunnel(
|
||||
tunnel.IDOption(p.id),
|
||||
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
|
||||
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
|
||||
tunnel.UsernameOption(username),
|
||||
tunnel.PasswordOption(password),
|
||||
tunnel.HostnameOption(hostname),
|
||||
tunnel.EnableTLSOption(p.bTLS.Value),
|
||||
)
|
||||
|
||||
if err := s.Run(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
tunnel.SetTunnel(s)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *httpEditPage) Layout(gtx C, th *material.Theme) D {
|
||||
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return component.Surface(th).Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return p.layout(gtx, th)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *httpEditPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layoutHeader(gtx, th, tunnel.GetTunnelID(p.id), &p.wgID, &p.wgEntrypoint)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Tunnel name").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.name.Layout(gtx, th, "Name")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Endpoint address").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
addr := strings.TrimSpace(p.addr.Text())
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.addr.SetError(err.Error())
|
||||
} else {
|
||||
p.addr.ClearError()
|
||||
}
|
||||
|
||||
return p.addr.Layout(gtx, th, "Address")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Inset{Top: 10, Bottom: 10}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body1(th, "Use custom hostname (rewrite HTTP Host header)").Layout),
|
||||
layout.Rigid(material.Switch(th, &p.bHost, "custom hostname").Layout),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !p.bHost.Value {
|
||||
p.hostname.SetText("")
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return p.hostname.Layout(gtx, th, "Hostname")
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Inset{Top: 10, Bottom: 10}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body1(th, "Use basic auth").Layout),
|
||||
layout.Rigid(material.Switch(th, &p.cbBasicAuth, "use basic auth").Layout),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !p.cbBasicAuth.Value {
|
||||
p.username.SetText("")
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return p.username.Layout(gtx, th, "Username")
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !p.cbBasicAuth.Value {
|
||||
p.password.SetText("")
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return p.password.Layout(gtx, th, "Password")
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Inset{Top: 10, Bottom: 10}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body1(th, "Enable TLS").Layout),
|
||||
layout.Rigid(material.Switch(th, &p.bTLS, "enable TLS").Layout),
|
||||
)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/go-gost/gost-plus/ui/icons"
|
||||
"golang.org/x/exp/shiny/materialdesign/colornames"
|
||||
)
|
||||
|
||||
type menuPage struct {
|
||||
router *Router
|
||||
|
||||
list widget.List
|
||||
|
||||
wgFile widget.Clickable
|
||||
wgHTTP widget.Clickable
|
||||
wgTCP widget.Clickable
|
||||
wgUDP widget.Clickable
|
||||
}
|
||||
|
||||
func NewMenuPage(r *Router) Page {
|
||||
return &menuPage{
|
||||
router: r,
|
||||
list: widget.List{
|
||||
List: layout.List{
|
||||
Axis: layout.Vertical,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *menuPage) Init(opts ...PageOption) {
|
||||
p.router.bar.SetActions(nil, nil)
|
||||
p.router.bar.Title = "Add"
|
||||
p.router.bar.NavigationIcon = icons.IconBack
|
||||
}
|
||||
|
||||
func (p *menuPage) Layout(gtx C, th *material.Theme) D {
|
||||
if clicked := func() bool {
|
||||
if p.wgFile.Clicked(gtx) {
|
||||
p.router.SwitchTo(Route{Path: PageNewFile})
|
||||
return true
|
||||
}
|
||||
if p.wgHTTP.Clicked(gtx) {
|
||||
p.router.SwitchTo(Route{Path: PageNewHTTP})
|
||||
return true
|
||||
}
|
||||
if p.wgTCP.Clicked(gtx) {
|
||||
p.router.SwitchTo(Route{Path: PageNewTCP})
|
||||
return true
|
||||
}
|
||||
if p.wgUDP.Clicked(gtx) {
|
||||
p.router.SwitchTo(Route{Path: PageNewUDP})
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}(); clicked {
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
return p.list.List.Layout(gtx, 1, func(gtx C, _ int) D {
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
label := material.H6(th, "Services")
|
||||
label.Font.Weight = font.Bold
|
||||
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, label.Layout)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, func(gtx C) D {
|
||||
return component.Surface(th).Layout(gtx, func(gtx C) D {
|
||||
return p.wgFile.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return p.layoutCard(gtx, th, "File", "Expose local files to public network")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, func(gtx C) D {
|
||||
return component.Surface(th).Layout(gtx, func(gtx C) D {
|
||||
return p.wgHTTP.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return p.layoutCard(gtx, th, "HTTP", "Expose local HTTP service to public network")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, func(gtx C) D {
|
||||
return component.Surface(th).Layout(gtx, func(gtx C) D {
|
||||
return p.wgTCP.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return p.layoutCard(gtx, th, "TCP", "Expose local TCP service to public network")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, func(gtx C) D {
|
||||
return component.Surface(th).Layout(gtx, func(gtx C) D {
|
||||
return p.wgUDP.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return p.layoutCard(gtx, th, "UDP", "Expose local UDP service to public network")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *menuPage) layoutCard(gtx C, th *material.Theme, name, desc string) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Horizontal,
|
||||
Spacing: layout.SpaceBetween,
|
||||
Alignment: layout.Middle,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
title := material.Body1(th, name)
|
||||
title.Font.Weight = font.Bold
|
||||
return title.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
title := material.Body1(th, desc)
|
||||
return title.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return icons.IconForward.Layout(gtx, color.NRGBA(colornames.Grey500))
|
||||
}),
|
||||
)
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/go-gost/gost-plus/tunnel"
|
||||
"golang.org/x/exp/shiny/materialdesign/colornames"
|
||||
)
|
||||
|
||||
const (
|
||||
PageHome = "/"
|
||||
PageMenu = "/menu"
|
||||
PageNewFile = "/tunnel/file/create"
|
||||
PageNewHTTP = "/tunnel/http/create"
|
||||
PageNewTCP = "/tunnel/tcp/create"
|
||||
PageNewUDP = "/tunnel/udp/create"
|
||||
PageEditFile = "/tunnel/file/edit"
|
||||
PageEditHTTP = "/tunnel/http/edit"
|
||||
PageEditTCP = "/tunnel/tcp/edit"
|
||||
PageEditUDP = "/tunnel/udp/edit"
|
||||
|
||||
PageAbout = "/about"
|
||||
)
|
||||
|
||||
type OverflowAction string
|
||||
|
||||
const (
|
||||
OverflowActionAbout OverflowAction = "about"
|
||||
)
|
||||
|
||||
type PageOptions struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
type PageOption func(*PageOptions)
|
||||
|
||||
func IDPageOption(id string) PageOption {
|
||||
return func(opts *PageOptions) {
|
||||
opts.ID = id
|
||||
}
|
||||
}
|
||||
|
||||
type Page interface {
|
||||
Init(opts ...PageOption)
|
||||
Layout(gtx layout.Context, th *material.Theme) layout.Dimensions
|
||||
}
|
||||
|
||||
func layoutHeader(gtx C, th *material.Theme, tun tunnel.Tunnel, wgID, wgEntrypoint *widget.Clickable) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if tun == nil {
|
||||
return D{}
|
||||
}
|
||||
|
||||
copied := false
|
||||
if wgID.Clicked(gtx) {
|
||||
copied = true
|
||||
clipboard.WriteOp{
|
||||
Text: tun.ID(),
|
||||
}.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
return wgID.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
label := material.Body1(th, tun.ID())
|
||||
label.Font.Weight = font.Bold
|
||||
return label.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
label := material.Body1(th, "Copy")
|
||||
label.Color = color.NRGBA(colornames.Blue500)
|
||||
if copied {
|
||||
label = material.Body1(th, "Copied")
|
||||
label.Color = color.NRGBA(colornames.Green500)
|
||||
}
|
||||
return label.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if tun == nil {
|
||||
return D{}
|
||||
}
|
||||
|
||||
copied := false
|
||||
if wgEntrypoint.Clicked(gtx) {
|
||||
copied = true
|
||||
clipboard.WriteOp{
|
||||
Text: tun.Entrypoint(),
|
||||
}.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
return wgEntrypoint.Layout(gtx, func(gtx C) D {
|
||||
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
label := material.Body1(th, tun.Entrypoint())
|
||||
return label.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
label := material.Body1(th, "Copy")
|
||||
label.Color = color.NRGBA(colornames.Blue500)
|
||||
if copied {
|
||||
label = material.Body1(th, "Copied")
|
||||
label.Color = color.NRGBA(colornames.Green500)
|
||||
}
|
||||
return label.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
)
|
||||
|
||||
type C = layout.Context
|
||||
type D = layout.Dimensions
|
||||
|
||||
type Route struct {
|
||||
Path string
|
||||
ID string
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
modal *component.ModalLayer
|
||||
bar *component.AppBar
|
||||
|
||||
pages map[string]Page
|
||||
current Route
|
||||
}
|
||||
|
||||
func NewRouter() *Router {
|
||||
modal := component.NewModal()
|
||||
bar := component.NewAppBar(modal)
|
||||
|
||||
r := &Router{
|
||||
modal: modal,
|
||||
bar: bar,
|
||||
pages: make(map[string]Page),
|
||||
}
|
||||
|
||||
r.Register(PageHome, NewTunnelPage(r))
|
||||
r.Register(PageMenu, NewMenuPage(r))
|
||||
r.Register(PageNewFile, NewFileAddPage(r))
|
||||
r.Register(PageEditFile, NewFileEditPage(r))
|
||||
r.Register(PageNewHTTP, NewHTTPAddPage(r))
|
||||
r.Register(PageEditHTTP, NewHTTPEditPage(r))
|
||||
r.Register(PageNewTCP, NewTCPAddPage(r))
|
||||
r.Register(PageEditTCP, NewTCPEditPage(r))
|
||||
r.Register(PageNewUDP, NewUDPAddPage(r))
|
||||
r.Register(PageEditUDP, NewUDPEditPage(r))
|
||||
r.Register(PageAbout, NewAboutPage(r))
|
||||
|
||||
r.SwitchTo(Route{Path: PageHome})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Router) Register(path string, page Page) {
|
||||
if page != nil {
|
||||
r.pages[path] = page
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) SwitchTo(route Route) {
|
||||
p := r.pages[route.Path]
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.Init(IDPageOption(route.ID))
|
||||
|
||||
r.current = route
|
||||
}
|
||||
|
||||
func (r *Router) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
|
||||
for _, event := range r.bar.Events(gtx) {
|
||||
switch event := event.(type) {
|
||||
case component.AppBarNavigationClicked:
|
||||
// log.Printf("navigation clicked: %+v", event)
|
||||
r.SwitchTo(Route{Path: PageHome})
|
||||
case component.AppBarContextMenuDismissed:
|
||||
// log.Printf("Context menu dismissed: %+v", event)
|
||||
r.SwitchTo(Route{Path: PageHome})
|
||||
case component.AppBarOverflowActionClicked:
|
||||
if event.Tag == OverflowActionAbout {
|
||||
r.SwitchTo(Route{Path: PageAbout})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defer r.modal.Layout(gtx, th)
|
||||
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return r.bar.Layout(gtx, th, "Menu", "Actions")
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return r.pages[r.current.Path].Layout(gtx, th)
|
||||
}),
|
||||
)
|
||||
}
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/go-gost/gost-plus/tunnel"
|
||||
"github.com/go-gost/gost-plus/ui/icons"
|
||||
"golang.org/x/exp/shiny/materialdesign/colornames"
|
||||
)
|
||||
|
||||
type tcpAddPage struct {
|
||||
router *Router
|
||||
|
||||
list layout.List
|
||||
wgDone widget.Clickable
|
||||
|
||||
name component.TextField
|
||||
addr component.TextField
|
||||
}
|
||||
|
||||
func NewTCPAddPage(r *Router) Page {
|
||||
return &tcpAddPage{
|
||||
router: r,
|
||||
list: layout.List{
|
||||
Axis: layout.Vertical,
|
||||
Alignment: layout.Middle,
|
||||
},
|
||||
name: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
addr: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *tcpAddPage) Init(opts ...PageOption) {
|
||||
p.name.SetText("")
|
||||
p.addr.SetText("")
|
||||
|
||||
p.router.bar.SetActions(
|
||||
[]component.AppBarAction{
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Create",
|
||||
Tag: &p.wgDone,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgDone.Clicked(gtx) {
|
||||
defer p.router.SwitchTo(Route{Path: PageHome})
|
||||
p.createTunnel()
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
p.router.bar.Title = "TCP"
|
||||
p.router.bar.NavigationIcon = icons.IconClose
|
||||
}
|
||||
|
||||
func (p *tcpAddPage) Layout(gtx C, th *material.Theme) D {
|
||||
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return component.Surface(th).Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return p.layout(gtx, th)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *tcpAddPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(material.Body1(th, "Expose local TCP tunnel to public network.").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Tunnel name").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.name.Layout(gtx, th, "Name")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Endpoint address").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
addr := strings.TrimSpace(p.addr.Text())
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.addr.SetError(err.Error())
|
||||
} else {
|
||||
p.addr.ClearError()
|
||||
}
|
||||
|
||||
return p.addr.Layout(gtx, th, "Address")
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *tcpAddPage) createTunnel() error {
|
||||
srv := tunnel.NewTCPTunnel(
|
||||
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
|
||||
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
|
||||
)
|
||||
|
||||
if err := srv.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tunnel.AddTunnel(srv)
|
||||
return nil
|
||||
}
|
||||
|
||||
type tcpEditPage struct {
|
||||
router *Router
|
||||
|
||||
id string
|
||||
|
||||
list layout.List
|
||||
wgFavorite widget.Clickable
|
||||
wgState widget.Clickable
|
||||
wgDelete widget.Clickable
|
||||
wgDone widget.Clickable
|
||||
|
||||
name component.TextField
|
||||
addr component.TextField
|
||||
|
||||
wgID widget.Clickable
|
||||
wgEntrypoint widget.Clickable
|
||||
}
|
||||
|
||||
func NewTCPEditPage(r *Router) Page {
|
||||
return &tcpEditPage{
|
||||
router: r,
|
||||
list: layout.List{
|
||||
Axis: layout.Vertical,
|
||||
Alignment: layout.Middle,
|
||||
},
|
||||
name: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
addr: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *tcpEditPage) Init(opts ...PageOption) {
|
||||
var options PageOptions
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
p.id = options.ID
|
||||
s := tunnel.GetTunnelID(p.id)
|
||||
if s != nil {
|
||||
sopts := s.Options()
|
||||
p.name.SetText(sopts.Name)
|
||||
p.addr.SetText(sopts.Endpoint)
|
||||
}
|
||||
|
||||
actions := []component.AppBarAction{
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Favorite",
|
||||
Tag: &p.wgFavorite,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
s := tunnel.GetTunnelID(p.id)
|
||||
if s == nil {
|
||||
return D{}
|
||||
}
|
||||
|
||||
if p.wgFavorite.Clicked(gtx) {
|
||||
s.Favorite(!s.IsFavorite())
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
|
||||
btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
|
||||
if s.IsFavorite() {
|
||||
btn.Color = color.NRGBA(colornames.Red500)
|
||||
} else {
|
||||
btn.Color = fg
|
||||
}
|
||||
return btn.Layout(gtx)
|
||||
},
|
||||
},
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Start/Stop",
|
||||
Tag: &p.wgState,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
s := tunnel.GetTunnelID(p.id)
|
||||
if p.wgState.Clicked(gtx) && s != nil {
|
||||
if s.IsClosed() {
|
||||
s = p.createTunnel()
|
||||
} else {
|
||||
s.Close()
|
||||
}
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
|
||||
if s != nil && !s.IsClosed() {
|
||||
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStop).Layout(gtx)
|
||||
} else {
|
||||
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStart).Layout(gtx)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Delete",
|
||||
Tag: &p.wgDelete,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgDelete.Clicked(gtx) {
|
||||
tunnel.DeleteTunnel(p.id)
|
||||
tunnel.SaveTunnel()
|
||||
p.router.SwitchTo(Route{Path: PageHome})
|
||||
}
|
||||
return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx)
|
||||
},
|
||||
},
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Save",
|
||||
Tag: &p.wgDone,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgDone.Clicked(gtx) {
|
||||
defer p.router.SwitchTo(Route{Path: PageHome})
|
||||
|
||||
if s := tunnel.GetTunnelID(p.id); s != nil {
|
||||
s.Close()
|
||||
p.createTunnel()
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
}
|
||||
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
|
||||
},
|
||||
},
|
||||
}
|
||||
p.router.bar.SetActions(actions, nil)
|
||||
p.router.bar.Title = "TCP"
|
||||
p.router.bar.NavigationIcon = icons.IconClose
|
||||
}
|
||||
|
||||
func (p *tcpEditPage) createTunnel() tunnel.Tunnel {
|
||||
s := tunnel.NewTCPTunnel(
|
||||
tunnel.IDOption(p.id),
|
||||
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
|
||||
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
|
||||
)
|
||||
|
||||
if err := s.Run(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
tunnel.SetTunnel(s)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *tcpEditPage) Layout(gtx C, th *material.Theme) D {
|
||||
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return component.Surface(th).Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return p.layout(gtx, th)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *tcpEditPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layoutHeader(gtx, th, tunnel.GetTunnelID(p.id), &p.wgID, &p.wgEntrypoint)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Tunnel name").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.name.Layout(gtx, th, "Name")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Endpoint address").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
addr := strings.TrimSpace(p.addr.Text())
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.addr.SetError(err.Error())
|
||||
} else {
|
||||
p.addr.ClearError()
|
||||
}
|
||||
|
||||
return p.addr.Layout(gtx, th, "Address")
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/go-gost/gost-plus/tunnel"
|
||||
"github.com/go-gost/gost-plus/ui/icons"
|
||||
"golang.org/x/exp/shiny/materialdesign/colornames"
|
||||
)
|
||||
|
||||
type tunnelState struct {
|
||||
editor widget.Clickable
|
||||
}
|
||||
|
||||
type tunnelPage struct {
|
||||
router *Router
|
||||
|
||||
list layout.List
|
||||
|
||||
wgFavorite widget.Clickable
|
||||
wgAdd widget.Clickable
|
||||
|
||||
tunnels map[int]*tunnelState
|
||||
favorite atomic.Bool
|
||||
}
|
||||
|
||||
func NewTunnelPage(r *Router) Page {
|
||||
return &tunnelPage{
|
||||
router: r,
|
||||
list: layout.List{
|
||||
Axis: layout.Vertical,
|
||||
Alignment: layout.Middle,
|
||||
},
|
||||
tunnels: make(map[int]*tunnelState),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *tunnelPage) Init(opts ...PageOption) {
|
||||
p.router.bar.SetActions(
|
||||
[]component.AppBarAction{
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Favorite",
|
||||
Tag: &p.wgFavorite,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgFavorite.Clicked(gtx) {
|
||||
p.favorite.Store(!p.favorite.Load())
|
||||
}
|
||||
|
||||
btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
|
||||
if p.favorite.Load() {
|
||||
btn.Color = color.NRGBA(colornames.Red500)
|
||||
} else {
|
||||
btn.Color = fg
|
||||
}
|
||||
return btn.Layout(gtx)
|
||||
},
|
||||
},
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Add",
|
||||
Tag: &p.wgAdd,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgAdd.Clicked(gtx) {
|
||||
p.router.SwitchTo(Route{Path: PageMenu})
|
||||
}
|
||||
return component.SimpleIconButton(bg, fg, &p.wgAdd, icons.IconAdd).Layout(gtx)
|
||||
},
|
||||
},
|
||||
},
|
||||
[]component.OverflowAction{
|
||||
{
|
||||
Name: "About",
|
||||
Tag: OverflowActionAbout,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
p.router.bar.Title = "Tunnels"
|
||||
p.router.bar.NavigationIcon = icons.IconHome
|
||||
}
|
||||
|
||||
func (p *tunnelPage) Layout(gtx C, th *material.Theme) D {
|
||||
favorite := p.favorite.Load()
|
||||
// gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
return p.list.Layout(gtx, tunnel.TunnelCount(), func(gtx C, index int) D {
|
||||
s := tunnel.GetTunnel(index)
|
||||
if s == nil {
|
||||
delete(p.tunnels, index)
|
||||
return D{}
|
||||
}
|
||||
|
||||
if p.tunnels[index] == nil {
|
||||
p.tunnels[index] = &tunnelState{}
|
||||
}
|
||||
|
||||
if favorite && !s.IsFavorite() {
|
||||
return D{}
|
||||
}
|
||||
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
surface := component.Surface(th)
|
||||
return surface.Layout(gtx, func(gtx C) D {
|
||||
state := p.tunnels[index]
|
||||
if state.editor.Clicked(gtx) {
|
||||
switch s.Type() {
|
||||
case tunnel.FileTunnel:
|
||||
p.router.SwitchTo(Route{Path: PageEditFile, ID: s.ID()})
|
||||
case tunnel.HTTPTunnel:
|
||||
p.router.SwitchTo(Route{Path: PageEditHTTP, ID: s.ID()})
|
||||
case tunnel.TCPTunnel:
|
||||
p.router.SwitchTo(Route{Path: PageEditTCP, ID: s.ID()})
|
||||
case tunnel.UDPTunnel:
|
||||
p.router.SwitchTo(Route{Path: PageEditUDP, ID: s.ID()})
|
||||
}
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
return state.editor.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return p.layoutTunnel(gtx, th, s)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *tunnelPage) layoutTunnel(gtx C, th *material.Theme, s tunnel.Tunnel) D {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
label := material.Body1(th, s.ID())
|
||||
label.Font.Weight = font.Bold
|
||||
return label.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 5}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("Type: %s", strings.ToUpper(s.Type()))).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 5}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("Name: %s", s.Name())).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 5}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("Endpoint: %s", s.Endpoint())).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 5}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("Entrypoint: %s", s.Entrypoint())).Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
c := colornames.Green500
|
||||
if s.IsClosed() {
|
||||
c = colornames.Grey500
|
||||
}
|
||||
return icons.IconTunnelState.Layout(gtx, color.NRGBA(c))
|
||||
}),
|
||||
)
|
||||
}
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/go-gost/gost-plus/tunnel"
|
||||
"github.com/go-gost/gost-plus/ui/icons"
|
||||
"golang.org/x/exp/shiny/materialdesign/colornames"
|
||||
)
|
||||
|
||||
type udpAddPage struct {
|
||||
router *Router
|
||||
|
||||
list layout.List
|
||||
wgDone widget.Clickable
|
||||
|
||||
name component.TextField
|
||||
addr component.TextField
|
||||
}
|
||||
|
||||
func NewUDPAddPage(r *Router) Page {
|
||||
return &udpAddPage{
|
||||
router: r,
|
||||
list: layout.List{
|
||||
Axis: layout.Vertical,
|
||||
Alignment: layout.Middle,
|
||||
},
|
||||
name: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
addr: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *udpAddPage) Init(opts ...PageOption) {
|
||||
p.name.SetText("")
|
||||
p.addr.SetText("")
|
||||
|
||||
p.router.bar.SetActions(
|
||||
[]component.AppBarAction{
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Create",
|
||||
Tag: &p.wgDone,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgDone.Clicked(gtx) {
|
||||
defer p.router.SwitchTo(Route{Path: PageHome})
|
||||
p.createTunnel()
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
p.router.bar.Title = "UDP"
|
||||
p.router.bar.NavigationIcon = icons.IconClose
|
||||
}
|
||||
|
||||
func (p *udpAddPage) Layout(gtx C, th *material.Theme) D {
|
||||
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return component.Surface(th).Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return p.layout(gtx, th)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *udpAddPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(material.Body1(th, "Expose local UDP tunnel to public network.").Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Tunnel name").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.name.Layout(gtx, th, "Name")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Endpoint address").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
addr := strings.TrimSpace(p.addr.Text())
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.addr.SetError(err.Error())
|
||||
} else {
|
||||
p.addr.ClearError()
|
||||
}
|
||||
|
||||
return p.addr.Layout(gtx, th, "Address")
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *udpAddPage) createTunnel() error {
|
||||
srv := tunnel.NewUDPTunnel(
|
||||
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
|
||||
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
|
||||
)
|
||||
|
||||
if err := srv.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tunnel.AddTunnel(srv)
|
||||
return nil
|
||||
}
|
||||
|
||||
type udpEditPage struct {
|
||||
router *Router
|
||||
|
||||
id string
|
||||
|
||||
list layout.List
|
||||
wgFavorite widget.Clickable
|
||||
wgState widget.Clickable
|
||||
wgDelete widget.Clickable
|
||||
wgDone widget.Clickable
|
||||
|
||||
name component.TextField
|
||||
addr component.TextField
|
||||
|
||||
wgID widget.Clickable
|
||||
wgEntrypoint widget.Clickable
|
||||
}
|
||||
|
||||
func NewUDPEditPage(r *Router) Page {
|
||||
return &udpEditPage{
|
||||
router: r,
|
||||
list: layout.List{
|
||||
Axis: layout.Vertical,
|
||||
Alignment: layout.Middle,
|
||||
},
|
||||
name: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
addr: component.TextField{
|
||||
Editor: widget.Editor{
|
||||
SingleLine: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *udpEditPage) Init(opts ...PageOption) {
|
||||
var options PageOptions
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
p.id = options.ID
|
||||
s := tunnel.GetTunnelID(p.id)
|
||||
if s != nil {
|
||||
sopts := s.Options()
|
||||
p.name.SetText(sopts.Name)
|
||||
p.addr.SetText(sopts.Endpoint)
|
||||
}
|
||||
|
||||
actions := []component.AppBarAction{
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Favorite",
|
||||
Tag: &p.wgFavorite,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
s := tunnel.GetTunnelID(p.id)
|
||||
if s == nil {
|
||||
return D{}
|
||||
}
|
||||
|
||||
if p.wgFavorite.Clicked(gtx) {
|
||||
s.Favorite(!s.IsFavorite())
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
|
||||
btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
|
||||
if s.IsFavorite() {
|
||||
btn.Color = color.NRGBA(colornames.Red500)
|
||||
} else {
|
||||
btn.Color = fg
|
||||
}
|
||||
return btn.Layout(gtx)
|
||||
},
|
||||
},
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Start/Stop",
|
||||
Tag: &p.wgState,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
s := tunnel.GetTunnelID(p.id)
|
||||
if p.wgState.Clicked(gtx) && s != nil {
|
||||
if s.IsClosed() {
|
||||
s = p.createTunnel()
|
||||
} else {
|
||||
s.Close()
|
||||
}
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
|
||||
if s != nil && !s.IsClosed() {
|
||||
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStop).Layout(gtx)
|
||||
} else {
|
||||
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStart).Layout(gtx)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Delete",
|
||||
Tag: &p.wgDelete,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgDelete.Clicked(gtx) {
|
||||
tunnel.DeleteTunnel(p.id)
|
||||
tunnel.SaveTunnel()
|
||||
p.router.SwitchTo(Route{Path: PageHome})
|
||||
}
|
||||
return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx)
|
||||
},
|
||||
},
|
||||
{
|
||||
OverflowAction: component.OverflowAction{
|
||||
Name: "Save",
|
||||
Tag: &p.wgDone,
|
||||
},
|
||||
Layout: func(gtx C, bg, fg color.NRGBA) D {
|
||||
if p.wgDone.Clicked(gtx) {
|
||||
defer p.router.SwitchTo(Route{Path: PageHome})
|
||||
|
||||
p.createTunnel()
|
||||
if s := tunnel.GetTunnelID(p.id); s != nil {
|
||||
s.Close()
|
||||
p.createTunnel()
|
||||
tunnel.SaveTunnel()
|
||||
}
|
||||
}
|
||||
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
|
||||
},
|
||||
},
|
||||
}
|
||||
p.router.bar.SetActions(actions, nil)
|
||||
p.router.bar.Title = "UDP"
|
||||
p.router.bar.NavigationIcon = icons.IconClose
|
||||
}
|
||||
|
||||
func (p *udpEditPage) createTunnel() tunnel.Tunnel {
|
||||
s := tunnel.NewUDPTunnel(
|
||||
tunnel.IDOption(p.id),
|
||||
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
|
||||
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
|
||||
)
|
||||
|
||||
if err := s.Run(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
tunnel.SetTunnel(s)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (p *udpEditPage) Layout(gtx C, th *material.Theme) D {
|
||||
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return component.Surface(th).Layout(gtx, func(gtx C) D {
|
||||
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
|
||||
return p.layout(gtx, th)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *udpEditPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layoutHeader(gtx, th, tunnel.GetTunnelID(p.id), &p.wgID, &p.wgEntrypoint)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Tunnel name").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.name.Layout(gtx, th, "Name")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 10}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, "Endpoint address").Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
addr := strings.TrimSpace(p.addr.Text())
|
||||
if addr == "" {
|
||||
return nil
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(addr); err != nil {
|
||||
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.addr.SetError(err.Error())
|
||||
} else {
|
||||
p.addr.ClearError()
|
||||
}
|
||||
|
||||
return p.addr.Layout(gtx, th, "Address")
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"gioui.org/font/gofont"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/text"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/go-gost/gost-plus/ui/page"
|
||||
)
|
||||
|
||||
type C = layout.Context
|
||||
type D = layout.Dimensions
|
||||
|
||||
type UI struct {
|
||||
th *material.Theme
|
||||
router *page.Router
|
||||
}
|
||||
|
||||
func NewUI() *UI {
|
||||
th := material.NewTheme()
|
||||
th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
|
||||
// th.Bg = color.NRGBA(colornames.Brown800)
|
||||
// th.Fg = color.NRGBA(colornames.Grey50)
|
||||
|
||||
ui := &UI{
|
||||
th: th,
|
||||
router: page.NewRouter(),
|
||||
}
|
||||
|
||||
return ui
|
||||
}
|
||||
|
||||
func (ui *UI) Layout(gtx C) D {
|
||||
return ui.router.Layout(gtx, ui.th)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package version
|
||||
|
||||
var Version = "0.1.0"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"RT_GROUP_ICON": {
|
||||
"OTHER": {
|
||||
"0000": [
|
||||
"icon.png"
|
||||
]
|
||||
}
|
||||
},
|
||||
"RT_MANIFEST": {
|
||||
"#1": {
|
||||
"0409": {
|
||||
"identity": {
|
||||
"name": "",
|
||||
"version": ""
|
||||
},
|
||||
"description": "Simple GUI client for GOST.PLUS",
|
||||
"minimum-os": "win7",
|
||||
"execution-level": "as invoker",
|
||||
"ui-access": false,
|
||||
"auto-elevate": false,
|
||||
"dpi-awareness": "system",
|
||||
"disable-theming": false,
|
||||
"disable-window-filtering": false,
|
||||
"high-resolution-scrolling-aware": false,
|
||||
"ultra-high-resolution-scrolling-aware": false,
|
||||
"long-path-aware": false,
|
||||
"printer-driver-isolation": false,
|
||||
"gdi-scaling": false,
|
||||
"segment-heap": false,
|
||||
"use-common-controls-v6": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"RT_VERSION": {
|
||||
"#1": {
|
||||
"0000": {
|
||||
"fixed": {
|
||||
"file_version": "0.1.0.0",
|
||||
"product_version": "0.1.0.0"
|
||||
},
|
||||
"info": {
|
||||
"0409": {
|
||||
"Comments": "",
|
||||
"CompanyName": "GOST.PLUS",
|
||||
"FileDescription": "A simple GUI client for GOST.PLUS",
|
||||
"FileVersion": "0.1.0",
|
||||
"InternalName": "gost.plus",
|
||||
"LegalCopyright": "Copyright © 2024 GOST.PLUS",
|
||||
"LegalTrademarks": "GOST.PLUS",
|
||||
"OriginalFilename": "gost-plus",
|
||||
"PrivateBuild": "",
|
||||
"ProductName": "GOST.PLUS",
|
||||
"ProductVersion": "0.1.0",
|
||||
"SpecialBuild": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user