package httpserver import ( "b612.me/starcrypto" "b612.me/starlog" "b612.me/starnet" "b612.me/staros" "context" _ "embed" "encoding/base64" "errors" "fmt" "io" "io/ioutil" "math" "net" "net/http" "os" "path/filepath" "strconv" "strings" "time" ) var version = "2.1.0" 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 } 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 var htmlTitle string = ` B612 Http Server %s

B612 Http Server - %s


%s

%s ` var htmlTail = `
Name Modified Size Type

	

B612.Me © Apache 2.0 License

` 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.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(`B612 Http Server

404 NOT FOUND


`)) } func (h *HttpServer) Page403(w http.ResponseWriter) { w.WriteHeader(403) if h.page403 != "" { data, err := os.ReadFile(h.page403) if err == nil { w.Write(data) return } } w.Write([]byte(`B612 Http Server

403 Forbidden


`)) } func (h *HttpServer) Page401(w http.ResponseWriter) { w.WriteHeader(401) if h.page401 != "" { data, err := os.ReadFile(h.page401) if err == nil { w.Write(data) return } } w.Write([]byte(` 401 Authorization Required

401 Authorization Required


B612 HTTP SERVER
`)) } func (h *HttpServer) GiveBasicAuth(w http.ResponseWriter) { w.Header().Set("WWW-Authenticate", ` Basic realm="Please Enter Passwd"`) h.Page401(w) } func (h *HttpServer) BasicAuth(log *starlog.StarLogger, 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 { log.Noticeln("No Authed! Get Path is", r.URL.Path, r.RemoteAddr) 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" { log.Noticeln("Auth Failed! Get Path is", r.URL.Path, r.RemoteAddr, "pwd enter is", authHeader) h.GiveBasicAuth(w) return false } log.Infof("Path %s Authoried by %s:%s\n", r.URL.Path, r.RemoteAddr, authHeader) } } return true } func (h *HttpServer) SetUpload(w http.ResponseWriter, r *http.Request, path string) bool { if h.uploadFolder != "" { if r.URL.Query().Get("bootstrap") == "true" { w.Header().Set("Content-Type", "text/css") w.Write(bootStrap) return true } if r.URL.Query().Get("jquery") == "true" { w.Header().Set("Content-Type", "application/javascript") w.Write(jquery) return true } if len(r.URL.Query()["upload"]) != 0 { w.Header().Set("Content-Type", "text/html") w.Write(uploadPage) return true } } return false } func (h *HttpServer) debugMode(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") w.WriteHeader(200) html := `B612 Http Server

Debug Mode


%s` resp := "

Url

" resp += "

" + r.Method + " " + r.URL.Path + "

" resp += "

query: " + r.URL.RawQuery + "

" resp += "

fragment: " + r.URL.Fragment + "

" resp += "

FullUrl: " + r.URL.String() + "

" resp += "

Query

" for k, v := range r.URL.Query() { resp += fmt.Sprintf("

%s:%s

", k, v) } resp += "

Header

" for key, val := range r.Header { for _, v := range val { resp += fmt.Sprintf("

%s:%s

", key, v) } } resp += "

Cookie

" for _, c := range r.Cookies() { resp += fmt.Sprintf("

%s:%s

", c.Name, c.Value) } resp += "

RemoteAddr

" resp += "

" + r.RemoteAddr + "

" resp += "

Proto

" 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/"+version) 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 rangeStart, rangeEnd = -1, -1 for k, v := range r.Header { if strings.ToLower(k) == "range" { if strings.Contains(v[0], "bytes=") { v[0] = strings.Replace(v[0], "bytes=", "", -1) } else { continue } data := strings.Split(v[0], "-") if len(data) == 0 { break } rangeStart, _ = strconv.ParseInt(data[0], 10, 64) if len(data) > 1 { rangeEnd, _ = strconv.ParseInt(data[1], 10, 64) } //w.WriteHeader(206) //206 支持断点续传 break } } 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 } w.Header().Set("Content-Length", strconv.FormatInt(finfo.Size(), 10)) start, end := h.CalcRange(r) if start != -1 { if end == -1 { 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-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)) } } } } 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) { return h.getFolder(log, w, r, fullpath) } return h.getFile(log, w, r, fullpath) } 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("

Cannot Access!

")) } if r.Method != "GET" { return nil } var upload string if h.uploadFolder != "" { upload = `Upload Web Page Is Openned!` } w.Write([]byte(fmt.Sprintf(htmlTitle, r.URL.Path, version, "Index of "+r.URL.Path, upload))) if r.URL.Path != "/" { p := r.URL.Path if p[len(p)-1:] != "/" { p += "/" } w.Write([]byte(fmt.Sprintf(`%s%s%s%s`, p+"..", "../", "-", "-", "上层文件夹"))) } if r.URL.Path == "/" { r.URL.Path = "" } else if r.URL.Path[len(r.URL.Path)-1:] == "/" { r.URL.Path = r.URL.Path[0 : len(r.URL.Path)-1] } for _, v := range dir { if v.Name() != "." || v.Name() != ".." { if !v.IsDir() { w.Write([]byte(fmt.Sprintf(`%s%s%s%s`, r.URL.Path+"/"+v.Name(), v.Name(), v.ModTime().Format("2006-01-02 15:04:05"), fmt.Sprintf("%d (%s)", v.Size(), h.trimSize(v.Size())), h.FileType(v.Name())))) } else { w.Write([]byte(fmt.Sprintf(`%s%s%s%s`, r.URL.Path+"/"+v.Name(), v.Name()+"/", v.ModTime().Format("2006-01-02 15:04:05"), "-", "文件夹"))) } } } w.Write([]byte(htmlTail)) return nil } func (h *HttpServer) getFile(log *starlog.StarLogger, w http.ResponseWriter, r *http.Request, fullpath string) error { if !staros.Exists(fullpath) { h.Page404(w) 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 { log.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) } } } log.Infof("Tranfered File %s %d bytes (%s) to remote %v\n", r.URL.Path, transferData, tani, r.RemoteAddr) } }() if startRange == -1 { hook, needCurl := h.willHook(fullpath) if !needCurl { w.WriteHeader(200) for { buf := make([]byte, 1048576) n, err := fp.Read(buf) if n != 0 { ns, err := w.Write(buf[0:n]) transferData += ns if err != nil { log.Errorf("Transfer File %s to Remote Failed:%v\n", fullpath, err) return err } } if err != nil { if err == io.EOF { break } log.Errorln("Read File %s Failed:%v\n", fullpath, err) return err } } return nil } data, err := os.ReadFile(fullpath) if err != nil { w.WriteHeader(502) w.Write([]byte(`B612 Http Server

502 SERVER ERROR


`)) log.Errorf("Read File %s Failed:%v\n", fullpath, err) return err } b64 := base64.StdEncoding.EncodeToString(data) req, err := starnet.Curl(starnet.NewRequests(hook.Url, starnet.BuildPostForm(map[string]string{ "data": b64, "ip": r.RemoteAddr, }), "POST", starnet.WithTimeout(time.Duration(hook.Timeout)*time.Millisecond))) if err != nil || len(req.RecvData) == 0 { w.Header().Set("Content-Length", strconv.Itoa(len(data))) w.WriteHeader(200) ns, err := w.Write(data) transferData += ns if err != nil { log.Errorf("Transfer File %s to Remote Failed:%v\n", fullpath, err) return err } return nil } w.WriteHeader(200) w.Header().Set("Content-Length", strconv.Itoa(len(req.RecvData))) ns, err := w.Write(req.RecvData) transferData += ns if err != nil { log.Errorf("Transfer File %s to Remote Failed:%v\n", fullpath, err) return err } return nil } log.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, 1048576) n, err := fp.Read(buf) if err != nil { if err == io.EOF { break } log.Errorf("Read File %s Failed:%v\n", r.URL.Path, err) return err } if endRange == -1 { ns, err := w.Write(buf[0:n]) transferData += ns if err != nil { log.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]) transferData += ns if err != nil { log.Errorln("Transfer Error:", err) return err } count += int64(n) } } return nil } func (h *HttpServer) trimSize(size int64) string { var tani string tani = fmt.Sprintf("%v Byte", size) if f64 := float64(size) / 1024; f64 > 1 { tani = fmt.Sprintf("%.3f KiB", math.Trunc(f64*1e3+0.5)*1e-3) if f64 = float64(f64) / 1024; f64 > 1 { tani = fmt.Sprintf("%.3f MiB", math.Trunc(f64*1e3+0.5)*1e-3) if f64 = float64(f64) / 1024; f64 > 1 { tani = fmt.Sprintf("%.3f GiB", math.Trunc(f64*1e3+0.5)*1e-3) } } } return tani } func (h *HttpServer) uploadFile(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(200) w.Write([]byte("USE POST METHOD!")) return } r.ParseMultipartForm(10485760) file, handler, err := r.FormFile("victorique") if err != nil { starlog.Errorf("Parse File From Form Failed:%v\n", err) w.WriteHeader(502) w.Write([]byte(err.Error())) return } defer file.Close() starlog.Noticef("Uploading %s From %s\n", handler.Filename, r.RemoteAddr) os.MkdirAll(filepath.Join(h.envPath, h.uploadFolder), 0755) f, err := os.OpenFile(filepath.Join(h.uploadFolder, handler.Filename), os.O_WRONLY|os.O_CREATE, 0755) if err != nil { starlog.Errorf("Open Local File %s Form %v Failed:%v\n", handler.Filename, r.RemoteAddr, err) return } defer f.Close() _, err = io.Copy(f, file) if err != nil { starlog.Errorf("Write File %s Form %v Failed:%v\n", handler.Filename, r.RemoteAddr, err) return } starlog.Infof("Write File %s Form %v Finished\n", handler.Filename, r.RemoteAddr) fmt.Fprintf(w, `

%v

Return To Web Page

`, handler.Header) }