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" ) var version = "2.1.0" func (h *ReverseConfig) Run() error { err := h.init() if err != nil { return err } 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 } 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 { 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) }) h.httpserver = http.Server{ Addr: fmt.Sprintf("%s:%d", h.Addr, h.Port), Handler: &h.httpmux, } 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 !realAllowHTTPWithHttps && !realAutoGenCert { if h.httpserver.ListenAndServeTLS(h.Cert, h.Key); err != http.ErrServerClosed { return err } return nil } */ 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 } defer lis.Close() if err := h.httpserver.Serve(lis); err != http.ErrServerClosed { return err } return nil } var toolCa *x509.Certificate var toolCaKey any 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 tlsCfg, ok := h.hostnameTlsCache[hostname]; ok { return tlsCfg } c, ok := h.routes[hostname] if !ok { if h.autogenCert { return h.autoGenCert(hostname) } 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{} } if c.AutoGenerateCert { return h.autoGenCert(hostname) } cert, err := tls.LoadX509KeyPair(c.Cert, c.Key) if err != nil { starlog.Errorln("Load X509 Key Pair Error:", err) return &tls.Config{} } h.Lock() if h.hostnameTlsCache == nil { h.hostnameTlsCache = make(map[string]*tls.Config) } if tlsCfg, ok := h.hostnameTlsCache[hostname]; ok { h.Unlock() return tlsCfg } h.hostnameTlsCache[hostname] = &tls.Config{ Certificates: []tls.Certificate{cert}, } h.Unlock() return h.hostnameTlsCache[hostname] } func (h *ReverseConfig) autoGenCert(hostname string) *tls.Config { if toolCa == nil { toolCa, toolCaKey = utils.ToolCert("") } 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 } h.Lock() if h.hostnameTlsCache == nil { h.hostnameTlsCache = make(map[string]*tls.Config) } if tlsCfg, ok := h.hostnameTlsCache[hostname]; ok { h.Unlock() return tlsCfg } h.hostnameTlsCache[hostname] = &tls.Config{Certificates: []tls.Certificate{cert}} h.Unlock() return h.hostnameTlsCache[hostname] } func (h *ReverseConfig) Close() error { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() return h.httpserver.Shutdown(ctx) } func (h *SingleReverseConfig) dialTLS(ctx context.Context, network, addr string) (net.Conn, error) { var host string if h.ProxyHost != "" && h.ProxyHost != "$user" { host = h.ProxyHost } else if h.ProxyHost != "" { if val, ok := ctx.Value("realhost").(string); ok { host = val } } else { host, _, _ = net.SplitHostPort(addr) } conn, err := net.DialTimeout(network, addr, time.Second*20) if err != nil { return nil, err } if host == "" { host, _, err = net.SplitHostPort(addr) if err != nil { return nil, err } } 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 } 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 } 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 { 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) } case string: if h.httpPage == nil { h.httpPage = make(map[string]string) } 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, DialContext: h.dial}, } 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 != "" { if h.ProxyHost != "$user" { req.Host = h.ProxyHost } else { ctx := context.WithValue(req.Context(), "realhost", req.Host) *req = *req.WithContext(ctx) } } 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 *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]) } 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 } } func (h *SingleReverseConfig) isInCIDR(ip string) bool { nip := net.ParseIP(strings.TrimSpace(ip)) if nip == nil { return false } for _, c := range h.CIDR { if c.Contains(nip) { return true } } return false } 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]) case 2: 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, ", ")) 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) } } if len(h.ReplaceList) > 0 { req.Header.Set("Accept-Encoding", "deflate") } 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]) } } func (h *SingleReverseConfig) GiveBasicAuth(w http.ResponseWriter) { w.Header().Set("WWW-Authenticate", ` Basic realm="Please Enter Passwd"`) w.WriteHeader(401) w.Write([]byte(` 401 Authorization Required

401 Authorization Required


B612 HTTP SERVER
`)) } func (h *SingleReverseConfig) BasicAuth(w http.ResponseWriter, r *http.Request) bool { 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 } func (h *SingleReverseConfig) filter(w http.ResponseWriter, r *http.Request) bool { 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 } func joinURLPath(a, b *url.URL, hpath string) (path, rawpath string) { if hpath != "/" { b.Path = strings.TrimPrefix(b.Path, hpath) b.RawPath = strings.TrimPrefix(b.RawPath, hpath) } 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 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 }