initial commit

This commit is contained in:
ginuerzh
2023-12-14 15:38:33 +08:00
commit 905c6363ff
28 changed files with 4149 additions and 0 deletions
+26
View File
@@ -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
+21
View File
@@ -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.
+16
View File
@@ -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
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

+136
View File
@@ -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)
}
+65
View File
@@ -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
)
+197
View File
@@ -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=
+49
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+313
View File
@@ -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
View File
@@ -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
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

+56
View File
@@ -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
}
+57
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}),
)
})
})
}),
)
}
+95
View File
@@ -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
View File
@@ -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")
}),
)
}
+172
View File
@@ -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
View File
@@ -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")
}),
)
}
+35
View File
@@ -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)
}
+3
View File
@@ -0,0 +1,3 @@
package version
var Version = "0.1.0"
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+60
View File
@@ -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": ""
}
}
}
}
}
}