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 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 } 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: 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) } } } } } h.envPath, err = filepath.Abs(h.envPath) 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) 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 } 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 } fullpath := filepath.Clean(filepath.Join(h.envPath, path)) { //security check if fullpath != h.envPath && !strings.HasPrefix(fullpath, h.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) } } 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) 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