mirror of
https://github.com/e1732a364fed/v2ray_simple.git
synced 2025-09-27 05:05:53 +08:00
402 lines
9.9 KiB
Go
402 lines
9.9 KiB
Go
package machine
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"crypto/tls"
|
|
"flag"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/e1732a364fed/v2ray_simple/proxy"
|
|
"github.com/e1732a364fed/v2ray_simple/tlsLayer"
|
|
"github.com/e1732a364fed/v2ray_simple/utils"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const eIllegalParameter = "illegal parameter"
|
|
|
|
/*
|
|
curl -k https://127.0.0.1:48345/api/allstate
|
|
*/
|
|
|
|
type ApiServerConf struct {
|
|
EnableApiServer bool `toml:"enable"`
|
|
PlainHttp bool `toml:"plain"`
|
|
KeyFile string `toml:"key"`
|
|
CertFile string `toml:"cert"`
|
|
PathPrefix string `toml:"prefix"`
|
|
AdminPass string `toml:"admin_pass"`
|
|
Addr string `toml:"addr"`
|
|
}
|
|
|
|
// 内含默认值的 ApiServerConf
|
|
func NewApiServerConf() (ac ApiServerConf) {
|
|
ac.SetupFlags(flag.NewFlagSet("", 10))
|
|
return
|
|
}
|
|
|
|
// if fs == nil, flag.CommandLine will be used
|
|
func (asc *ApiServerConf) SetupFlags(fs *flag.FlagSet) {
|
|
if fs == nil {
|
|
fs = flag.CommandLine
|
|
}
|
|
fs.BoolVar(&asc.EnableApiServer, "ea", false, "enable api server")
|
|
|
|
fs.BoolVar(&asc.PlainHttp, "sunsafe", false, "if given, api Server will use http instead of https")
|
|
|
|
fs.StringVar(&asc.PathPrefix, "spp", "/api", "api Server Path Prefix, must start with '/' ")
|
|
fs.StringVar(&asc.AdminPass, "sap", "", "api Server admin password, but won't be used if it's empty")
|
|
fs.StringVar(&asc.Addr, "sa", "127.0.0.1:48345", "api Server listen address")
|
|
fs.StringVar(&asc.CertFile, "scert", "", "api Server tls cert file path")
|
|
fs.StringVar(&asc.KeyFile, "skey", "", "api Server tls cert key path")
|
|
|
|
}
|
|
|
|
// 若 ref 里有与默认值不同的项且字符串不为空, 将该项的值赋值给 c
|
|
func (c *ApiServerConf) SetNonDefault(ref *ApiServerConf) {
|
|
d := NewApiServerConf()
|
|
var emptyAc ApiServerConf
|
|
|
|
if ref.PlainHttp != d.PlainHttp {
|
|
c.PlainHttp = ref.PlainHttp
|
|
}
|
|
if ref.EnableApiServer != d.EnableApiServer {
|
|
c.EnableApiServer = ref.EnableApiServer
|
|
}
|
|
|
|
if ref.Addr != d.Addr && ref.Addr != emptyAc.Addr {
|
|
c.Addr = ref.Addr
|
|
}
|
|
if ref.AdminPass != d.AdminPass {
|
|
c.AdminPass = ref.AdminPass
|
|
}
|
|
if ref.PathPrefix != d.PathPrefix && ref.PathPrefix != emptyAc.PathPrefix {
|
|
c.PathPrefix = ref.PathPrefix
|
|
}
|
|
if ref.CertFile != d.CertFile {
|
|
c.CertFile = ref.CertFile
|
|
}
|
|
if ref.KeyFile != d.KeyFile {
|
|
c.KeyFile = ref.KeyFile
|
|
}
|
|
}
|
|
|
|
// 非阻塞,如果运行成功则 apiServerRunning 会被设为 true
|
|
func (m *M) TryRunApiServer() {
|
|
|
|
m.apiServerRunning = true
|
|
|
|
go m.runApiServer()
|
|
|
|
}
|
|
|
|
// 阻塞
|
|
func (m *M) runApiServer() {
|
|
|
|
var adminUUID string = m.AdminPass
|
|
|
|
var addrStr = m.Addr
|
|
if m.PlainHttp {
|
|
if !strings.HasPrefix(addrStr, "http://") {
|
|
addrStr = "http://" + addrStr
|
|
|
|
}
|
|
} else {
|
|
if !strings.HasPrefix(addrStr, "https://") {
|
|
addrStr = "https://" + addrStr
|
|
|
|
}
|
|
}
|
|
|
|
utils.Info("Start Api Server at " + addrStr)
|
|
|
|
ser := newApiServer("admin", adminUUID)
|
|
ser.PathPrefix = m.PathPrefix
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
failBadRequest := func(e error, eInfo string, w http.ResponseWriter) {
|
|
if ce := utils.CanLogWarn(eInfo); ce != nil {
|
|
ce.Write(zap.Error(e))
|
|
}
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}
|
|
|
|
ser.addServerHandle(mux, "allstate", func(w http.ResponseWriter, r *http.Request) {
|
|
m.PrintAllState(w, false)
|
|
})
|
|
ser.addServerHandle(mux, "hotDelete", func(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
|
|
listenIndexStr := q.Get("listen")
|
|
dialIndexStr := q.Get("dial")
|
|
if listenIndexStr != "" {
|
|
if ce := utils.CanLogInfo("api server got hot delete listen request"); ce != nil {
|
|
ce.Write(zap.String("listenIndexStr", listenIndexStr))
|
|
}
|
|
|
|
listenIndex, err := strconv.Atoi(listenIndexStr)
|
|
if err != nil {
|
|
failBadRequest(err, eIllegalParameter, w)
|
|
|
|
w.Write([]byte(eIllegalParameter))
|
|
return
|
|
}
|
|
m.HotDeleteServer(listenIndex)
|
|
}
|
|
if dialIndexStr != "" {
|
|
|
|
if ce := utils.CanLogInfo("api server got hot delete dial request"); ce != nil {
|
|
ce.Write(zap.String("dialIndexStr", dialIndexStr))
|
|
}
|
|
|
|
dialIndex, err := strconv.Atoi(dialIndexStr)
|
|
if err != nil {
|
|
failBadRequest(err, eIllegalParameter, w)
|
|
|
|
w.Write([]byte(eIllegalParameter))
|
|
return
|
|
}
|
|
m.HotDeleteClient(dialIndex)
|
|
}
|
|
})
|
|
|
|
ser.addServerHandle(mux, "hotLoadUrl", func(w http.ResponseWriter, r *http.Request) {
|
|
if e := r.ParseForm(); e != nil {
|
|
|
|
failBadRequest(e, "api server ParseForm failed", w)
|
|
return
|
|
|
|
}
|
|
|
|
f := r.Form
|
|
//log.Println("f", f, len(f))
|
|
|
|
uf := proxy.UrlFormat
|
|
|
|
listenStr := f.Get("listen")
|
|
dialStr := f.Get("dial")
|
|
urlFormatStr := f.Get("urlFormat")
|
|
if urlFormatStr != "" {
|
|
var err error
|
|
uf, err = strconv.Atoi(urlFormatStr)
|
|
if err != nil || uf >= proxy.Url_FormatUnknown {
|
|
failBadRequest(utils.ErrInErr{ErrDetail: err, Data: urlFormatStr}, "api server parse urlFormat failed", w)
|
|
return
|
|
|
|
}
|
|
}
|
|
|
|
var resultStr string = "result:"
|
|
if listenStr != "" {
|
|
|
|
if ce := utils.CanLogInfo("api server got hot load listen request"); ce != nil {
|
|
ce.Write(zap.String("listenUrl", listenStr))
|
|
}
|
|
e := m.HotLoadListenUrl(listenStr, uf)
|
|
if e == nil {
|
|
resultStr += "\nhot load listen Url Success for " + listenStr
|
|
} else {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
resultStr += "\nhot load listen Url Failed for " + listenStr
|
|
}
|
|
}
|
|
if dialStr != "" {
|
|
|
|
if ce := utils.CanLogInfo("api server got hot load dial request"); ce != nil {
|
|
ce.Write(zap.String("dialUrl", dialStr))
|
|
}
|
|
e := m.HotLoadDialUrl(dialStr, uf)
|
|
if e == nil {
|
|
resultStr += "\nhot load dial Url Success for " + dialStr
|
|
} else {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
resultStr += "\nhot load dial Url Failed for " + dialStr
|
|
}
|
|
}
|
|
w.Write([]byte(resultStr))
|
|
})
|
|
|
|
ser.addServerHandle(mux, "getDetailUrl", func(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
|
|
indexStr := q.Get("index")
|
|
isDial := utils.QueryPositive(q, "isDial")
|
|
if indexStr != "" {
|
|
if ce := utils.CanLogInfo("api server got hot delete listen request"); ce != nil {
|
|
ce.Write(zap.String("listenIndexStr", indexStr))
|
|
}
|
|
|
|
ind, err := strconv.Atoi(indexStr)
|
|
if err != nil || ind < 0 || (isDial && ind >= len(m.allClients)) || (!isDial && ind >= len(m.allServers)) {
|
|
failBadRequest(err, eIllegalParameter, w)
|
|
|
|
w.Write([]byte(eIllegalParameter))
|
|
return
|
|
}
|
|
if isDial {
|
|
dc := m.dumpDialConf(ind)
|
|
url := proxy.ToStandardUrl(&dc.CommonConf, &dc, nil)
|
|
w.Write([]byte(url))
|
|
} else {
|
|
lc := m.dumpListenConf(ind)
|
|
url := proxy.ToStandardUrl(&lc.CommonConf, nil, &lc)
|
|
w.Write([]byte(url))
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
//保存所有配置到标准配置文件. 如果是GET, 直接将文件打印给客户, 如果是POST, 接收name参数并导出到文件
|
|
ser.addServerHandle(mux, "dump", func(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
|
|
fn := q.Get("name")
|
|
if fn == "" && r.Method == "POST" {
|
|
if ce := utils.CanLogWarn("api server got dump request but no file name given"); ce != nil {
|
|
ce.Write()
|
|
}
|
|
return
|
|
}
|
|
vc := m.DumpVSConf()
|
|
|
|
bs, e := utils.GetPurgedTomlBytes(vc)
|
|
if e != nil {
|
|
if ce := utils.CanLogErr("api server: 转换格式错误"); ce != nil {
|
|
ce.Write(zap.Error(e))
|
|
}
|
|
w.WriteHeader(500)
|
|
w.Write([]byte("failed"))
|
|
|
|
return
|
|
}
|
|
|
|
if r.Method == "GET" {
|
|
w.Write(bs)
|
|
} else {
|
|
e = os.WriteFile(fn, bs, 0666)
|
|
|
|
if e != nil {
|
|
if ce := utils.CanLogErr("写入文件错误"); ce != nil {
|
|
ce.Write(zap.Error(e))
|
|
}
|
|
w.WriteHeader(500)
|
|
w.Write([]byte("failed"))
|
|
|
|
return
|
|
}
|
|
|
|
if ce := utils.CanLogInfo("导出成功"); ce != nil {
|
|
ce.Write(zap.String("filename", fn))
|
|
}
|
|
w.Write([]byte("ok"))
|
|
}
|
|
|
|
})
|
|
|
|
tlsConf := &tls.Config{}
|
|
|
|
if m.PlainHttp {
|
|
|
|
} else if m.CertFile == "" || m.KeyFile == "" {
|
|
log.Println("api server will use tls but key or cert file not provided, use random cert instead")
|
|
tlsConf = &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
Certificates: tlsLayer.GenerateRandomTLSCert(), //curl -k
|
|
}
|
|
}
|
|
|
|
srv := &http.Server{
|
|
Addr: m.Addr,
|
|
Handler: mux,
|
|
IdleTimeout: time.Minute,
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 30 * time.Second,
|
|
TLSConfig: tlsConf,
|
|
}
|
|
|
|
if m.PlainHttp {
|
|
srv.ListenAndServe()
|
|
|
|
} else {
|
|
srv.ListenAndServeTLS(m.CertFile, m.KeyFile)
|
|
|
|
}
|
|
m.apiServerRunning = false
|
|
}
|
|
|
|
type auth struct {
|
|
expectedUsernameHash [32]byte
|
|
expectedPasswordHash [32]byte
|
|
}
|
|
|
|
type apiServer struct {
|
|
admin_auth auth
|
|
nopass bool
|
|
PathPrefix string
|
|
}
|
|
|
|
func newApiServer(user, pass string) *apiServer {
|
|
s := new(apiServer)
|
|
|
|
if pass != "" {
|
|
s.admin_auth.expectedUsernameHash = sha256.Sum256([]byte(user))
|
|
s.admin_auth.expectedPasswordHash = sha256.Sum256([]byte(pass))
|
|
|
|
} else {
|
|
s.nopass = true
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (ser *apiServer) addServerHandle(mux *http.ServeMux, name string, f func(w http.ResponseWriter, r *http.Request)) {
|
|
mux.HandleFunc(ser.PathPrefix+"/"+name, ser.basicAuth(f))
|
|
}
|
|
|
|
func (ser *apiServer) basicAuth(realfunc http.HandlerFunc) http.HandlerFunc {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
doFunc := func() {
|
|
if ce := utils.CanLogInfo("api server got new request"); ce != nil {
|
|
ce.Write(
|
|
zap.String("method", r.Method),
|
|
zap.String("requestURL", r.RequestURI),
|
|
)
|
|
}
|
|
w.Header().Add("Access-Control-Allow-Origin", "*") //避免在网页请求本api时, 客户端遇到CSRF保护问题
|
|
|
|
realfunc.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
if ser.nopass {
|
|
doFunc()
|
|
return
|
|
}
|
|
|
|
thisun, thispass, ok := r.BasicAuth()
|
|
if ok {
|
|
usernameHash := sha256.Sum256([]byte(thisun))
|
|
passwordHash := sha256.Sum256([]byte(thispass))
|
|
|
|
usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], ser.admin_auth.expectedUsernameHash[:]) == 1)
|
|
passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], ser.admin_auth.expectedPasswordHash[:]) == 1)
|
|
|
|
if usernameMatch && passwordMatch {
|
|
|
|
doFunc()
|
|
return
|
|
}
|
|
}
|
|
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
})
|
|
}
|