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
Name |
Modified |
Size |
Type |
`
var htmlTail = `
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 Server404 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 Server403 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 ServerDebug 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 Server502 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 Server502 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
`, handler.Header)
}