package httpserver import ( "b612.me/apps/b612/version" "b612.me/starcrypto" "b612.me/starlog" "b612.me/starnet" "b612.me/staros" "context" _ "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 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, } 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 != "" { 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 == "" { if err := server.ListenAndServe(); err != http.ErrServerClosed { return err } return nil } if err := server.ListenAndServeTLS(h.cert, h.key); err != http.ErrServerClosed { return err } return nil } 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) } } 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("%s %s From %s %s %.2fs %v\n", r.Method, path, r.RemoteAddr, ua, time.Since(now).Seconds(), err) } else { log.Infof("%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("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("%s %s From %s %s %.2fs %v\n", r.Method, path, r.RemoteAddr, ua, time.Since(now).Seconds(), err) return } log.Infof("%s %s From %s %s %.2fs\n", r.Method, path, r.RemoteAddr, ua, time.Since(now).Seconds()) default: log.Errorf("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