2023-04-04 14:11:09 +08:00
|
|
|
package httpreverse
|
|
|
|
|
|
|
|
import (
|
2024-05-12 16:32:13 +08:00
|
|
|
"b612.me/apps/b612/httpreverse/rp"
|
2025-06-13 13:05:50 +08:00
|
|
|
"b612.me/apps/b612/utils"
|
2025-06-19 23:47:39 +08:00
|
|
|
"b612.me/starcrypto"
|
2023-04-04 14:11:09 +08:00
|
|
|
"b612.me/starlog"
|
2025-06-13 13:05:50 +08:00
|
|
|
"b612.me/starnet"
|
2025-06-19 23:47:39 +08:00
|
|
|
"b612.me/staros"
|
2023-04-04 14:11:09 +08:00
|
|
|
"bytes"
|
|
|
|
"context"
|
2024-02-04 13:17:47 +08:00
|
|
|
"crypto/tls"
|
2025-06-13 13:05:50 +08:00
|
|
|
"crypto/x509"
|
2023-04-04 14:11:09 +08:00
|
|
|
"encoding/base64"
|
2025-06-19 23:47:39 +08:00
|
|
|
"errors"
|
2023-04-04 14:11:09 +08:00
|
|
|
"fmt"
|
2025-06-19 23:47:39 +08:00
|
|
|
"github.com/gabriel-vasile/mimetype"
|
|
|
|
"io"
|
2023-04-04 14:11:09 +08:00
|
|
|
"io/ioutil"
|
2024-02-04 13:17:47 +08:00
|
|
|
"net"
|
2023-04-04 14:11:09 +08:00
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2025-06-19 23:47:39 +08:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2023-04-04 14:11:09 +08:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
2024-03-17 16:12:54 +08:00
|
|
|
var version = "2.1.0"
|
2023-04-04 14:11:09 +08:00
|
|
|
|
|
|
|
func (h *ReverseConfig) Run() error {
|
|
|
|
err := h.init()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
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)
|
2023-04-04 14:11:09 +08:00
|
|
|
return
|
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
writer.Write([]byte(`
|
|
|
|
<html>
|
|
|
|
<head><title>403 Forbidden</title></head>
|
|
|
|
<body>
|
|
|
|
<center><h1>403 Forbidden</h1></center>
|
|
|
|
<center><h3>You Are Not Allowed to Access This Page</h3></center>
|
|
|
|
<center><h3>Please Contact Site Administrator For Help</h3></center>
|
|
|
|
<hr><center>B612 HTTP REVERSE SERVER</center>
|
|
|
|
</body>
|
|
|
|
</html>`))
|
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
2023-04-04 14:11:09 +08:00
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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)
|
2023-04-04 14:11:09 +08:00
|
|
|
return
|
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
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 {
|
|
|
|
starlog.Errorf("<%s> Path:%s is not in the write path, reject request\n", c.Name, checkPath)
|
|
|
|
rejectWith403(writer, request)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
})
|
2023-04-04 14:11:09 +08:00
|
|
|
h.httpserver = http.Server{
|
|
|
|
Addr: fmt.Sprintf("%s:%d", h.Addr, h.Port),
|
|
|
|
Handler: &h.httpmux,
|
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
var realUsingsSSL bool
|
|
|
|
var realAllowHTTPWithHttps bool
|
|
|
|
for _, c := range h.Config {
|
|
|
|
if c.UsingSSL {
|
|
|
|
realUsingsSSL = true
|
2023-04-04 14:11:09 +08:00
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
if c.AutoGenerateCert {
|
|
|
|
h.autogenCert = true
|
|
|
|
}
|
|
|
|
if c.AllowHTTPWithHttps {
|
|
|
|
realAllowHTTPWithHttps = true
|
|
|
|
}
|
|
|
|
starlog.Infoln(c.Name + " Listening on " + h.Addr + ":" + strconv.Itoa(h.Port))
|
2023-04-04 14:11:09 +08:00
|
|
|
}
|
2025-06-13 13:05:50 +08:00
|
|
|
|
2025-06-19 23:47:39 +08:00
|
|
|
if !realUsingsSSL {
|
|
|
|
if err := h.httpserver.ListenAndServe(); err != http.ErrServerClosed {
|
2025-06-13 13:05:50 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
/*
|
|
|
|
if !realAllowHTTPWithHttps && !realAutoGenCert {
|
|
|
|
if h.httpserver.ListenAndServeTLS(h.Cert, h.Key); err != http.ErrServerClosed {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
2025-06-13 13:05:50 +08:00
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
*/
|
|
|
|
var lis net.Listener
|
|
|
|
lis, err = starnet.ListenTLSWithConfig("tcp", fmt.Sprintf("%s:%d", h.Addr, h.Port), &tls.Config{}, h.getCert, realAllowHTTPWithHttps)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2025-06-13 13:05:50 +08:00
|
|
|
}
|
|
|
|
defer lis.Close()
|
|
|
|
if err := h.httpserver.Serve(lis); err != http.ErrServerClosed {
|
2023-04-04 14:11:09 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2025-06-13 13:05:50 +08:00
|
|
|
var toolCa *x509.Certificate
|
|
|
|
var toolCaKey any
|
|
|
|
|
2025-06-19 23:47:39 +08:00
|
|
|
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(`<html><title>B612 Http Server</title><body><h1 "style="text-align: center;">502 SERVER ERROR</h1><hr ></body></html>`))
|
|
|
|
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 {
|
2025-06-20 12:04:49 +08:00
|
|
|
if tlsCfg, ok := h.hostnameTlsCache[hostname]; ok {
|
|
|
|
return tlsCfg
|
2025-06-19 23:47:39 +08:00
|
|
|
}
|
|
|
|
c, ok := h.routes[hostname]
|
|
|
|
if !ok {
|
2025-06-20 12:04:49 +08:00
|
|
|
if h.autogenCert {
|
|
|
|
return h.autoGenCert(hostname)
|
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
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{}
|
|
|
|
}
|
2025-06-20 12:04:49 +08:00
|
|
|
if c.AutoGenerateCert {
|
|
|
|
return h.autoGenCert(hostname)
|
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
cert, err := tls.LoadX509KeyPair(c.Cert, c.Key)
|
|
|
|
if err != nil {
|
|
|
|
starlog.Errorln("Load X509 Key Pair Error:", err)
|
|
|
|
return &tls.Config{}
|
|
|
|
}
|
2025-06-20 19:40:14 +08:00
|
|
|
h.Lock()
|
2025-06-20 12:04:49 +08:00
|
|
|
if h.hostnameTlsCache == nil {
|
|
|
|
h.hostnameTlsCache = make(map[string]*tls.Config)
|
|
|
|
}
|
2025-06-20 19:40:14 +08:00
|
|
|
if tlsCfg, ok := h.hostnameTlsCache[hostname]; ok {
|
|
|
|
h.Unlock()
|
|
|
|
return tlsCfg
|
|
|
|
}
|
2025-06-20 12:04:49 +08:00
|
|
|
h.hostnameTlsCache[hostname] = &tls.Config{
|
2025-06-19 23:47:39 +08:00
|
|
|
Certificates: []tls.Certificate{cert},
|
|
|
|
}
|
2025-06-20 19:40:14 +08:00
|
|
|
h.Unlock()
|
2025-06-20 12:04:49 +08:00
|
|
|
return h.hostnameTlsCache[hostname]
|
2025-06-19 23:47:39 +08:00
|
|
|
}
|
2025-06-20 12:04:49 +08:00
|
|
|
|
2025-06-19 23:47:39 +08:00
|
|
|
func (h *ReverseConfig) autoGenCert(hostname string) *tls.Config {
|
2025-06-13 13:05:50 +08:00
|
|
|
if toolCa == nil {
|
2025-06-17 13:10:35 +08:00
|
|
|
toolCa, toolCaKey = utils.ToolCert("")
|
2025-06-13 13:05:50 +08:00
|
|
|
}
|
|
|
|
cert, err := utils.GenerateTlsCert(utils.GenerateCertParams{
|
|
|
|
Country: "CN",
|
|
|
|
Organization: "B612 HTTP SERVER",
|
|
|
|
OrganizationUnit: "cert@b612.me",
|
|
|
|
CommonName: hostname,
|
|
|
|
Dns: []string{hostname},
|
|
|
|
KeyUsage: int(x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign),
|
|
|
|
ExtendedKeyUsage: []int{
|
|
|
|
int(x509.ExtKeyUsageServerAuth),
|
|
|
|
int(x509.ExtKeyUsageClientAuth),
|
|
|
|
},
|
|
|
|
IsCA: false,
|
|
|
|
StartDate: time.Now().Add(-24 * time.Hour),
|
|
|
|
EndDate: time.Now().AddDate(1, 0, 0),
|
|
|
|
Type: "RSA",
|
|
|
|
Bits: 2048,
|
|
|
|
CA: toolCa,
|
|
|
|
CAPriv: toolCaKey,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
2025-06-20 19:40:14 +08:00
|
|
|
h.Lock()
|
2025-06-20 12:04:49 +08:00
|
|
|
if h.hostnameTlsCache == nil {
|
|
|
|
h.hostnameTlsCache = make(map[string]*tls.Config)
|
|
|
|
}
|
2025-06-20 19:40:14 +08:00
|
|
|
if tlsCfg, ok := h.hostnameTlsCache[hostname]; ok {
|
|
|
|
h.Unlock()
|
|
|
|
return tlsCfg
|
|
|
|
}
|
2025-06-20 12:04:49 +08:00
|
|
|
h.hostnameTlsCache[hostname] = &tls.Config{Certificates: []tls.Certificate{cert}}
|
2025-06-20 19:40:14 +08:00
|
|
|
h.Unlock()
|
2025-06-20 12:04:49 +08:00
|
|
|
return h.hostnameTlsCache[hostname]
|
2025-06-13 13:05:50 +08:00
|
|
|
}
|
|
|
|
|
2023-04-04 14:11:09 +08:00
|
|
|
func (h *ReverseConfig) Close() error {
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
|
|
defer cancel()
|
|
|
|
return h.httpserver.Shutdown(ctx)
|
|
|
|
}
|
|
|
|
|
2025-06-19 23:47:39 +08:00
|
|
|
func (h *SingleReverseConfig) dialTLS(ctx context.Context, network, addr string) (net.Conn, error) {
|
2025-06-20 13:10:10 +08:00
|
|
|
var host string
|
2025-06-20 19:40:14 +08:00
|
|
|
if h.ProxyHost != "" && h.ProxyHost != "$user" {
|
|
|
|
host = h.ProxyHost
|
|
|
|
} else if h.ProxyHost != "" {
|
|
|
|
if val, ok := ctx.Value("realhost").(string); ok {
|
|
|
|
host = val
|
|
|
|
}
|
2025-06-20 13:10:10 +08:00
|
|
|
} else {
|
2025-06-20 19:40:14 +08:00
|
|
|
host, _, _ = net.SplitHostPort(addr)
|
2024-02-04 13:17:47 +08:00
|
|
|
}
|
2025-06-20 13:10:10 +08:00
|
|
|
conn, err := net.DialTimeout(network, addr, time.Second*20)
|
2024-02-04 13:17:47 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2025-06-20 13:10:10 +08:00
|
|
|
if host == "" {
|
|
|
|
host, _, err = net.SplitHostPort(addr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-02-04 13:17:47 +08:00
|
|
|
}
|
|
|
|
cfg := &tls.Config{ServerName: host}
|
|
|
|
tlsConn := tls.Client(conn, cfg)
|
|
|
|
if err := tlsConn.Handshake(); err != nil {
|
|
|
|
conn.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
cs := tlsConn.ConnectionState()
|
|
|
|
cert := cs.PeerCertificates[0]
|
|
|
|
|
|
|
|
// Verify here
|
|
|
|
if !h.SkipSSLVerify {
|
|
|
|
err = cert.VerifyHostname(host)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return tlsConn, nil
|
|
|
|
}
|
|
|
|
|
2025-06-20 13:10:10 +08:00
|
|
|
func (h *SingleReverseConfig) dial(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
|
|
conn, err := net.DialTimeout(network, addr, time.Second*20)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return conn, nil
|
|
|
|
}
|
|
|
|
|
2023-04-04 14:11:09 +08:00
|
|
|
func (h *ReverseConfig) init() error {
|
2025-06-19 23:47:39 +08:00
|
|
|
for _, v := range h.Config {
|
|
|
|
v.init()
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *SingleReverseConfig) init() error {
|
2024-05-12 16:32:13 +08:00
|
|
|
h.proxy = make(map[string]*rp.ReverseProxy)
|
2023-04-04 14:11:09 +08:00
|
|
|
for key, val := range h.ReverseURL {
|
2025-06-19 23:47:39 +08:00
|
|
|
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)
|
2024-05-12 16:32:13 +08:00
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
case string:
|
|
|
|
if h.httpPage == nil {
|
|
|
|
h.httpPage = make(map[string]string)
|
|
|
|
}
|
|
|
|
h.httpPage[key] = tp
|
|
|
|
if key == "=/" {
|
|
|
|
h.rootLeaf = tp
|
|
|
|
continue
|
2024-05-12 16:32:13 +08:00
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
h.router.AddLeaf(key, tp)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *SingleReverseConfig) newReverseProxy(key string, val *url.URL) error {
|
|
|
|
h.proxy[key] = &rp.ReverseProxy{
|
2025-06-20 13:10:10 +08:00
|
|
|
Transport: &http.Transport{DialTLSContext: h.dialTLS, DialContext: h.dial},
|
2025-06-19 23:47:39 +08:00
|
|
|
}
|
|
|
|
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 != "" {
|
2025-06-20 13:10:10 +08:00
|
|
|
if h.ProxyHost != "$user" {
|
|
|
|
req.Host = h.ProxyHost
|
2025-06-20 19:40:14 +08:00
|
|
|
} else {
|
|
|
|
ctx := context.WithValue(req.Context(), "realhost", req.Host)
|
|
|
|
*req = *req.WithContext(ctx)
|
2025-06-20 13:10:10 +08:00
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
} else {
|
|
|
|
req.Host = val.Host
|
2023-04-04 14:11:09 +08:00
|
|
|
}
|
2025-06-20 19:40:14 +08:00
|
|
|
req.URL.Host = val.Host
|
2025-06-19 23:47:39 +08:00
|
|
|
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)
|
2023-04-04 14:11:09 +08:00
|
|
|
}
|
2025-06-19 23:47:39 +08:00
|
|
|
if key == "=/" {
|
|
|
|
h.rootLeaf = h.proxy[key]
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
h.router.AddLeaf(key, h.proxy[key])
|
2023-04-04 14:11:09 +08:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2025-06-19 23:47:39 +08:00
|
|
|
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 {
|
2023-04-04 14:11:09 +08:00
|
|
|
return func(resp *http.Response) error {
|
|
|
|
for _, v := range h.OutHeader {
|
|
|
|
resp.Header.Set(v[0], v[1])
|
|
|
|
}
|
|
|
|
resp.Header.Del("X-Powered-By")
|
|
|
|
resp.Header.Del("Server")
|
|
|
|
resp.Header.Del("X-Proxy")
|
|
|
|
resp.Header.Set("X-Powered-By", "B612.ME")
|
|
|
|
resp.Header.Set("Server", "B612/"+version)
|
|
|
|
resp.Header.Set("X-Proxy", "B612 Reverse Proxy")
|
|
|
|
if len(h.ReplaceList) != 0 && resp.ContentLength <= 20*1024*1024 && strings.Contains(resp.Header.Get("Content-Type"), "text") {
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
strBody := string(body)
|
|
|
|
replaceCount := -1
|
|
|
|
if h.ReplaceOnce {
|
|
|
|
replaceCount = 1
|
|
|
|
}
|
|
|
|
for _, v := range h.ReplaceList {
|
|
|
|
strBody = strings.Replace(strBody, v[0], v[1], replaceCount)
|
|
|
|
}
|
|
|
|
body = []byte(strBody)
|
|
|
|
resp.Body = ioutil.NopCloser(bytes.NewReader(body))
|
|
|
|
resp.ContentLength = int64(len(body))
|
|
|
|
resp.Header.Set("Content-Length", strconv.Itoa(len(body)))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-19 23:47:39 +08:00
|
|
|
func (h *SingleReverseConfig) isInCIDR(ip string) bool {
|
2024-05-12 16:32:13 +08:00
|
|
|
nip := net.ParseIP(strings.TrimSpace(ip))
|
|
|
|
if nip == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, c := range h.CIDR {
|
|
|
|
if c.Contains(nip) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2025-06-19 23:47:39 +08:00
|
|
|
func (h *SingleReverseConfig) ModifyRequest(req *http.Request, remote *url.URL) {
|
2024-05-12 16:32:13 +08:00
|
|
|
switch h.IPFilterMode {
|
|
|
|
case 1:
|
2023-04-04 14:11:09 +08:00
|
|
|
req.Header.Set("X-Forwarded-For", strings.Split(req.RemoteAddr, ":")[0])
|
2024-05-12 16:32:13 +08:00
|
|
|
case 2:
|
2023-04-04 14:11:09 +08:00
|
|
|
xforward := strings.Split(strings.TrimSpace(req.Header.Get("X-Forwarded-For")), ",")
|
|
|
|
xforward = append(xforward, strings.Split(req.RemoteAddr, ":")[0])
|
|
|
|
req.Header.Set("X-Forwarded-For", strings.Join(xforward, ", "))
|
2024-05-12 16:32:13 +08:00
|
|
|
case 3:
|
|
|
|
var lastForwardIP string
|
|
|
|
var xforward []string
|
|
|
|
if h.FilterMustKey != "" && req.Header.Get(h.FilterMustKey) != "" {
|
|
|
|
lastForwardIP = req.Header.Get(h.FilterMustKey)
|
|
|
|
xforward = []string{lastForwardIP}
|
|
|
|
} else {
|
|
|
|
for _, ip := range append(strings.Split(strings.TrimSpace(req.Header.Get("X-Forwarded-For")), ","), strings.Split(req.RemoteAddr, ":")[0]) {
|
|
|
|
ip = strings.TrimSpace(ip)
|
|
|
|
if !h.isInCIDR(ip) {
|
|
|
|
xforward = append(xforward, ip)
|
|
|
|
lastForwardIP = ip
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if lastForwardIP == "" {
|
|
|
|
lastForwardIP = strings.Split(req.RemoteAddr, ":")[0]
|
|
|
|
}
|
|
|
|
if h.FilterXForward {
|
|
|
|
req.Header.Set("X-Forwarded-For", strings.Join(xforward, ", "))
|
|
|
|
}
|
|
|
|
if h.FilterRemoteAddr {
|
|
|
|
req.Header.Set("X-Real-IP", lastForwardIP)
|
|
|
|
}
|
|
|
|
if h.FilterSetKey != "" {
|
|
|
|
req.Header.Set(h.FilterSetKey, lastForwardIP)
|
|
|
|
}
|
2023-04-04 14:11:09 +08:00
|
|
|
}
|
2025-06-20 19:40:14 +08:00
|
|
|
if len(h.ReplaceList) > 0 {
|
|
|
|
req.Header.Set("Accept-Encoding", "deflate")
|
|
|
|
}
|
2023-04-04 14:11:09 +08:00
|
|
|
for _, v := range h.Cookie {
|
|
|
|
req.AddCookie(&http.Cookie{
|
|
|
|
Name: v[1],
|
|
|
|
Value: v[2],
|
|
|
|
Path: v[0],
|
|
|
|
})
|
|
|
|
}
|
|
|
|
for _, v := range h.InHeader {
|
|
|
|
req.Header.Set(v[0], v[1])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-19 23:47:39 +08:00
|
|
|
func (h *SingleReverseConfig) GiveBasicAuth(w http.ResponseWriter) {
|
2023-04-04 14:11:09 +08:00
|
|
|
w.Header().Set("WWW-Authenticate", ` Basic realm="Please Enter Passwd"`)
|
|
|
|
w.WriteHeader(401)
|
|
|
|
w.Write([]byte(`
|
|
|
|
<html>
|
|
|
|
<head><title>401 Authorization Required</title></head>
|
|
|
|
<body>
|
|
|
|
<center><h1>401 Authorization Required</h1></center>
|
|
|
|
<hr><center>B612 HTTP SERVER</center>
|
|
|
|
</body>
|
|
|
|
</html>`))
|
|
|
|
}
|
|
|
|
|
2025-06-19 23:47:39 +08:00
|
|
|
func (h *SingleReverseConfig) BasicAuth(w http.ResponseWriter, r *http.Request) bool {
|
2023-04-04 14:11:09 +08:00
|
|
|
if h.basicAuthPwd != "" {
|
|
|
|
if len(h.protectAuthPage) != 0 {
|
|
|
|
for _, v := range h.protectAuthPage {
|
|
|
|
if !(strings.Index(r.URL.Path, v) == 0 || strings.Contains(r.URL.RawQuery, v)) {
|
|
|
|
return true
|
|
|
|
} else {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
authHeader := strings.TrimSpace(r.Header.Get("Authorization"))
|
|
|
|
if len(authHeader) == 0 {
|
|
|
|
h.GiveBasicAuth(w)
|
|
|
|
return false
|
|
|
|
} else {
|
|
|
|
userAuth := base64.StdEncoding.EncodeToString([]byte(h.basicAuthUser + ":" + h.basicAuthPwd))
|
|
|
|
authStr := strings.Split(authHeader, " ")
|
|
|
|
if strings.TrimSpace(authStr[1]) != userAuth || strings.ToLower(strings.TrimSpace(authStr[0])) != "basic" {
|
|
|
|
h.GiveBasicAuth(w)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2025-06-19 23:47:39 +08:00
|
|
|
func (h *SingleReverseConfig) filter(w http.ResponseWriter, r *http.Request) bool {
|
2023-04-04 14:11:09 +08:00
|
|
|
if len(h.blackip) == 0 && len(h.whiteip) == 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if len(h.whiteip) != 0 {
|
|
|
|
if _, ok := h.whiteip[strings.Split(r.RemoteAddr, ":")[0]]; ok {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
for k, v := range h.whiteip {
|
|
|
|
if match, _ := IPinRange2(k, v, strings.Split(r.RemoteAddr, ":")[0]); match {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if _, ok := h.blackip[strings.Split(r.RemoteAddr, ":")[0]]; ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for k, v := range h.blackip {
|
|
|
|
if match, _ := IPinRange2(k, v, strings.Split(r.RemoteAddr, ":")[0]); match {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2024-05-12 16:32:13 +08:00
|
|
|
func joinURLPath(a, b *url.URL, hpath string) (path, rawpath string) {
|
2024-05-12 19:52:35 +08:00
|
|
|
if hpath != "/" {
|
|
|
|
b.Path = strings.TrimPrefix(b.Path, hpath)
|
|
|
|
b.RawPath = strings.TrimPrefix(b.RawPath, hpath)
|
|
|
|
}
|
2023-04-04 14:11:09 +08:00
|
|
|
if a.RawPath == "" && b.RawPath == "" {
|
|
|
|
return singleJoiningSlash(a.Path, b.Path), ""
|
|
|
|
}
|
|
|
|
// Same as singleJoiningSlash, but uses EscapedPath to determine
|
|
|
|
// whether a slash should be added
|
2024-05-12 16:32:13 +08:00
|
|
|
|
2023-04-04 14:11:09 +08:00
|
|
|
apath := a.EscapedPath()
|
|
|
|
bpath := b.EscapedPath()
|
|
|
|
|
|
|
|
aslash := strings.HasSuffix(apath, "/")
|
|
|
|
bslash := strings.HasPrefix(bpath, "/")
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case aslash && bslash:
|
|
|
|
return a.Path + b.Path[1:], apath + bpath[1:]
|
|
|
|
case !aslash && !bslash:
|
|
|
|
return a.Path + "/" + b.Path, apath + "/" + bpath
|
|
|
|
}
|
|
|
|
return a.Path + b.Path, apath + bpath
|
|
|
|
}
|
|
|
|
|
|
|
|
func singleJoiningSlash(a, b string) string {
|
|
|
|
aslash := strings.HasSuffix(a, "/")
|
|
|
|
bslash := strings.HasPrefix(b, "/")
|
|
|
|
switch {
|
|
|
|
case aslash && bslash:
|
|
|
|
return a + b[1:]
|
|
|
|
case !aslash && !bslash:
|
|
|
|
return a + "/" + b
|
|
|
|
}
|
|
|
|
return a + b
|
|
|
|
}
|