mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-27 02:51:36 +00:00
Add daemon support
This commit is contained in:
parent
a940703ae1
commit
f48f8c5d1c
2
Makefile
2
Makefile
|
@ -1,6 +1,6 @@
|
||||||
NAME = sing-box
|
NAME = sing-box
|
||||||
COMMIT = $(shell git rev-parse --short HEAD)
|
COMMIT = $(shell git rev-parse --short HEAD)
|
||||||
TAGS ?= with_quic,with_wireguard,with_clash_api
|
TAGS ?= with_quic,with_wireguard,with_clash_api,with_daemon
|
||||||
PARAMS = -v -trimpath -tags '$(TAGS)' -ldflags \
|
PARAMS = -v -trimpath -tags '$(TAGS)' -ldflags \
|
||||||
'-X "github.com/sagernet/sing-box/constant.Commit=$(COMMIT)" \
|
'-X "github.com/sagernet/sing-box/constant.Commit=$(COMMIT)" \
|
||||||
-w -s -buildid='
|
-w -s -buildid='
|
||||||
|
|
272
cmd/sing-box/cmd_daemon.go
Normal file
272
cmd/sing-box/cmd_daemon.go
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
//go:build with_daemon
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/json"
|
||||||
|
"github.com/sagernet/sing-box/experimental/daemon"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandDaemon = &cobra.Command{
|
||||||
|
Use: "daemon",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandDaemon.AddCommand(commandDaemonInstall)
|
||||||
|
commandDaemon.AddCommand(commandDaemonUninstall)
|
||||||
|
commandDaemon.AddCommand(commandDaemonStart)
|
||||||
|
commandDaemon.AddCommand(commandDaemonStop)
|
||||||
|
commandDaemon.AddCommand(commandDaemonRestart)
|
||||||
|
commandDaemon.AddCommand(commandDaemonRun)
|
||||||
|
mainCommand.AddCommand(commandDaemon)
|
||||||
|
mainCommand.AddCommand(commandStart)
|
||||||
|
mainCommand.AddCommand(commandStop)
|
||||||
|
mainCommand.AddCommand(commandStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandDaemonInstall = &cobra.Command{
|
||||||
|
Use: "install",
|
||||||
|
Short: "Install daemon",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := installDaemon()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandDaemonUninstall = &cobra.Command{
|
||||||
|
Use: "uninstall",
|
||||||
|
Short: "Uninstall daemon",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := uninstallDaemon()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandDaemonStart = &cobra.Command{
|
||||||
|
Use: "start",
|
||||||
|
Short: "Start daemon",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := startDaemon()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandDaemonStop = &cobra.Command{
|
||||||
|
Use: "stop",
|
||||||
|
Short: "Stop daemon",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := stopDaemon()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandDaemonRestart = &cobra.Command{
|
||||||
|
Use: "restart",
|
||||||
|
Short: "Restart daemon",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := restartDaemon()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandDaemonRun = &cobra.Command{
|
||||||
|
Use: "run",
|
||||||
|
Short: "Run daemon",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := runDaemon()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
func installDaemon() error {
|
||||||
|
instance, err := daemon.New()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return instance.Install()
|
||||||
|
}
|
||||||
|
|
||||||
|
func uninstallDaemon() error {
|
||||||
|
instance, err := daemon.New()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return instance.Uninstall()
|
||||||
|
}
|
||||||
|
|
||||||
|
func startDaemon() error {
|
||||||
|
instance, err := daemon.New()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return instance.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopDaemon() error {
|
||||||
|
instance, err := daemon.New()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return instance.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func restartDaemon() error {
|
||||||
|
instance, err := daemon.New()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return instance.Restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDaemon() error {
|
||||||
|
instance, err := daemon.New()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return instance.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandStart = &cobra.Command{
|
||||||
|
Use: "start",
|
||||||
|
Short: "Start service",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := startService()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandStop = &cobra.Command{
|
||||||
|
Use: "stop",
|
||||||
|
Short: "Stop service",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := stopService()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandStatus = &cobra.Command{
|
||||||
|
Use: "status",
|
||||||
|
Short: "Check service",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := checkService()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
|
||||||
|
func doRequest(method string, path string, params url.Values, body io.ReadCloser) ([]byte, error) {
|
||||||
|
requestURL := url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Path: path,
|
||||||
|
Host: net.JoinHostPort("127.0.0.1", F.ToString(daemon.DefaultDaemonPort)),
|
||||||
|
}
|
||||||
|
if params != nil {
|
||||||
|
requestURL.RawQuery = params.Encode()
|
||||||
|
}
|
||||||
|
request, err := http.NewRequest(method, requestURL.String(), body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response, err := http.DefaultClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
var content []byte
|
||||||
|
if response.StatusCode != http.StatusNoContent {
|
||||||
|
content, err = io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusNoContent {
|
||||||
|
return nil, E.New(string(content))
|
||||||
|
}
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ping() error {
|
||||||
|
response, err := doRequest("GET", "/ping", nil, nil)
|
||||||
|
if err != nil || string(response) != "pong" {
|
||||||
|
return E.New("daemon not running")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startService() error {
|
||||||
|
if err := ping(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configContent, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "read config")
|
||||||
|
}
|
||||||
|
return common.Error(doRequest("POST", "/run", nil, io.NopCloser(bytes.NewReader(configContent))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopService() error {
|
||||||
|
if err := ping(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return common.Error(doRequest("GET", "/stop", nil, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkService() error {
|
||||||
|
if err := ping(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
response, err := doRequest("GET", "/status", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var statusResponse daemon.StatusResponse
|
||||||
|
err = json.Unmarshal(response, &statusResponse)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if statusResponse.Running {
|
||||||
|
log.Info("service running")
|
||||||
|
} else {
|
||||||
|
log.Info("service stopped")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
165
experimental/daemon/daemon.go
Executable file
165
experimental/daemon/daemon.go
Executable file
|
@ -0,0 +1,165 @@
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/rw"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultDaemonName = "sing-box-daemon"
|
||||||
|
DefaultDaemonPort = 9091
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultDaemonOptions = Options{
|
||||||
|
Listen: "127.0.0.1",
|
||||||
|
ListenPort: DefaultDaemonPort,
|
||||||
|
WorkingDirectory: workingDirectory(),
|
||||||
|
}
|
||||||
|
|
||||||
|
func workingDirectory() string {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
return filepath.Join("/usr/local/lib", DefaultDaemonName)
|
||||||
|
default:
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err == nil {
|
||||||
|
return filepath.Join(configDir, DefaultDaemonName)
|
||||||
|
} else {
|
||||||
|
return DefaultDaemonName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemdScript = `[Unit]
|
||||||
|
Description=sing-box service
|
||||||
|
Documentation=https://sing-box.sagernet.org
|
||||||
|
After=network.target nss-lookup.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=root
|
||||||
|
ExecStart={{.Path|cmdEscape}}{{range .Arguments}} {{.|cmd}}{{end}}
|
||||||
|
WorkingDirectory={{.WorkingDirectory|cmdEscape}}
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10s
|
||||||
|
LimitNOFILE=infinity
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target`
|
||||||
|
|
||||||
|
type Daemon struct {
|
||||||
|
service service.Service
|
||||||
|
workingDirectory string
|
||||||
|
executable string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() (*Daemon, error) {
|
||||||
|
daemonInterface := NewInterface(defaultDaemonOptions)
|
||||||
|
executable := filepath.Join(defaultDaemonOptions.WorkingDirectory, "sing-box")
|
||||||
|
if C.IsWindows {
|
||||||
|
executable += ".exe"
|
||||||
|
}
|
||||||
|
daemonService, err := service.New(daemonInterface, &service.Config{
|
||||||
|
Name: DefaultDaemonName,
|
||||||
|
Description: "The universal proxy platform.",
|
||||||
|
Arguments: []string{"daemon", "run"},
|
||||||
|
Executable: executable,
|
||||||
|
Option: service.KeyValue{
|
||||||
|
"SystemdScript": systemdScript,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.New(strings.ToLower(err.Error()))
|
||||||
|
}
|
||||||
|
return &Daemon{
|
||||||
|
service: daemonService,
|
||||||
|
workingDirectory: defaultDaemonOptions.WorkingDirectory,
|
||||||
|
executable: executable,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) Install() error {
|
||||||
|
_, err := d.service.Status()
|
||||||
|
if err != service.ErrNotInstalled {
|
||||||
|
d.service.Stop()
|
||||||
|
err = d.service.Uninstall()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
executablePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !rw.FileExists(d.workingDirectory) {
|
||||||
|
err = os.MkdirAll(d.workingDirectory, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outputFile, err := os.OpenFile(d.executable, os.O_CREATE|os.O_WRONLY, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inputFile, err := os.Open(executablePath)
|
||||||
|
if err != nil {
|
||||||
|
outputFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(outputFile, inputFile)
|
||||||
|
inputFile.Close()
|
||||||
|
outputFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = d.service.Install()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return d.service.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) Uninstall() error {
|
||||||
|
_, err := d.service.Status()
|
||||||
|
if err != service.ErrNotInstalled {
|
||||||
|
d.service.Stop()
|
||||||
|
err = d.service.Uninstall()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return os.RemoveAll(d.workingDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) Run() error {
|
||||||
|
d.chdir()
|
||||||
|
return d.service.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) chdir() error {
|
||||||
|
executable, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Chdir(filepath.Dir(executable))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) Start() error {
|
||||||
|
return d.service.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) Stop() error {
|
||||||
|
return d.service.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Daemon) Restart() error {
|
||||||
|
return d.service.Restart()
|
||||||
|
}
|
58
experimental/daemon/instance.go
Normal file
58
experimental/daemon/instance.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Instance struct {
|
||||||
|
access sync.Mutex
|
||||||
|
boxInstance *box.Box
|
||||||
|
boxCancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) Running() bool {
|
||||||
|
i.access.Lock()
|
||||||
|
defer i.access.Unlock()
|
||||||
|
return i.boxInstance != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) Start(options option.Options) error {
|
||||||
|
i.access.Lock()
|
||||||
|
defer i.access.Unlock()
|
||||||
|
if i.boxInstance != nil {
|
||||||
|
i.boxCancel()
|
||||||
|
i.boxInstance.Close()
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
instance, err := box.New(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = instance.Start()
|
||||||
|
if err != nil {
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.boxInstance = instance
|
||||||
|
i.boxCancel = cancel
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) Close() error {
|
||||||
|
i.access.Lock()
|
||||||
|
defer i.access.Unlock()
|
||||||
|
if i.boxInstance == nil {
|
||||||
|
return os.ErrClosed
|
||||||
|
}
|
||||||
|
i.boxCancel()
|
||||||
|
err := i.boxInstance.Close()
|
||||||
|
i.boxInstance = nil
|
||||||
|
i.boxCancel = nil
|
||||||
|
return err
|
||||||
|
}
|
20
experimental/daemon/interface.go
Normal file
20
experimental/daemon/interface.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
import "github.com/kardianos/service"
|
||||||
|
|
||||||
|
type Interface struct {
|
||||||
|
server *Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterface(options Options) *Interface {
|
||||||
|
return &Interface{NewServer(options)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Interface) Start(_ service.Service) error {
|
||||||
|
return d.server.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Interface) Stop(_ service.Service) error {
|
||||||
|
d.server.Close()
|
||||||
|
return nil
|
||||||
|
}
|
147
experimental/daemon/server.go
Normal file
147
experimental/daemon/server.go
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/pprof"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/json"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/cors"
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Listen string `json:"listen"`
|
||||||
|
ListenPort uint16 `json:"listen_port"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
WorkingDirectory string `json:"working_directory"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
options Options
|
||||||
|
httpServer *http.Server
|
||||||
|
instance Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(options Options) *Server {
|
||||||
|
return &Server{
|
||||||
|
options: options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
tcpConn, err := net.Listen("tcp", net.JoinHostPort(s.options.Listen, F.ToString(s.options.ListenPort)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
router := chi.NewRouter()
|
||||||
|
router.Use(cors.New(cors.Options{
|
||||||
|
AllowedOrigins: []string{"*"},
|
||||||
|
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
|
||||||
|
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
||||||
|
MaxAge: 300,
|
||||||
|
}).Handler)
|
||||||
|
if s.options.Secret != "" {
|
||||||
|
router.Use(s.authentication)
|
||||||
|
}
|
||||||
|
router.Get("/ping", s.ping)
|
||||||
|
router.Get("/status", s.status)
|
||||||
|
router.Post("/run", s.run)
|
||||||
|
router.Get("/stop", s.stop)
|
||||||
|
router.Route("/debug/pprof", func(r chi.Router) {
|
||||||
|
r.HandleFunc("/", pprof.Index)
|
||||||
|
r.HandleFunc("/cmdline", pprof.Cmdline)
|
||||||
|
r.HandleFunc("/profile", pprof.Profile)
|
||||||
|
r.HandleFunc("/symbol", pprof.Symbol)
|
||||||
|
r.HandleFunc("/trace", pprof.Trace)
|
||||||
|
})
|
||||||
|
httpServer := &http.Server{
|
||||||
|
Handler: router,
|
||||||
|
}
|
||||||
|
go httpServer.Serve(tcpConn)
|
||||||
|
s.httpServer = httpServer
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) authentication(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if websocket.IsWebSocketUpgrade(request) && request.URL.Query().Get("token") != "" {
|
||||||
|
token := request.URL.Query().Get("token")
|
||||||
|
if token != s.options.Secret {
|
||||||
|
render.Status(request, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(writer, request)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
header := request.Header.Get("Authorization")
|
||||||
|
bearer, token, found := strings.Cut(header, " ")
|
||||||
|
hasInvalidHeader := bearer != "Bearer"
|
||||||
|
hasInvalidSecret := !found || token != s.options.Secret
|
||||||
|
if hasInvalidHeader || hasInvalidSecret {
|
||||||
|
render.Status(request, http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next.ServeHTTP(writer, request)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Close() error {
|
||||||
|
return common.Close(
|
||||||
|
common.PtrOrNil(s.httpServer),
|
||||||
|
&s.instance,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) ping(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
render.PlainText(writer, request, "pong")
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusResponse struct {
|
||||||
|
Running bool `json:"running"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) status(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
render.JSON(writer, request, StatusResponse{
|
||||||
|
Running: s.instance.Running(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) run(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
err := s.run0(request)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn(err)
|
||||||
|
render.Status(request, http.StatusBadRequest)
|
||||||
|
render.PlainText(writer, request, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writer.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) run0(request *http.Request) error {
|
||||||
|
configContent, err := io.ReadAll(request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "read config")
|
||||||
|
}
|
||||||
|
var options option.Options
|
||||||
|
err = json.Unmarshal(configContent, &options)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "decode config")
|
||||||
|
}
|
||||||
|
return s.instance.Start(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) stop(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
s.instance.Close()
|
||||||
|
writer.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
1
go.mod
1
go.mod
|
@ -14,6 +14,7 @@ require (
|
||||||
github.com/gofrs/uuid v4.2.0+incompatible
|
github.com/gofrs/uuid v4.2.0+incompatible
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/hashicorp/yamux v0.1.1
|
github.com/hashicorp/yamux v0.1.1
|
||||||
|
github.com/kardianos/service v1.2.1
|
||||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||||
github.com/mholt/acmez v1.0.4
|
github.com/mholt/acmez v1.0.4
|
||||||
github.com/oschwald/maxminddb-golang v1.10.0
|
github.com/oschwald/maxminddb-golang v1.10.0
|
||||||
|
|
3
go.sum
3
go.sum
|
@ -88,6 +88,8 @@ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbg
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/kardianos/service v1.2.1 h1:AYndMsehS+ywIS6RB9KOlcXzteWUzxgMgBymJD7+BYk=
|
||||||
|
github.com/kardianos/service v1.2.1/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
|
github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0=
|
||||||
github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
|
@ -239,6 +241,7 @@ golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
|
Loading…
Reference in a new issue