package httpserver import ( "b612.me/apps/b612/utils" "b612.me/apps/b612/version" "b612.me/starcrypto" "b612.me/starlog" "b612.me/starnet" "b612.me/staros" "context" "crypto/tls" "crypto/x509" _ "embed" "encoding/base64" "encoding/json" "errors" "fmt" "html/template" "io" "io/ioutil" "math" "net" "net/http" "os" "path/filepath" "strconv" "strings" "time" ) var ver = version.Version type HttpServerCfgs func(cfg *HttpServerCfg) type HttpServerCfg struct { basicAuthUser string basicAuthPwd string EnvPath []string SubFolder []string uploadFolder string logpath string indexFile string cert string key string allowHttpWithHttps bool autoGenCert bool addr string port string page404 string page403 string page401 string protectAuthPage []string disableMIME bool ctx context.Context hooks []ServerHook httpDebug bool noListPath []string listPwd map[string]string listSameForFile bool // speed limit means xx bytes/s speedlimit uint64 background string mobildBackground string blackpath Router whitepath Router blacklist []string whitelist []string envPath Router } type ServerHook struct { MatchType []string `json:"match_type"` Url string `json:"url"` Timeout int `json:"timeout"` MaxHookLength int `json:"max_hook_length"` } type HttpServer struct { HttpServerCfg } //go:embed bootstrap.css var bootStrap []byte //go:embed jquery.js var jquery []byte //go:embed upload.html var uploadPage []byte //go:embed template.html var templateHtml []byte func WithHooks(hooks []ServerHook) HttpServerCfgs { return func(cfg *HttpServerCfg) { for k, v := range hooks { if v.MaxHookLength == 0 { hooks[k].MaxHookLength = 1024 * 1024 } } cfg.hooks = hooks } } func WithTLSCert(cert, key string) HttpServerCfgs { return func(cfg *HttpServerCfg) { cfg.key = key cfg.cert = cert } } func WithUploadFolder(path string) HttpServerCfgs { return func(cfg *HttpServerCfg) { cfg.uploadFolder = path } } func NewHttpServer(addr, port, path string, opts ...HttpServerCfgs) *HttpServer { var server = HttpServer{ HttpServerCfg: HttpServerCfg{ addr: addr, port: port, EnvPath: []string{path}, }, } for _, opt := range opts { opt(&server.HttpServerCfg) } return &server } func (h *HttpServer) Run(ctx context.Context) error { h.ctx = ctx server := http.Server{ Addr: h.addr + ":" + h.port, Handler: h, ConnContext: func(ctx context.Context, c net.Conn) context.Context { switch conn := c.(type) { case *tls.Conn: return context.WithValue(ctx, "istls", true) case *starnet.Conn: return context.WithValue(ctx, "istls", conn) } return context.WithValue(ctx, "istls", false) }, } go func() { select { case <-h.ctx.Done(): ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() server.Shutdown(ctx) } }() if h.logpath != "" && starlog.GetWriter() == nil { starlog.SetLogFile(h.logpath, starlog.Std, true) } netcards, err := net.Interfaces() if err == nil { for _, v := range netcards { if strings.Contains(v.Flags.String(), "up") { addrs, err := v.Addrs() if err == nil { for _, ip := range addrs { starlog.Cyan("Name:%s\tIP:%s\n", v.Name, ip) } } } } } 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) return err } uconn, err := net.Dial("udp", "106.55.44.79:80") if err == nil { schema := "http://" if h.cert != "" || h.autoGenCert { schema = "https://" } starlog.Infof("Visit: %s%s:%s\n", schema, uconn.LocalAddr().(*net.UDPAddr).IP.String(), h.port) uconn.Close() } starlog.Infoln("Listening on " + h.addr + ":" + h.port) if h.cert == "" && !h.autoGenCert { if err := server.ListenAndServe(); err != http.ErrServerClosed { return err } return nil } if !h.allowHttpWithHttps && !h.autoGenCert { if err := server.ListenAndServeTLS(h.cert, h.key); err != http.ErrServerClosed { return err } return nil } var lis net.Listener if !h.autoGenCert { lis, err = starnet.ListenTLS("tcp", h.addr+":"+h.port, h.cert, h.key, h.allowHttpWithHttps) if err != nil { return err } } else { lis, err = starnet.ListenTLSWithConfig("tcp", h.addr+":"+h.port, &tls.Config{}, autoGenCert, h.allowHttpWithHttps) if err != nil { return err } } defer lis.Close() if err := server.Serve(lis); err != http.ErrServerClosed { return err } return nil } var certCache = make(map[string]tls.Certificate) var toolCa *x509.Certificate var toolCaKey any func autoGenCert(hostname string) *tls.Config { if cert, ok := certCache[hostname]; ok { return &tls.Config{Certificates: []tls.Certificate{cert}} } 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 } certCache[hostname] = cert return &tls.Config{Certificates: []tls.Certificate{cert}} } func (h *HttpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.Listen(w, r) } func (h *HttpServer) Page404(w http.ResponseWriter) { w.WriteHeader(404) if h.page404 != "" { data, err := os.ReadFile(h.page404) if err == nil { w.Write(data) return } } w.Write([]byte(`
" + r.Method + " " + r.URL.Path + "
" resp += "query: " + r.URL.RawQuery + "
" resp += "fragment: " + r.URL.Fragment + "
" resp += "FullUrl: " + r.URL.String() + "
" resp += "%s:%s
", k, v) } resp += "%s:%s
", key, v) } } resp += "%s:%s
", c.Name, c.Value) } resp += "" + r.RemoteAddr + "
" resp += "" + r.Proto + "
" 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) log.SetShowOriginFile(false) w.Header().Set("X-Powered-By", "B612.ME") w.Header().Set("Server", "B612/"+ver) 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 { log.Infof("debug mode:%s %s From %s %s\n", r.Method, path, r.RemoteAddr, ua) h.debugMode(w, r) return } if h.uploadFolder != "" && path == "/recv" && len(r.URL.Query()["upload"]) != 0 { h.uploadFile(w, r) return } 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 != envPath && !strings.HasPrefix(fullpath, envPath) { log.Warningf("Invalid Path %s IP:%s Fullpath:%s\n", path, r.RemoteAddr, fullpath) h.Page403(w) return } } if h.indexFile != "" && staros.IsFolder(fullpath) { if staros.Exists(filepath.Join(fullpath, h.indexFile)) { fullpath = filepath.Join(fullpath, h.indexFile) path = filepath.Join(path, h.indexFile) } } now := time.Now() switch r.Method { case "OPTIONS", "HEAD": err := h.BuildHeader(w, r, fullpath) if err != nil { log.Warningf(isTls+"%s %s From %s %s %.2fs %v\n", r.Method, path, r.RemoteAddr, ua, time.Since(now).Seconds(), err) } else { log.Infof(isTls+"%s %s From %s %s %.2fs \n", r.Method, path, r.RemoteAddr, ua, time.Since(now).Seconds()) } case "GET": err := h.BuildHeader(w, r, fullpath) if err != nil { log.Warningf(isTls+"GET Header Build Failed Path:%s IP:%s Err:%v\n", path, r.RemoteAddr, err) } err = h.ResponseGet(log, w, r, fullpath) if err != nil { log.Warningf(isTls+"%s %s From %s %s %.2fs %v\n", r.Method, path, r.RemoteAddr, ua, time.Since(now).Seconds(), err) return } log.Infof(isTls+"%s %s From %s %s %.2fs\n", r.Method, path, r.RemoteAddr, ua, time.Since(now).Seconds()) default: log.Errorf(isTls+"Invalid %s %s From %s %s %.2fs\n", r.Method, path, r.RemoteAddr, ua, time.Since(now).Seconds()) return } } func (h *HttpServer) 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 *HttpServer) BuildHeader(w http.ResponseWriter, r *http.Request, fullpath string) error { 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 := h.MIME(fullpath) if h.disableMIME || mime == "" { 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) } 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" { if _, ok := h.willHook(fullpath); ok { return nil } 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 *HttpServer) willHook(fullpath string) (ServerHook, bool) { finfo, err := os.Stat(fullpath) if err != nil { return ServerHook{}, false } if finfo.Size() < 1024*1024*10 && len(h.hooks) > 0 { ext := h.GetExt(fullpath) for _, hk := range h.hooks { for _, e := range hk.MatchType { if e == ext { return hk, true } } } } return ServerHook{}, false } func (h *HttpServer) ResponseGet(log *starlog.StarLogger, w http.ResponseWriter, r *http.Request, fullpath string) error { if staros.IsFolder(fullpath) { if len(h.listPwd) != 0 { for k, v := range h.listPwd { if strings.HasPrefix(r.URL.Path, k) { if r.URL.Query().Get("list") == v { return h.getFolder(log, w, r, fullpath) } } } } if len(h.noListPath) != 0 { for _, v := range h.noListPath { if strings.HasPrefix(r.URL.Path, v) { h.Page403(w) return nil } } } return h.getFolder(log, w, r, fullpath) } if !h.listSameForFile { return h.getFile(log, w, r, fullpath) } if len(h.listPwd) != 0 { for k, v := range h.listPwd { if strings.HasPrefix(r.URL.Path, k) { if r.URL.Query().Get("list") == v { return h.getFile(log, w, r, fullpath) } } } } if len(h.noListPath) != 0 { for _, v := range h.noListPath { if strings.HasPrefix(r.URL.Path, v) { h.Page403(w) return nil } } } return h.getFile(log, w, r, fullpath) } type FileData struct { Attr string `json:"attr"` Name string `json:"name"` Modified string `json:"modified"` Size int64 `json:"size"` Type string `json:"type"` } func (h *HttpServer) getFolder(log *starlog.StarLogger, w http.ResponseWriter, r *http.Request, fullpath string) error { dir, err := ioutil.ReadDir(fullpath) if err != nil { log.Errorf("Read Folder %s failed:%v\n", fullpath, err) w.WriteHeader(403) if r.Method == "HEAD" { return err } w.Write([]byte("%v