From 7650951518a558d9864eb0ab3a882f568c6d8069 Mon Sep 17 00:00:00 2001 From: starainrt Date: Thu, 19 Jun 2025 23:47:39 +0800 Subject: [PATCH] =?UTF-8?q?1.http=20server=E6=94=AF=E6=8C=81=E9=BB=91?= =?UTF-8?q?=E7=99=BD=E8=B7=AF=E7=94=B1=E5=90=8D=E5=8D=95=E5=92=8C=E5=AD=90?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=B9=E6=8C=82=E8=BD=BD=202.http=E5=8F=8D?= =?UTF-8?q?=E5=90=91=E4=BB=A3=E7=90=86=E6=94=AF=E6=8C=81=E5=90=8C=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E7=AB=AF=E5=8F=A3=E6=8C=89host=E5=8C=BA=E5=88=86?= =?UTF-8?q?=E4=B8=8D=E9=80=9A=E6=9C=8D=E5=8A=A1=EF=BC=8C=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E9=BB=91=E7=99=BD=E8=B7=AF=E7=94=B1=E5=90=8D=E5=8D=95=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- httpreverse/cmd.go | 14 +- httpreverse/reverse.go | 77 +++++-- httpreverse/route.go | 117 ++++++++++ httpreverse/service.go | 503 +++++++++++++++++++++++++++++++++++------ httpserver/cmd.go | 27 ++- httpserver/route.go | 117 ++++++++++ httpserver/server.go | 142 ++++++++++-- netforward/cmd.go | 1 + netforward/forward.go | 7 +- tls/cert.go | 103 +++++++-- utils/cert.go | 14 +- utils/cert_test.go | 2 +- 12 files changed, 984 insertions(+), 140 deletions(-) create mode 100644 httpreverse/route.go create mode 100644 httpserver/route.go diff --git a/httpreverse/cmd.go b/httpreverse/cmd.go index f757e2f..0120261 100644 --- a/httpreverse/cmd.go +++ b/httpreverse/cmd.go @@ -63,20 +63,26 @@ var Cmd = &cobra.Command{ starlog.Errorln(err) os.Exit(3) } - reverse := ReverseConfig{ + cfg := &SingleReverseConfig{ Name: "web", - Addr: addr, Host: host, - ReverseURL: map[string]*url.URL{ + ReverseURL: map[string]any{ "/": u, }, - Port: port, UsingSSL: enablessl, SkipSSLVerify: skipsslverify, Key: key, Cert: cert, IPFilterMode: 1, } + reverse := ReverseConfig{ + Addr: addr, + Port: port, + Config: []*SingleReverseConfig{cfg}, + routes: map[string]*SingleReverseConfig{ + host: cfg, + }, + } go func() { sig := make(chan os.Signal) signal.Notify(sig, os.Kill, os.Interrupt) diff --git a/httpreverse/reverse.go b/httpreverse/reverse.go index e7e0cec..399c5dd 100644 --- a/httpreverse/reverse.go +++ b/httpreverse/reverse.go @@ -12,21 +12,31 @@ import ( "net/http" "net/url" "os" + "path" "strings" "sync" ) type ReverseConfig struct { + Addr string + Port int + httpmux http.ServeMux + httpserver http.Server + Config []*SingleReverseConfig + routes map[string]*SingleReverseConfig + autogenCert bool //是否自动生成证书 +} + +type SingleReverseConfig struct { Name string - Addr string - ReverseURL map[string]*url.URL - Port int + ReverseURL map[string]any UsingSSL bool AllowHTTPWithHttps bool AutoGenerateCert bool Key string Cert string Host string + ProxyHost string SkipSSLVerify bool InHeader [][2]string OutHeader [][2]string @@ -34,14 +44,13 @@ type ReverseConfig struct { ReplaceList [][2]string ReplaceOnce bool proxy map[string]*rp.ReverseProxy + httpPage map[string]string IPFilterMode int //0=off 1=useremote 2=add 3=filter FilterXForward bool FilterRemoteAddr bool FilterMustKey string FilterSetKey string FilterFile string - httpmux http.ServeMux - httpserver http.Server CIDR []*net.IPNet basicAuthUser string @@ -51,25 +60,40 @@ type ReverseConfig struct { whiteip map[string]int warningpage string warnpagedata []byte + router Router + blackpath Router + whitepath Router + rootLeaf any } type HttpReverseServer struct { Config []*ReverseConfig } -func Parse(path string) (HttpReverseServer, error) { +func Parse(cfgPath string) (HttpReverseServer, error) { var res HttpReverseServer ini := sysconf.NewIni() - err := ini.ParseFromFile(path) + err := ini.ParseFromFile(cfgPath) if err != nil { return res, err } + serverMap := make(map[string]*ReverseConfig) for _, v := range ini.Data { - var ins = ReverseConfig{ + var cfg *ReverseConfig + keyID := v.Get("addr") + v.Get("port") + if _, ok := serverMap[keyID]; ok { + cfg = serverMap[keyID] + } else { + cfg = &ReverseConfig{ + Addr: v.Get("addr"), + Port: v.Int("port"), + routes: make(map[string]*SingleReverseConfig), + } + serverMap[keyID] = cfg + } + var ins = SingleReverseConfig{ Name: v.Name, Host: v.Get("host"), - Addr: v.Get("addr"), - Port: v.Int("port"), UsingSSL: v.Bool("enablessl"), AllowHTTPWithHttps: v.Bool("tlsallowhttp"), AutoGenerateCert: v.Bool("autogencert"), @@ -125,16 +149,21 @@ func Parse(path string) (HttpReverseServer, error) { ins.warnpagedata = data } ins.proxy = make(map[string]*rp.ReverseProxy) - ins.ReverseURL = make(map[string]*url.URL) + ins.ReverseURL = make(map[string]any) for _, reverse := range v.GetAll("reverse") { kv := strings.SplitN(reverse, "::", 2) if len(kv) != 2 { return res, errors.New("reverse settings not correct:" + reverse) } - ins.ReverseURL[strings.TrimSpace(kv[0])], err = url.Parse(strings.TrimSpace(kv[1])) - if err != nil { - return res, err + if !strings.HasPrefix(strings.TrimSpace(kv[1]), "http://") && !strings.HasPrefix(strings.TrimSpace(kv[1]), "https://") { + ins.ReverseURL[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) + } else { + ins.ReverseURL[strings.TrimSpace(kv[0])], err = url.Parse(strings.TrimSpace(kv[1])) + if err != nil { + return res, err + } } + } for _, header := range v.GetAll("inheader") { kv := strings.SplitN(header, "::", 2) @@ -154,6 +183,20 @@ func Parse(path string) (HttpReverseServer, error) { } ins.blackip[ip] = cidr } + for _, blackpath := range v.GetAll("blackpath") { + isStar := path.Base(blackpath) == "*" + if isStar { + blackpath = path.Dir(blackpath) + } + ins.blackpath.AddLeaf(blackpath, isStar) + } + for _, whitepath := range v.GetAll("whitepath") { + isStar := path.Base(whitepath) == "*" + if isStar { + whitepath = path.Dir(whitepath) + } + ins.whitepath.AddLeaf(whitepath, isStar) + } ins.whiteip = make(map[string]int) for _, whiteip := range v.GetAll("whiteip") { ip, cidr, err := IPCIDR(whiteip) @@ -183,7 +226,11 @@ func Parse(path string) (HttpReverseServer, error) { } ins.ReplaceList = append(ins.ReplaceList, [2]string{strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1])}) } - res.Config = append(res.Config, &ins) + cfg.routes[ins.Host] = &ins + cfg.Config = append(cfg.Config, &ins) + } + for _, v := range serverMap { + res.Config = append(res.Config, v) } return res, nil } diff --git a/httpreverse/route.go b/httpreverse/route.go new file mode 100644 index 0000000..b66ea1c --- /dev/null +++ b/httpreverse/route.go @@ -0,0 +1,117 @@ +package httpreverse + +import "strings" + +type Router struct { + Hostname string + Leaf *Leaf + leafMap map[string]*Leaf +} + +type Leaf struct { + Name string + Val any + Last *Leaf + Next map[string]*Leaf + FullPath string +} + +func NewRouter(hostname string) *Router { + return &Router{ + Hostname: hostname, + Leaf: &Leaf{ + Name: "/", + Next: make(map[string]*Leaf), + FullPath: "/", + }, + leafMap: make(map[string]*Leaf), + } +} + +func (r *Router) AddLeaf(path string, val any) { + if r.Leaf == nil { + r.Leaf = &Leaf{ + Name: "/", + Next: make(map[string]*Leaf), + FullPath: "/", + } + } + if r.leafMap == nil { + r.leafMap = make(map[string]*Leaf) + r.leafMap["/"] = r.Leaf + } + names := strings.Split(path, "/") + leaf := r.Leaf + for _, name := range names { + if name == "" { + continue + } + if leaf.Next[name] == nil { + fullPath := leaf.FullPath + "/" + name + if leaf.FullPath == "/" { + fullPath = "/" + name + } + leaf.Next[name] = &Leaf{ + Name: name, + Next: make(map[string]*Leaf), + FullPath: fullPath, + } + r.leafMap[fullPath] = leaf.Next[name] + } + leaf = leaf.Next[name] + } + leaf.Val = val +} + +func (r *Router) GetLeaf(path string) *Leaf { + return r.leafMap[path] +} + +func (r *Router) NearestLeaf(path string) *Leaf { + if path == "/" { + return r.Leaf + } + if leaf, ok := r.leafMap[path]; ok { + return leaf + } + parts := strings.Split(path, "/") + leaf := r.Leaf + for _, v := range parts { + if v == "" { + continue + } + if leaf.Next[v] == nil { + return leaf + } + leaf = leaf.Next[v] + } + return leaf +} + +func (r *Router) NearestLeafWithVal(path string) *Leaf { + if path == "/" { + return r.Leaf + } + if leaf, ok := r.leafMap[path]; ok && leaf.Val != nil { + return leaf + } + var lastValue *Leaf + parts := strings.Split(path, "/") + leaf := r.Leaf + if leaf.Val != nil { + lastValue = leaf + } + for _, v := range parts { + if v == "" { + continue + } + if leaf.Next[v] == nil { + return lastValue + } + leaf = leaf.Next[v] + if leaf.Val != nil { + lastValue = leaf + } + } + return lastValue +} diff --git a/httpreverse/service.go b/httpreverse/service.go index 652202d..778748d 100644 --- a/httpreverse/service.go +++ b/httpreverse/service.go @@ -3,18 +3,25 @@ package httpreverse import ( "b612.me/apps/b612/httpreverse/rp" "b612.me/apps/b612/utils" + "b612.me/starcrypto" "b612.me/starlog" "b612.me/starnet" + "b612.me/staros" "bytes" "context" "crypto/tls" "crypto/x509" "encoding/base64" + "errors" "fmt" + "github.com/gabriel-vasile/mimetype" + "io" "io/ioutil" "net" "net/http" "net/url" + "os" + "path/filepath" "strconv" "strings" "time" @@ -27,64 +34,147 @@ func (h *ReverseConfig) Run() error { if err != nil { return err } - for key, proxy := range h.proxy { - h.httpmux.HandleFunc(key, func(writer http.ResponseWriter, request *http.Request) { - starlog.Infof("<%s> Req Path:%s ListenAddr:%s UA:%s\n", h.Name, request.URL.Path, request.RemoteAddr, request.Header.Get("User-Agent")) - if !h.BasicAuth(writer, request) { - h.SetResponseHeader(writer) + h.httpmux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { + c, ok := h.routes[request.Host] + if !ok { + if _, ok := h.routes[""]; ok { + c = h.routes[""] + } else { + if len(h.routes) > 0 { + for _, v := range h.routes { + c = v + break + } + } + } + } + rejectWith403 := func(writer http.ResponseWriter, request *http.Request) { + c.SetResponseHeader(writer) + writer.WriteHeader(403) + if len(c.warnpagedata) != 0 { + writer.Write(c.warnpagedata) return } - if !h.filter(writer, request) { - h.SetResponseHeader(writer) - writer.WriteHeader(403) - if len(h.warnpagedata) != 0 { - writer.Write(h.warnpagedata) + writer.Write([]byte(` + + 403 Forbidden + +

403 Forbidden

+

You Are Not Allowed to Access This Page

+

Please Contact Site Administrator For Help

+
B612 HTTP REVERSE SERVER
+ + `)) + } + starlog.Infof("<%s> Req Path:%s ListenAddr:%s UA:%s\n", c.Name, request.URL.Path, request.RemoteAddr, request.Header.Get("User-Agent")) + checkPath := request.URL.Path + if strings.HasSuffix(checkPath, "/") && checkPath != "/" { + checkPath = strings.TrimSuffix(checkPath, "/") + } + if c.blackpath.Leaf != nil { + bp := c.blackpath.NearestLeafWithVal(checkPath) + if bp != nil { + if ppr, ok := bp.Val.(bool); ok { + if ppr || (!ppr && bp.FullPath == checkPath) { + starlog.Errorf("<%s> Path:%s is in black path, reject request\n", c.Name, checkPath) + rejectWith403(writer, request) + return + } + } + } + } + if c.whitepath.Leaf != nil { + bp := c.whitepath.NearestLeafWithVal(checkPath) + if bp == nil { + starlog.Errorf("<%s> Path:%s is not in the write path, reject request\n", c.Name, checkPath) + rejectWith403(writer, request) + return + } + if ppr, ok := bp.Val.(bool); !ok { + starlog.Errorf("<%s> Path:%s is not in the write path, reject request\n", c.Name, checkPath) + rejectWith403(writer, request) + return + } else { + if !ppr && bp.FullPath != checkPath { + fmt.Println(bp.FullPath, checkPath) + starlog.Errorf("<%s> Path:%s is not in the write path, reject request\n", c.Name, checkPath) + rejectWith403(writer, request) return } - writer.Write([]byte(` - - 403 Forbidden - -

403 Forbidden

-

You Are Not Allowed to Access This Page

-

Please Contact Site Administrator For Help

-
B612 HTTP REVERSE SERVER
- - `)) - return } - proxy.ServeHTTP(writer, request) - }) - } + } + if !c.BasicAuth(writer, request) { + c.SetResponseHeader(writer) + return + } + if !c.filter(writer, request) { + rejectWith403(writer, request) + return + } + leaf := c.router.NearestLeafWithVal(checkPath) + if request.URL.Path == "/" && c.rootLeaf != nil { + leaf = &Leaf{ + Name: leaf.Name, + Val: c.rootLeaf, + Last: leaf.Last, + Next: leaf.Next, + FullPath: leaf.FullPath, + } + } + fmt.Println(leaf.Val) + if leaf == nil { + starlog.Errorf("<%s> No Reverse Proxy Found For Path:%s\n", c.Name, request.URL.Path) + writer.WriteHeader(404) + writer.Write([]byte("404 Not Found")) + return + } + if rp, ok := leaf.Val.(*rp.ReverseProxy); ok { + rp.ServeHTTP(writer, request) + return + } + if page, ok := leaf.Val.(string); ok { + h.fileHandle(leaf.FullPath, page, writer, request) + return + } + writer.WriteHeader(500) + }) h.httpserver = http.Server{ Addr: fmt.Sprintf("%s:%d", h.Addr, h.Port), Handler: &h.httpmux, } - starlog.Infoln(h.Name + " Listening on " + h.Addr + ":" + strconv.Itoa(h.Port)) - if !h.UsingSSL && !h.AutoGenerateCert { + var realUsingsSSL bool + var realAllowHTTPWithHttps bool + for _, c := range h.Config { + if c.UsingSSL { + realUsingsSSL = true + } + if c.AutoGenerateCert { + h.autogenCert = true + } + if c.AllowHTTPWithHttps { + realAllowHTTPWithHttps = true + } + starlog.Infoln(c.Name + " Listening on " + h.Addr + ":" + strconv.Itoa(h.Port)) + } + + if !realUsingsSSL { if err := h.httpserver.ListenAndServe(); err != http.ErrServerClosed { return err } return nil } - - if !h.AllowHTTPWithHttps && !h.AutoGenerateCert { - if h.httpserver.ListenAndServeTLS(h.Cert, h.Key); err != http.ErrServerClosed { - return err + /* + if !realAllowHTTPWithHttps && !realAutoGenCert { + if h.httpserver.ListenAndServeTLS(h.Cert, h.Key); err != http.ErrServerClosed { + return err + } + return nil } - return nil - } + */ var lis net.Listener - if !h.AutoGenerateCert { - lis, err = starnet.ListenTLS("tcp", fmt.Sprintf("%s:%d", h.Addr, h.Port), h.Cert, h.Key, h.AllowHTTPWithHttps) - if err != nil { - return err - } - } else { - lis, err = starnet.ListenTLSWithConfig("tcp", fmt.Sprintf("%s:%d", h.Addr, h.Port), &tls.Config{}, autoGenCert, h.AllowHTTPWithHttps) - if err != nil { - return err - } + lis, err = starnet.ListenTLSWithConfig("tcp", fmt.Sprintf("%s:%d", h.Addr, h.Port), &tls.Config{}, h.getCert, realAllowHTTPWithHttps) + if err != nil { + return err } defer lis.Close() if err := h.httpserver.Serve(lis); err != http.ErrServerClosed { @@ -97,7 +187,242 @@ var certCache = make(map[string]tls.Certificate) var toolCa *x509.Certificate var toolCaKey any -func autoGenCert(hostname string) *tls.Config { +func (h *ReverseConfig) CalcRange(r *http.Request) (int64, int64) { + var rangeStart, rangeEnd int64 = -1, -1 + ranges := r.Header.Get("Range") + if ranges == "" { + return rangeStart, rangeEnd + } + if !strings.Contains(ranges, "bytes=") { + return rangeStart, rangeEnd + } + ranges = strings.TrimPrefix(ranges, "bytes=") + data := strings.Split(ranges, "-") + if len(data) == 0 { + return rangeStart, rangeEnd + } + rangeStart, _ = strconv.ParseInt(data[0], 10, 64) + if len(data) > 1 { + rangeEnd, _ = strconv.ParseInt(data[1], 10, 64) + } + if rangeEnd == 0 { + rangeEnd = -1 + } + return rangeStart, rangeEnd +} + +func (h *ReverseConfig) BuildHeader(w http.ResponseWriter, r *http.Request, fullpath string) error { + h.SetResponseHeader(w) + if r.Method == "OPTIONS" { + w.Header().Set("Allow", "OPTIONS,GET,HEAD") + w.Header().Set("Content-Length", "0") + } + w.Header().Set("Date", strings.ReplaceAll(time.Now().UTC().Format("Mon, 2 Jan 2006 15:04:05 MST"), "UTC", "GMT")) + if staros.IsFolder(fullpath) { + return nil + } + mime, _ := mimetype.DetectFile(fullpath) + if mime == nil { + w.Header().Set("Content-Type", "application/download") + w.Header().Set("Content-Disposition", "attachment;filename="+filepath.Base(fullpath)) + w.Header().Set("Content-Transfer-Encoding", "binary") + } else { + w.Header().Set("Content-Type", mime.String()) + } + if staros.Exists(fullpath) { + finfo, err := os.Stat(fullpath) + if err != nil { + w.WriteHeader(502) + w.Write([]byte("Failed to Read " + fullpath + ",reason is " + err.Error())) + return err + } + w.Header().Set("Accept-Ranges", "bytes") + w.Header().Set("ETag", starcrypto.Md5Str([]byte(finfo.ModTime().String()))) + w.Header().Set("Last-Modified", strings.ReplaceAll(finfo.ModTime().UTC().Format("Mon, 2 Jan 2006 15:04:05 MST"), "UTC", "GMT")) + if r.Method != "OPTIONS" { + start, end := h.CalcRange(r) + if start != -1 { + if end == -1 { + w.Header().Set("Content-Length", strconv.FormatInt(finfo.Size()-start, 10)) + w.Header().Set("Content-Range", `bytes `+strconv.FormatInt(start, 10)+"-"+strconv.FormatInt(finfo.Size()-1, 10)+"/"+strconv.FormatInt(finfo.Size(), 10)) + //w.Header().Set("Content-Length", strconv.FormatInt(fpinfo.Size()-rangeStart, 10)) + } else { + w.Header().Set("Content-Length", strconv.FormatInt(end-start+1, 10)) + w.Header().Set("Content-Range", `bytes `+strconv.FormatInt(start, 10)+"-"+strconv.FormatInt(end, 10)+"/"+strconv.FormatInt(finfo.Size(), 10)) + //w.Header().Set("Content-Length", strconv.FormatInt(1+rangeEnd-rangeStart, 10)) + } + } else { + w.Header().Set("Content-Length", strconv.FormatInt(finfo.Size(), 10)) + } + } + } + return nil +} + +func (h *ReverseConfig) getFile(w http.ResponseWriter, r *http.Request, fullpath string) error { + if !staros.Exists(fullpath) { + w.WriteHeader(404) + return errors.New("File Not Found! 404 ERROR") + } + //starlog.Debugln(r.Header) + startRange, endRange := h.CalcRange(r) + fp, err := os.Open(fullpath) + if err != nil { + starlog.Errorf("Failed to open file %s,reason:%v\n", r.URL.Path, err) + w.WriteHeader(502) + w.Write([]byte(`B612 Http Server

502 SERVER ERROR


`)) + return err + } + defer fp.Close() + var transferData int + defer func() { + if transferData != 0 { + var tani string + tani = fmt.Sprintf("%v Byte", transferData) + if f64 := float64(transferData) / 1024; f64 > 1 { + tani = fmt.Sprintf("%v KiB", f64) + if f64 = float64(f64) / 1024; f64 > 1 { + tani = fmt.Sprintf("%v MiB", f64) + if f64 = float64(f64) / 1024; f64 > 1 { + tani = fmt.Sprintf("%v GiB", f64) + } + } + } + starlog.Infof("Tranfered File %s %d bytes (%s) to remote %v\n", r.URL.Path, + transferData, tani, r.RemoteAddr) + } + }() + if startRange == -1 { + w.WriteHeader(200) + for { + buf := make([]byte, 16384) + n, err := fp.Read(buf) + if n != 0 { + ns, err := w.Write(buf[:n]) + transferData += ns + if err != nil { + starlog.Errorf("Transfer File %s to Remote Failed:%v\n", fullpath, err) + return err + } + } + if err != nil { + if err == io.EOF { + break + } + starlog.Errorf("Read File %s Failed:%v\n", fullpath, err) + return err + } + } + return nil + } + starlog.Debugf("206 transfer mode for %v %v start %v end %v\n", r.URL.Path, r.RemoteAddr, startRange, endRange) + w.WriteHeader(206) + fp.Seek(int64(startRange), 0) + count := startRange + for { + buf := make([]byte, 16384) + n, err := fp.Read(buf) + if err != nil { + if err == io.EOF { + break + } + starlog.Errorf("Read File %s Failed:%v\n", r.URL.Path, err) + return err + } + if endRange == -1 { + ns, err := w.Write(buf[:n]) + transferData += ns + if err != nil { + starlog.Errorf("Transfer File %s to Remote Failed:%v\n", r.URL.Path, err) + return err + } + } else { + if count > endRange { + break + } + writeNum := n + if count+int64(n) > endRange { + writeNum = int(endRange - count + 1) + } + ns, err := w.Write(buf[0:writeNum]) + if err != nil { + starlog.Errorln("Transfer Error:", err) + return err + } + transferData += ns + count += int64(n) + } + } + return nil +} + +func (h *ReverseConfig) fileHandle(dirPath, diskpath string, writer http.ResponseWriter, request *http.Request) { + var checkUrl = request.URL.Path + if strings.HasSuffix(request.URL.Path, "/") && len(request.URL.Path) > 1 { + checkUrl = strings.TrimSuffix(request.URL.Path, "/") + } + if staros.IsFile(diskpath) { + if checkUrl != dirPath { + h.SetResponseHeader(writer) + writer.WriteHeader(404) + return + } + h.BuildHeader(writer, request, diskpath) + if request.Method == "OPTIONS" { + return + } + h.getFile(writer, request, diskpath) + return + } + reqPath := strings.TrimPrefix(checkUrl, dirPath) + realPath := filepath.Join(diskpath, reqPath) + if !staros.Exists(realPath) { + h.SetResponseHeader(writer) + writer.WriteHeader(404) + return + } + if staros.IsFile(realPath) { + h.BuildHeader(writer, request, realPath) + if request.Method == "OPTIONS" { + return + } + h.getFile(writer, request, realPath) + return + } + h.SetResponseHeader(writer) + writer.WriteHeader(403) +} + +func (h *ReverseConfig) getCert(hostname string) *tls.Config { + if h.autogenCert { + return h.autoGenCert(hostname) + } + c, ok := h.routes[hostname] + if !ok { + if _, ok := h.routes[""]; ok { + c = h.routes[""] + } else { + if len(h.routes) > 0 { + for _, v := range h.routes { + c = v + break + } + } + } + } + if c == nil { + return &tls.Config{} + } + cert, err := tls.LoadX509KeyPair(c.Cert, c.Key) + if err != nil { + starlog.Errorln("Load X509 Key Pair Error:", err) + return &tls.Config{} + } + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + } +} +func (h *ReverseConfig) autoGenCert(hostname string) *tls.Config { if cert, ok := certCache[hostname]; ok { return &tls.Config{Certificates: []tls.Certificate{cert}} } @@ -136,7 +461,7 @@ func (h *ReverseConfig) Close() error { return h.httpserver.Shutdown(ctx) } -func (h *ReverseConfig) dialTLS(ctx context.Context, network, addr string) (net.Conn, error) { +func (h *SingleReverseConfig) dialTLS(ctx context.Context, network, addr string) (net.Conn, error) { conn, err := net.DialTimeout(network, addr, time.Second*20) if err != nil { return nil, err @@ -146,8 +471,8 @@ func (h *ReverseConfig) dialTLS(ctx context.Context, network, addr string) (net. if err != nil { return nil, err } - if h.Host != "" { - host = h.Host + if h.ProxyHost != "" { + host = h.ProxyHost } cfg := &tls.Config{ServerName: host} tlsConn := tls.Client(conn, cfg) @@ -170,40 +495,78 @@ func (h *ReverseConfig) dialTLS(ctx context.Context, network, addr string) (net. } func (h *ReverseConfig) init() error { + for _, v := range h.Config { + v.init() + } + return nil +} + +func (h *SingleReverseConfig) init() error { h.proxy = make(map[string]*rp.ReverseProxy) for key, val := range h.ReverseURL { - h.proxy[key] = &rp.ReverseProxy{ - Transport: &http.Transport{DialTLSContext: h.dialTLS}, - } - h.proxy[key].ModifyResponse = h.ModifyResponse() - h.proxy[key].Director = func(req *http.Request) { - targetQuery := val.RawQuery - req.URL.Scheme = val.Scheme - if h.Host == "" { - req.Host = val.Host - } else { - req.Host = h.Host + switch tp := val.(type) { + case *url.URL: + if err := h.newReverseProxy(key, tp); err != nil { + return fmt.Errorf("new reverse proxy for %s failed: %w", key, err) } - req.URL.Host = val.Host - req.URL.Path, req.URL.RawPath = joinURLPath(val, req.URL, key) - if targetQuery == "" || req.URL.RawQuery == "" { - req.URL.RawQuery = targetQuery + req.URL.RawQuery - } else { - req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery + case string: + if h.httpPage == nil { + h.httpPage = make(map[string]string) } - h.ModifyRequest(req, val) + h.httpPage[key] = tp + if key == "=/" { + h.rootLeaf = tp + continue + } + h.router.AddLeaf(key, tp) } } return nil } +func (h *SingleReverseConfig) newReverseProxy(key string, val *url.URL) error { + h.proxy[key] = &rp.ReverseProxy{ + Transport: &http.Transport{DialTLSContext: h.dialTLS}, + } + h.proxy[key].ModifyResponse = h.ModifyResponse() + h.proxy[key].Director = func(req *http.Request) { + targetQuery := val.RawQuery + req.URL.Scheme = val.Scheme + if h.ProxyHost != "" { + req.Host = h.ProxyHost + } else { + req.Host = val.Host + } + req.URL.Host = val.Host + req.URL.Path, req.URL.RawPath = joinURLPath(val, req.URL, key) + if targetQuery == "" || req.URL.RawQuery == "" { + req.URL.RawQuery = targetQuery + req.URL.RawQuery + } else { + req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery + } + h.ModifyRequest(req, val) + } + if key == "=/" { + h.rootLeaf = h.proxy[key] + return nil + } + h.router.AddLeaf(key, h.proxy[key]) + return nil +} + func (h *ReverseConfig) SetResponseHeader(resp http.ResponseWriter) { resp.Header().Set("X-Powered-By", "B612.ME") resp.Header().Set("Server", "B612/"+version) resp.Header().Set("X-Proxy", "B612 Reverse Proxy") } -func (h *ReverseConfig) ModifyResponse() func(*http.Response) error { +func (h *SingleReverseConfig) SetResponseHeader(resp http.ResponseWriter) { + resp.Header().Set("X-Powered-By", "B612.ME") + resp.Header().Set("Server", "B612/"+version) + resp.Header().Set("X-Proxy", "B612 Reverse Proxy") +} + +func (h *SingleReverseConfig) ModifyResponse() func(*http.Response) error { return func(resp *http.Response) error { for _, v := range h.OutHeader { resp.Header.Set(v[0], v[1]) @@ -236,7 +599,7 @@ func (h *ReverseConfig) ModifyResponse() func(*http.Response) error { } } -func (h *ReverseConfig) isInCIDR(ip string) bool { +func (h *SingleReverseConfig) isInCIDR(ip string) bool { nip := net.ParseIP(strings.TrimSpace(ip)) if nip == nil { return false @@ -249,7 +612,7 @@ func (h *ReverseConfig) isInCIDR(ip string) bool { return false } -func (h *ReverseConfig) ModifyRequest(req *http.Request, remote *url.URL) { +func (h *SingleReverseConfig) ModifyRequest(req *http.Request, remote *url.URL) { switch h.IPFilterMode { case 1: req.Header.Set("X-Forwarded-For", strings.Split(req.RemoteAddr, ":")[0]) @@ -297,7 +660,7 @@ func (h *ReverseConfig) ModifyRequest(req *http.Request, remote *url.URL) { } } -func (h *ReverseConfig) GiveBasicAuth(w http.ResponseWriter) { +func (h *SingleReverseConfig) GiveBasicAuth(w http.ResponseWriter) { w.Header().Set("WWW-Authenticate", ` Basic realm="Please Enter Passwd"`) w.WriteHeader(401) w.Write([]byte(` @@ -310,7 +673,7 @@ func (h *ReverseConfig) GiveBasicAuth(w http.ResponseWriter) { `)) } -func (h *ReverseConfig) BasicAuth(w http.ResponseWriter, r *http.Request) bool { +func (h *SingleReverseConfig) BasicAuth(w http.ResponseWriter, r *http.Request) bool { if h.basicAuthPwd != "" { if len(h.protectAuthPage) != 0 { for _, v := range h.protectAuthPage { @@ -337,7 +700,7 @@ func (h *ReverseConfig) BasicAuth(w http.ResponseWriter, r *http.Request) bool { return true } -func (h *ReverseConfig) filter(w http.ResponseWriter, r *http.Request) bool { +func (h *SingleReverseConfig) filter(w http.ResponseWriter, r *http.Request) bool { if len(h.blackip) == 0 && len(h.whiteip) == 0 { return true } diff --git a/httpserver/cmd.go b/httpserver/cmd.go index 5e3a9d5..0d02f58 100644 --- a/httpserver/cmd.go +++ b/httpserver/cmd.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" "os" "os/signal" + "path" "regexp" "strconv" "strings" @@ -24,7 +25,8 @@ func init() { Cmd.Flags().StringVarP(&hooks, "hook", "H", "", "fileget hook for modify") Cmd.Flags().StringVarP(&s.port, "port", "p", "", "监听端口,http时默认80,https时默认443") Cmd.Flags().StringVarP(&s.addr, "ip", "i", "0.0.0.0", "监听ip") - Cmd.Flags().StringVarP(&s.envPath, "folder", "f", "./", "本地文件地址") + Cmd.Flags().StringSliceVarP(&s.EnvPath, "folder", "f", []string{"./"}, "本地文件地址") + Cmd.Flags().StringSliceVarP(&s.SubFolder, "sub-route", "s", []string{}, "挂载的路由,如 /sub,与folder对应一一,如果没有指定子文件夹,则默认挂载根路径") Cmd.Flags().StringVarP(&s.uploadFolder, "upload", "u", "", "文件上传文件夹路径") Cmd.Flags().BoolVarP(&daemon, "daemon", "d", false, "以后台进程运行") Cmd.Flags().StringVarP(&s.basicAuthUser, "auth", "a", "", "HTTP BASIC AUTH认证(用户名:密码)") @@ -41,7 +43,7 @@ func init() { Cmd.Flags().StringSliceVarP(&s.noListPath, "nolist", "N", []string{}, "禁止列出文件的路径,如/") Cmd.Flags().StringToStringVarP(&s.listPwd, "listpwd", "L", map[string]string{}, "列出文件的路径的密码,如/=/password") Cmd.Flags().BoolVarP(&s.listSameForFile, "list-same", "S", false, "如开启,文件的获取权限将与文件夹保持一致") - Cmd.Flags().StringVarP(&speedlimit, "speedlimit", "s", "", "限速,如1M,意思是1MB/s") + Cmd.Flags().StringVarP(&speedlimit, "speedlimit", "e", "", "限速,如1M,意思是1MB/s") Cmd.Flags().BoolVarP(&s.allowHttpWithHttps, "allow-http-with-https", "A", false, "同一个端口允许在HTTPS下使用HTTP协议") Cmd.Flags().BoolVarP(&s.autoGenCert, "autogen-cert", "G", false, "自动生成证书,此时使用--ssl-cert和--ssl-key参数无效") @@ -49,6 +51,9 @@ func init() { Cmd.Flags().StringVar(&s.background, "background", "", "背景图片地址") Cmd.Flags().StringVar(&s.mobildBackground, "mbackground", "", "移动端背景图片地址") + Cmd.Flags().StringSliceVarP(&s.blacklist, "blacklist", "b", nil, "黑名单地址,禁止访问的URL地址列表,如 /admin,/private") + Cmd.Flags().StringSliceVarP(&s.whitelist, "whitelist", "w", nil, "白名单地址,允许访问的URL地址列表,如 /public,/shared") + Cmd.Flags().MarkHidden("daeapplied") } @@ -160,6 +165,24 @@ var Cmd = &cobra.Command{ s.basicAuthUser = strings.TrimSpace(tmp[0]) s.basicAuthPwd = tmp[1] } + if len(s.blacklist) > 0 { + for _, v := range s.blacklist { + isStar := path.Base(v) == "*" + if isStar { + v = path.Dir(v) + } + s.blackpath.AddLeaf(strings.TrimSpace(v), isStar) + } + } + if len(s.whitelist) > 0 { + for _, v := range s.whitelist { + isStar := path.Base(v) == "*" + if isStar { + v = path.Dir(v) + } + s.whitepath.AddLeaf(strings.TrimSpace(v), isStar) + } + } err := s.Run(ctx) if err != nil { starlog.Errorln("Http Server Closed by Errors", err) diff --git a/httpserver/route.go b/httpserver/route.go new file mode 100644 index 0000000..804b7a5 --- /dev/null +++ b/httpserver/route.go @@ -0,0 +1,117 @@ +package httpserver + +import "strings" + +type Router struct { + Hostname string + Leaf *Leaf + leafMap map[string]*Leaf +} + +type Leaf struct { + Name string + Val any + Last *Leaf + Next map[string]*Leaf + FullPath string +} + +func NewRouter(hostname string) *Router { + return &Router{ + Hostname: hostname, + Leaf: &Leaf{ + Name: "/", + Next: make(map[string]*Leaf), + FullPath: "/", + }, + leafMap: make(map[string]*Leaf), + } +} + +func (r *Router) AddLeaf(path string, val any) { + if r.Leaf == nil { + r.Leaf = &Leaf{ + Name: "/", + Next: make(map[string]*Leaf), + FullPath: "/", + } + } + if r.leafMap == nil { + r.leafMap = make(map[string]*Leaf) + r.leafMap["/"] = r.Leaf + } + names := strings.Split(path, "/") + leaf := r.Leaf + for _, name := range names { + if name == "" { + continue + } + if leaf.Next[name] == nil { + fullPath := leaf.FullPath + "/" + name + if leaf.FullPath == "/" { + fullPath = "/" + name + } + leaf.Next[name] = &Leaf{ + Name: name, + Next: make(map[string]*Leaf), + FullPath: fullPath, + } + r.leafMap[fullPath] = leaf.Next[name] + } + leaf = leaf.Next[name] + } + leaf.Val = val +} + +func (r *Router) GetLeaf(path string) *Leaf { + return r.leafMap[path] +} + +func (r *Router) NearestLeaf(path string) *Leaf { + if path == "/" { + return r.Leaf + } + if leaf, ok := r.leafMap[path]; ok { + return leaf + } + parts := strings.Split(path, "/") + leaf := r.Leaf + for _, v := range parts { + if v == "" { + continue + } + if leaf.Next[v] == nil { + return leaf + } + leaf = leaf.Next[v] + } + return leaf +} + +func (r *Router) NearestLeafWithVal(path string) *Leaf { + if path == "/" { + return r.Leaf + } + if leaf, ok := r.leafMap[path]; ok && leaf.Val != nil { + return leaf + } + var lastValue *Leaf + parts := strings.Split(path, "/") + leaf := r.Leaf + if leaf.Val != nil { + lastValue = leaf + } + for _, v := range parts { + if v == "" { + continue + } + if leaf.Next[v] == nil { + return lastValue + } + leaf = leaf.Next[v] + if leaf.Val != nil { + lastValue = leaf + } + } + return lastValue +} diff --git a/httpserver/server.go b/httpserver/server.go index 1b1acae..cd70732 100644 --- a/httpserver/server.go +++ b/httpserver/server.go @@ -35,7 +35,8 @@ type HttpServerCfgs func(cfg *HttpServerCfg) type HttpServerCfg struct { basicAuthUser string basicAuthPwd string - envPath string + EnvPath []string + SubFolder []string uploadFolder string logpath string indexFile string @@ -61,6 +62,13 @@ type HttpServerCfg struct { background string mobildBackground string + + blackpath Router + whitepath Router + + blacklist []string + whitelist []string + envPath Router } type ServerHook struct { @@ -115,7 +123,7 @@ func NewHttpServer(addr, port, path string, opts ...HttpServerCfgs) *HttpServer HttpServerCfg: HttpServerCfg{ addr: addr, port: port, - envPath: path, + EnvPath: []string{path}, }, } for _, opt := range opts { @@ -163,9 +171,17 @@ func (h *HttpServer) Run(ctx context.Context) error { } } } - h.envPath, err = filepath.Abs(h.envPath) + for idx, val := range h.EnvPath { + h.EnvPath[idx], err = filepath.Abs(val) + if len(h.SubFolder) > 0 { + h.envPath.AddLeaf(h.SubFolder[idx], h.EnvPath[idx]) + } + } + if len(h.EnvPath) > 1 && len(h.EnvPath) != len(h.SubFolder) { + return errors.New("EnvPath and SubFolder length not match") + } if err != nil { - starlog.Errorln("Failed to get abs path of", h.envPath) + starlog.Errorln("Failed to get abs path of", h.EnvPath) return err } uconn, err := net.Dial("udp", "106.55.44.79:80") @@ -386,6 +402,82 @@ func (h *HttpServer) debugMode(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf(html, resp))) } +func (h *HttpServer) PermissionCheck(isTls string, w http.ResponseWriter, r *http.Request) bool { + checkPath := r.URL.Path + if strings.HasSuffix(checkPath, "/") && checkPath != "/" { + checkPath = strings.TrimSuffix(checkPath, "/") + } + if h.blackpath.Leaf != nil { + bp := h.blackpath.NearestLeafWithVal(checkPath) + if bp != nil { + if ppr, ok := bp.Val.(bool); ok { + if ppr || (!ppr && bp.FullPath == checkPath) { + starlog.Errorf(isTls+"%s %s From %s %s path is in black path, reject request \n", r.Method, checkPath, r.RemoteAddr, r.Header.Get("User-Agent")) + h.Page403(w) + return false + } + } + } + } + if h.whitepath.Leaf != nil { + bp := h.whitepath.NearestLeafWithVal(checkPath) + if bp == nil { + starlog.Errorf(isTls+"%s %s From %s %s path is not in white path, reject request \n", r.Method, checkPath, r.RemoteAddr, r.Header.Get("User-Agent")) + h.Page403(w) + return false + } + if ppr, ok := bp.Val.(bool); !ok { + starlog.Errorf(isTls+"%s %s From %s %s path is not in white path, reject request \n", r.Method, checkPath, r.RemoteAddr, r.Header.Get("User-Agent")) + h.Page403(w) + return false + } else { + if !ppr && bp.FullPath != checkPath { + starlog.Errorf(isTls+"%s %s From %s %s path is not in white path, reject request \n", r.Method, checkPath, r.RemoteAddr, r.Header.Get("User-Agent")) + h.Page403(w) + return false + } + } + } + return true +} +func (h *HttpServer) getEnvPath(r *http.Request) (string, string) { + if len(h.EnvPath) == 1 { + return "/", h.EnvPath[0] + } + checkPath := r.URL.Path + if strings.HasSuffix(checkPath, "/") && checkPath != "/" { + checkPath = strings.TrimSuffix(checkPath, "/") + } + if h.envPath.Leaf != nil { + leaf := h.envPath.NearestLeafWithVal(checkPath) + if leaf != nil && leaf.Val != nil { + if envPath, ok := leaf.Val.(string); ok { + return leaf.FullPath, envPath + } + } + } + return "", "" +} + +func (h *HttpServer) getStrEnvPath(r string) (string, string) { + if len(h.EnvPath) == 1 { + return "/", h.EnvPath[0] + } + checkPath := r + if strings.HasSuffix(checkPath, "/") && checkPath != "/" { + checkPath = strings.TrimSuffix(checkPath, "/") + } + if h.envPath.Leaf != nil { + leaf := h.envPath.NearestLeafWithVal(checkPath) + if leaf != nil && leaf.Val != nil { + if envPath, ok := leaf.Val.(string); ok { + return leaf.FullPath, envPath + } + } + } + return "", "" +} + func (h *HttpServer) Listen(w http.ResponseWriter, r *http.Request) { log := starlog.Std.NewFlag() log.SetShowFuncName(false) @@ -395,6 +487,10 @@ func (h *HttpServer) Listen(w http.ResponseWriter, r *http.Request) { if !h.BasicAuth(log, w, r) { return } + isTls := h.isTlsStr(r) + if !h.PermissionCheck(isTls, w, r) { + return + } path := r.URL.Path ua := r.Header.Get("User-Agent") if h.httpDebug { @@ -406,11 +502,20 @@ func (h *HttpServer) Listen(w http.ResponseWriter, r *http.Request) { h.uploadFile(w, r) return } - fullpath := filepath.Clean(filepath.Join(h.envPath, path)) - + if h.SetUpload(w, r, path) { + return + } + prefix, envPath := h.getEnvPath(r) + if envPath == "" { + log.Errorf(isTls+"%s %s From %s %s No EnvPath Found\n", r.Method, path, r.RemoteAddr, ua) + h.Page404(w) + return + } + path = "/" + strings.TrimPrefix(path, prefix) + fullpath := filepath.Clean(filepath.Join(envPath, path)) { //security check - if fullpath != h.envPath && !strings.HasPrefix(fullpath, h.envPath) { + if fullpath != envPath && !strings.HasPrefix(fullpath, envPath) { log.Warningf("Invalid Path %s IP:%s Fullpath:%s\n", path, r.RemoteAddr, fullpath) h.Page403(w) return @@ -422,11 +527,8 @@ func (h *HttpServer) Listen(w http.ResponseWriter, r *http.Request) { path = filepath.Join(path, h.indexFile) } } - isTls := h.isTlsStr(r) + now := time.Now() - if h.SetUpload(w, r, path) { - return - } switch r.Method { case "OPTIONS", "HEAD": err := h.BuildHeader(w, r, fullpath) @@ -612,7 +714,11 @@ func (h *HttpServer) getFolder(log *starlog.StarLogger, w http.ResponseWriter, r } var upload string if h.uploadFolder != "" { - upload = `Upload Web Page Is Openned!` + prefix := r.URL.Path + if prefix == "/" { + prefix = "" + } + upload = `Upload Web Page Is Openned!` } attr := r.URL.Query().Get("list") if attr != "" { @@ -926,8 +1032,14 @@ func (h *HttpServer) uploadFile(w http.ResponseWriter, r *http.Request) { } defer file.Close() starlog.Noticef("Uploading %s From %s\n", handler.Filename, r.RemoteAddr) - os.MkdirAll(filepath.Join(h.envPath, h.uploadFolder), 0755) - f, err := os.OpenFile(filepath.Join(h.uploadFolder, handler.Filename), os.O_WRONLY|os.O_CREATE, 0755) + _, envPath := h.getStrEnvPath(r.FormValue("path")) + if envPath == "" { + starlog.Errorf("No EnvPath Found For Upload File %s From %s\n", handler.Filename, r.RemoteAddr) + h.Page404(w) + return + } + os.MkdirAll(filepath.Join(envPath, h.uploadFolder), 0755) + f, err := os.OpenFile(filepath.Join(envPath, h.uploadFolder, handler.Filename), os.O_WRONLY|os.O_CREATE, 0755) if err != nil { starlog.Errorf("Open Local File %s Form %v Failed:%v\n", handler.Filename, r.RemoteAddr, err) return @@ -939,5 +1051,5 @@ func (h *HttpServer) uploadFile(w http.ResponseWriter, r *http.Request) { return } starlog.Infof("Write File %s Form %v Finished\n", handler.Filename, r.RemoteAddr) - fmt.Fprintf(w, `

%v

Return To Web Page

`, handler.Header) + //fmt.Fprintf(w, `

%v

Return To Web Page

`, handler.Header) } diff --git a/netforward/cmd.go b/netforward/cmd.go index a928578..c56f3a5 100644 --- a/netforward/cmd.go +++ b/netforward/cmd.go @@ -43,6 +43,7 @@ func init() { CmdNetforward.Flags().BoolVarP(&f.inTlsAutoGen, "in-autogen", "G", false, "auto generate input tls cert") CmdNetforward.Flags().StringSliceVar(&f.CaCerts, "ca", []string{}, "tls ca certs") CmdNetforward.Flags().BoolVarP(&f.allowNoTls, "allow-no-tls", "A", false, "allow no tls connection") + CmdNetforward.Flags().StringVarP(&f.serverName, "server-name", "H", "", "dial server name") } var CmdNetforward = &cobra.Command{ diff --git a/netforward/forward.go b/netforward/forward.go index a73e7e7..c7dd609 100644 --- a/netforward/forward.go +++ b/netforward/forward.go @@ -63,6 +63,7 @@ type NetForward struct { toolCaKey any caPool *x509.CertPool outTlsCertCache tls.Certificate + serverName string } func (n *NetForward) UdpListener() *net.UDPConn { @@ -311,11 +312,13 @@ func (n *NetForward) runTCP() error { } log.Infof("TCP Connect %s <==> %s\n", conn.RemoteAddr().String(), rmt.RemoteAddr().String()) if n.outTls { - serverName, _, _ := net.SplitHostPort(n.RemoteURI) + if n.serverName == "" { + n.serverName, _, _ = net.SplitHostPort(n.RemoteURI) + } tlsConfig := &tls.Config{ InsecureSkipVerify: n.outTlsSkipVerify, RootCAs: n.caPool, - ServerName: serverName, + ServerName: n.serverName, } if n.outTlsCert != "" && n.outTlsKey != "" { tlsConfig.Certificates = []tls.Certificate{n.outTlsCertCache} diff --git a/tls/cert.go b/tls/cert.go index 107a71e..3f2ee11 100644 --- a/tls/cert.go +++ b/tls/cert.go @@ -14,7 +14,6 @@ import ( "net" "os" "path/filepath" - "strconv" "strings" "sync" "time" @@ -27,55 +26,72 @@ var timeoutMillSec int var socks5 string var socks5Auth string var showCA bool +var skipVerify bool func init() { Cmd.Flags().BoolVarP(&hideDetail, "hide-detail", "H", false, "隐藏证书详细信息") Cmd.Flags().StringVarP(&dump, "dump", "d", "", "将证书保存到文件") - Cmd.Flags().IntVarP(&reqRawIP, "resolve-ip", "r", 0, "使用解析到的IP地址进行连接,输入数字表示使用解析到的第几个IP地址") + Cmd.Flags().IntVarP(&reqRawIP, "resolve-ip", "r", 0, "使用解析到的IP地址作为SNI进行tls连接,输入数字表示使用解析到的第几个IP地址") Cmd.Flags().IntVarP(&timeoutMillSec, "timeout", "t", 5000, "连接超时时间(毫秒)") Cmd.Flags().StringVarP(&socks5, "socks5", "p", "", "socks5代理,示例:127.0.0.1:1080") Cmd.Flags().StringVarP(&socks5Auth, "socks5-auth", "A", "", "socks5代理认证,示例:username:password") Cmd.Flags().BoolVarP(&showCA, "show-ca", "c", false, "显示CA证书") + Cmd.Flags().BoolVarP(&skipVerify, "skip-verify", "k", false, "跳过证书验证") } var Cmd = &cobra.Command{ Use: "tls", Short: "查看TLS证书信息", - Long: "查看TLS证书信息", + Long: `查看TLS证书信息 +用法: b612 tls [-d|-r|-t|-p|-A-c] [域名:端口] [域名:端口/服务器名] [域名/服务器名] +如果使用IP直接连接,对于IPv6地址需要使用方括号包裹,如[::1]:443 + +示例:b612 tls -d cert.pem example.com:443/test.example.com +含义:连接example.com:443,SNI设置为test.example.com,并将证书保存到cert.pem文件中。 + +示例:b612 tls -r 1 example.com:443 +含义:连接example.com:443,使用解析到的第一个IP地址进行连接,并且这个IP地址作为SNI进行连接 +。 +示例:b612 tls -r 1 example.com:443/example.com +含义:连接example.com:443,使用解析到的第一个IP地址进行连接,并使用example.com作为SNI进行连接。`, Run: func(cmd *cobra.Command, args []string) { for _, target := range args { - showTls(target, !hideDetail, showCA, reqRawIP, dump, time.Duration(timeoutMillSec)*time.Millisecond) + showTls(target, !hideDetail, showCA, reqRawIP, dump, skipVerify, time.Duration(timeoutMillSec)*time.Millisecond) } }, } -func showTls(target string, showDetail, showCA bool, reqRawIP int, dumpPath string, timeout time.Duration) { +func showTls(target string, showDetail, showCA bool, reqRawIP int, dumpPath string, skipVerify bool, timeout time.Duration) { var err error { - sp := strings.Split(target, ":") - if len(sp) < 2 { - target = target + ":443" - } else { - if _, err := strconv.Atoi(sp[len(sp)-1]); err != nil { - target = target + ":443" + sp := strings.Split(target, "/") + hostname, port, err := net.SplitHostPort(sp[0]) + if port == "" || err != nil { + if hostname == "" { + hostname = sp[0] } + port = "443" + sp[0] = hostname + ":443" + target = strings.Join(sp, "/") } } if timeout == 0 { timeout = 5 * time.Second } - hostname := strings.Split(target, ":")[0] - if strings.Count(target, ":") == 2 { - strs := strings.Split(target, ":") - if len(strs) != 3 { + hostname, _, _ := net.SplitHostPort(strings.Split(target, "/")[0]) + hasHostname := false + if strings.Count(target, "/") > 0 { + strs := strings.Split(target, "/") + if len(strs) != 2 { starlog.Errorln("invalid target format") return } - target = strs[0] + ":" + strs[2] + target = strs[0] hostname = strs[1] + hasHostname = true } if reqRawIP > 0 { - domain := strings.Split(target, ":")[0] + domain, port, _ := net.SplitHostPort(strings.Split(target, "/")[0]) ips, err := net.LookupIP(domain) if err != nil { starlog.Errorln("failed to resolve domain: " + err.Error()) @@ -91,8 +107,14 @@ func showTls(target string, showDetail, showCA bool, reqRawIP int, dumpPath stri if reqRawIP > len(ips) { reqRawIP = len(ips) } - target = ips[reqRawIP-1].String() + ":443" - hostname = ips[reqRawIP-1].String() + targetIp := ips[reqRawIP-1].String() + if ips[reqRawIP-1].To16() != nil { + targetIp = "[" + targetIp + "]" + } + target = targetIp + ":" + port + if !hasHostname { + hostname = ips[reqRawIP-1].String() + } starlog.Noticeln("使用解析到的IP地址进行连接:", target) } starlog.Noticef("将使用如下地址连接:%s ; ServerName: %s\n", target, hostname) @@ -135,6 +157,9 @@ func showTls(target string, showDetail, showCA bool, reqRawIP int, dumpPath stri wg.Add(1) go func() { defer wg.Done() + if !skipVerify { + return + } var conn *tls.Conn if socksDialer == nil { conn, verifyErr = tls.DialWithDialer(netDialer, "tcp", target, &tls.Config{ @@ -164,7 +189,7 @@ func showTls(target string, showDetail, showCA bool, reqRawIP int, dumpPath stri if socksDialer == nil { conn, err = tls.DialWithDialer(netDialer, "tcp", target, &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: skipVerify, ServerName: hostname, MinVersion: tls.VersionSSL30, }) @@ -180,7 +205,7 @@ func showTls(target string, showDetail, showCA bool, reqRawIP int, dumpPath stri } defer con.Close() conn = tls.Client(con, &tls.Config{ - InsecureSkipVerify: true, + InsecureSkipVerify: skipVerify, ServerName: hostname, MinVersion: tls.VersionSSL30, }) @@ -293,6 +318,7 @@ func showTls(target string, showDetail, showCA bool, reqRawIP int, dumpPath stri switch pub := c.PublicKey.(type) { case *rsa.PublicKey: fmt.Printf("RSA公钥位数: %d\n", pub.Size()*8) // RSA公钥的位数 + fmt.Printf("RSA公钥指数: %d\n", pub.E) // RSA公钥的指数 case *ecdsa.PublicKey: fmt.Printf("ECDSA Curve位数: %d\n", pub.Curve.Params().BitSize) // ECDSA公钥的位数 } @@ -316,6 +342,9 @@ func showTls(target string, showDetail, showCA bool, reqRawIP int, dumpPath stri if len(c.PermittedDNSDomains) != 0 { fmt.Printf("批准使用的DNS: %s\n", strings.Join(c.PermittedDNSDomains, ", ")) } + if len(c.ExcludedDNSDomains) != 0 { + fmt.Printf("禁止使用的DNS: %s\n", strings.Join(c.ExcludedDNSDomains, ", ")) + } if len(c.PermittedIPRanges) != 0 { ipRange := "" for _, ip := range c.PermittedIPRanges { @@ -324,12 +353,26 @@ func showTls(target string, showDetail, showCA bool, reqRawIP int, dumpPath stri ipRange = ipRange[:len(ipRange)-2] fmt.Printf("批准使用的IP: %s\n", ipRange) } + if len(c.ExcludedIPRanges) != 0 { + ipRange := "" + for _, ip := range c.ExcludedIPRanges { + ipRange += ip.String() + ", " + } + ipRange = ipRange[:len(ipRange)-2] + fmt.Printf("禁止使用的IP: %s\n", ipRange) + } if len(c.PermittedEmailAddresses) != 0 { fmt.Printf("批准使用的Email: %s\n", strings.Join(c.PermittedEmailAddresses, ", ")) } + if len(c.ExcludedEmailAddresses) != 0 { + fmt.Printf("禁止使用的Email: %s\n", strings.Join(c.ExcludedEmailAddresses, ", ")) + } if len(c.PermittedURIDomains) != 0 { fmt.Printf("批准使用的URI: %s\n", strings.Join(c.PermittedURIDomains, ", ")) } + if len(c.ExcludedURIDomains) != 0 { + fmt.Printf("禁止使用的URI: %s\n", strings.Join(c.ExcludedURIDomains, ", ")) + } fmt.Printf("证书密钥用途: %s\n", strings.Join(KeyUsageToString(c.KeyUsage), ", ")) extKeyUsage := []string{} for _, v := range c.ExtKeyUsage { @@ -367,6 +410,24 @@ func showTls(target string, showDetail, showCA bool, reqRawIP int, dumpPath stri } } fmt.Printf("证书扩展密钥用途: %s\n", strings.Join(extKeyUsage, ", ")) + if len(c.CRLDistributionPoints) > 0 { + fmt.Printf("证书CRL分发点: %s\n", strings.Join(c.CRLDistributionPoints, ",")) + } + if len(c.OCSPServer) > 0 { + fmt.Printf("证书OCSP服务器: %s\n", strings.Join(c.OCSPServer, ", ")) + } + if len(c.SubjectKeyId) > 0 { + fmt.Printf("证书主题密钥ID: %x\n", c.SubjectKeyId) + } + if len(c.AuthorityKeyId) > 0 { + fmt.Printf("证书颁发者密钥ID: %x\n", c.AuthorityKeyId) + } + if c.MaxPathLenZero { + fmt.Println("证书最大路径长度为0") + } + if c.MaxPathLen > 0 { + fmt.Printf("证书最大路径长度: %d\n", c.MaxPathLen) + } fmt.Printf("证书版本: %d\n----------\n", c.Version) //fmt.Printf("证书扩展信息: %+v\n\n----------", c.Extensions) } diff --git a/utils/cert.go b/utils/cert.go index 834b35f..ab9a217 100644 --- a/utils/cert.go +++ b/utils/cert.go @@ -6,7 +6,6 @@ import ( "crypto/ecdh" "crypto/ed25519" "crypto/elliptic" - "crypto/pbkdf2" "crypto/rand" "crypto/sha512" "crypto/tls" @@ -16,6 +15,7 @@ import ( "encoding/pem" "errors" "fmt" + "golang.org/x/crypto/pbkdf2" "io" "math/big" "net" @@ -245,12 +245,9 @@ func Encode(data []byte, key string) ([]byte, error) { if key == "" { return nil, errors.New("key is empty") } - aesKey, err := pbkdf2.Key(sha512.New, key, []byte("b612.me"), 923876, 32) - if err != nil { - return nil, fmt.Errorf("failed to generate AES key: %w", err) - } + aesKey := pbkdf2.Key([]byte(key), []byte("b612.me"), 923876, 32, sha512.New) var iv = make([]byte, 16) - _, err = io.ReadFull(rand.Reader, iv) + _, err := io.ReadFull(rand.Reader, iv) if err != nil { return nil, fmt.Errorf("failed to generate IV: %w", err) } @@ -269,10 +266,7 @@ func Decode(data []byte, key string) ([]byte, error) { if key == "" { return nil, errors.New("key is empty") } - aesKey, err := pbkdf2.Key(sha512.New, key, []byte("b612.me"), 923876, 32) - if err != nil { - return nil, fmt.Errorf("failed to generate AES key: %w", err) - } + aesKey := pbkdf2.Key([]byte(key), []byte("b612.me"), 923876, 32, sha512.New) iv := data[:16] ciphertext := data[16:] plaintext, err := starcrypto.CustomDecryptAesCFBNoBlock(ciphertext, aesKey, iv) diff --git a/utils/cert_test.go b/utils/cert_test.go index 1ccfb6f..0f8b013 100644 --- a/utils/cert_test.go +++ b/utils/cert_test.go @@ -86,7 +86,7 @@ func TestGenerateMiddleCA(t *testing.T) { MaxPathLen: 0, MaxPathLenZero: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny, x509.ExtKeyUsageServerAuth, - x509.ExtKeyUsageClientAuth}, + x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageOCSPSigning}, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageDigitalSignature, } rsa, _, err := starcrypto.GenerateRsaKey(4096)