c5_labsci/zciyon/web.go
2026-01-27 00:52:00 +08:00

397 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
=================================================================================
* License: GPL-2.0 license
* Author: 众产® https://ciy.cn/code
* Version: 0.1.3
=================================================================================
Web服务器 NewCiyWebServer
=================================================================================
=================================================================================
示例:
=================================================================================
Nginx配置示例:
=================================================================================
*/
package zciyon
import (
"context"
"fmt"
"net"
"net/http"
"net/http/fcgi"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
)
type CiyWebServer struct {
pathfn map[string]map[string]func(http.ResponseWriter, *http.Request) bool
mock func(http.ResponseWriter, *http.Request) error //Mock回调
phpproxy *httputil.ReverseProxy //PHP代理服务器
exts map[string]int //静态服务器允许的扩展名,不设置全部允许
}
type contextKey string
var GhttpKey contextKey = contextKey("ciy")
type RequestContext struct {
CiyAuth string
}
//在根上建立HandleFunc
//
func NewCiyWebServer() (*CiyWebServer, error) {
web := &CiyWebServer{}
web.exts = map[string]int{}
web.pathfn = map[string]map[string]func(http.ResponseWriter, *http.Request) bool{}
http.HandleFunc("/", web.WithRequestContext(web.runMainPath))
http.HandleFunc("/ws/", web.runWebsocket)
return web, nil
}
func (web *CiyWebServer) WithRequestContext(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), GhttpKey, &RequestContext{
CiyAuth: "",
})
next(w, r.WithContext(ctx))
}
}
func (thos *CiyWebServer) registerPath(path, fnname string, fn func(http.ResponseWriter, *http.Request) bool) {
_, ok := thos.pathfn[path]
if ok {
thos.pathfn[path][fnname] = fn
} else {
thos.pathfn[path] = map[string]func(http.ResponseWriter, *http.Request) bool{fnname: fn}
}
}
func (thos *CiyWebServer) SetExt(extstr string) { //允许的静态文件类型 .jpg;.png。不设置全部允许
exts := strings.Split(extstr, ";")
for _, ext := range exts {
ext = strings.ToLower(strings.TrimSpace(ext))
if !strings.HasPrefix(ext, ".") {
continue
}
thos.exts[ext] = 1
}
}
func (thos *CiyWebServer) runWebsocket(w http.ResponseWriter, r *http.Request) {
funname := GetQuery("func", r)
Clog("websocket", funname)
clsmothod := strings.Split(funname, ".")
if len(clsmothod) != 2 {
Clog("websocket", fmt.Sprintf("func format error:%v", funname))
return
}
for path := range thos.pathfn {
if path == clsmothod[0] {
for fnname, fn := range thos.pathfn[path] {
if clsmothod[1] == fnname {
fn(w, r)
return
}
}
Clog("websocket", fmt.Sprintf("RouterFunc not find mothod[%v] in [%v]", clsmothod[1], clsmothod[0]))
return
}
}
Clog("RouterFunc not find class:%v", clsmothod[0])
}
func (thos *CiyWebServer) runMainPath(w http.ResponseWriter, r *http.Request) {
Clog("root", r.URL.Path)
//将大于n毫秒的请求保存到log
//可以统计每个path.func的请求次数用时
rets := make([]string, 0)
if err := thos.execFunc(w, r); err == nil {
return
} else {
rets = append(rets, err.Error())
}
if thos.mock != nil {
if err := thos.execMock(w, r); err == nil {
return
} else {
rets = append(rets, err.Error())
}
}
if thos.phpproxy != nil {
if err := thos.execPHP(w, r); err == nil {
return
} else {
rets = append(rets, err.Error())
}
}
if CiyRunMode == CIYRUN_DEV {
ErrJSON(w, strings.Join(rets, "; "), nil)
} else {
http.NotFound(w, r)
}
}
func (thos *CiyWebServer) execMock(w http.ResponseWriter, r *http.Request) error {
Clog("execMock", r.URL.Path)
//找文件或数据库mocks若有返回。
return thos.mock(w, r)
}
func (thos *CiyWebServer) execPHP(w http.ResponseWriter, r *http.Request) error {
Clog("execPHP", r.URL.Path)
//找本地goc文件若有转发请求。
//找到 func指向的.php文件是否存在打开文件检查是否存在public static function json_xxx() static function sse_xxx(
thos.phpproxy.ServeHTTP(w, r)
return nil
}
func (thos *CiyWebServer) execFunc(w http.ResponseWriter, r *http.Request) error {
defer func() {
if rc := recover(); rc != nil {
funname := r.URL.Path
if funname == "" {
funname = GetQuery("sse", r)
}
errmsg := fmt.Sprintf("%v", rc)
Log.Error(funname, errmsg)
ErrJSON(w, errmsg)
}
}()
Clog("execFunc", r.URL.Path)
clsmothod := strings.Split(r.URL.Path, ".")
if len(clsmothod) != 2 {
return fmt.Errorf("RouterFunc format error:%v", r.URL.Path)
}
for path := range thos.pathfn {
if path == clsmothod[0] {
for fnname, fn := range thos.pathfn[path] {
if clsmothod[1] == fnname {
sttime := Timems()
boo := fn(w, r)
CiyVars.Func_runms += Timems() - sttime
if boo {
CiyVars.Func_succ++
} else {
CiyVars.Func_fail++
}
return nil
}
}
return fmt.Errorf("RouterFunc not find mothod[%v] in [%v]", clsmothod[1], clsmothod[0])
}
}
return fmt.Errorf("RouterFunc not find class:%v", clsmothod[0])
}
func (thos *CiyWebServer) runStatic(w http.ResponseWriter, r *http.Request) bool {
Clog("runStatic", r.URL.Path)
//模拟nginx功能
//判断文件扩展名是否允许
//判断文件@参数,如果无缓存则压缩图片,存到文件缓存,返回文件缓存地址
//文件在10分钟内被访问超过n次则进入内存缓存。
//缓存达到容量后自动清理10分钟内访问次数最少的几个文件缓存
//如果文件扩展名包含
file := CiyWebDir + r.URL.Path
if len(thos.exts) > 0 {
ext := strings.ToLower(filepath.Ext(file))
if ext != "" {
if _, ok := thos.exts[ext]; !ok {
http.NotFound(w, r)
return false
}
}
}
err := FileExist(file)
if err != nil {
http.NotFound(w, r)
}
http.ServeFile(w, r, file)
return true
}
func (thos *CiyWebServer) RouterStatic(path string) { //允许的静态目录
thos.registerPath(path, "", thos.runStatic)
}
func (thos *CiyWebServer) RouterFunc(path string, fnmap map[string]map[string]func(http.ResponseWriter, *http.Request) bool) {
for subpath, fnmap2 := range fnmap {
for funstr, fn := range fnmap2 {
thos.registerPath(path+"/"+subpath, funstr, fn)
}
}
}
func (thos *CiyWebServer) RouterHandle(path string, fn func(http.ResponseWriter, *http.Request)) {
http.HandleFunc(path, fn)
}
func (thos *CiyWebServer) SetPHP(host string) {
if host == "" {
return
}
if ind := strings.Index(host, "://"); ind > -1 {
host = host[ind+3:]
}
thos.phpproxy = httputil.NewSingleHostReverseProxy(
&url.URL{
Scheme: "http",
Host: host,
},
)
_, err := http.Get("http://" + host)
if err != nil {
Clog("WebRun PHP err:", err)
return
}
Clog("WebRun support PHP:", host)
}
func (thos *CiyWebServer) execMockFile(w http.ResponseWriter, r *http.Request) error {
funname := r.URL.Path
mockfile := CiyWebDir + "/ud/mock/" + funname + ".json"
err := FileExist(mockfile)
if err != nil {
return err
}
http.ServeFile(w, r, mockfile)
return nil
}
func (thos *CiyWebServer) SetMockFile() {
if thos.mock != nil {
return
}
thos.mock = thos.execMockFile
Clog("WebRun support Mock File")
}
func (thos *CiyWebServer) SetMockFn(fn func(http.ResponseWriter, *http.Request) error) {
if thos.mock != nil {
return
}
thos.mock = fn
Clog("WebRun support Mock Callback")
}
func (thos *CiyWebServer) Run(mode, ipsk string) {
if mode == "fastcgi" {
listener, err := net.Listen("tcp", ipsk)
if err != nil {
Clog("WebRun fastcgi listen err:", err)
return
}
defer listener.Close()
Clog("WebRun fastcgi running: " + ipsk)
err = fcgi.Serve(listener, nil)
if err != nil {
Clog("WebRun fastcgi err:", err)
return
}
} else if mode == "unixsock" {
// sigchan := make(chan os.Signal, 1)
// signal.Notify(sigchan, os.Interrupt)
// signal.Notify(sigchan, syscall.SIGTERM)
var sockf string
switch runtime.GOOS {
case "linux":
sockf = "/run/" + ipsk
default:
Clog("WebRun unixsock err: unknown system " + runtime.GOOS)
return
}
os.Remove(sockf)
listener, err := net.Listen("unix", sockf)
if err != nil {
Clog("WebRun unixsock listen err:", err)
return
}
defer listener.Close()
Clog("WebRun unixsock running: " + sockf)
err = fcgi.Serve(listener, nil)
if err != nil {
Clog("WebRun unixsock err:", err)
return
}
// <-sigchan
// os.Remove(sockf)
} else {
Clog("WebRun http running: " + ipsk)
err := http.ListenAndServe(ipsk, nil)
if err != nil {
Clog("WebRun http err:", err)
return
}
}
}
func (thos *CiyWebServer) RunHTTP2(parm string) {
if parm == "" {
return
}
parms := strings.Split(parm, ",")
if len(parms) != 3 {
Clog("WebRun http2 param err:", parm)
}
err := http.ListenAndServeTLS(parms[0], parms[1], parms[2], nil)
if err != nil {
Clog("WebRun http2 err:", err)
}
Clog("WebRun http2 running: " + parm)
}
func GetQuery(key string, r *http.Request) string {
query := r.URL.Query()
if query[key] == nil {
return ""
}
return query[key][0]
}
func ErrJSON(w http.ResponseWriter, errmsg string, argext ...any) bool {
jsn := map[string]any{
"errmsg": errmsg,
}
var err error
if len(argext) > 0 {
for i, v := range argext {
if i == 0 {
if daterr, ok := v.(error); ok {
err = daterr
continue
}
}
if datint, ok := v.(int); ok {
if datint != 1 {
jsn["code"] = datint
}
} else if datmap, ok := v.(map[string]any); ok {
for key, val := range datmap {
if key != "code" {
jsn[key] = val
}
}
}
}
}
if err != nil {
Log.Info("ErrJSON", errmsg+":"+err.Error())
}
w.Write(JSON_Byte(jsn))
return false
}
func SuccJSON(w http.ResponseWriter, r *http.Request, obj ...map[string]any) bool {
jsn := map[string]any{
"code": 1,
}
for _, kv := range obj {
for k, v := range kv {
if k != "code" {
jsn[k] = v
}
}
}
reqCtx := r.Context().Value(GhttpKey).(*RequestContext)
if reqCtx != nil && reqCtx.CiyAuth != "" {
jsn["_ciyauth"] = reqCtx.CiyAuth
}
w.Write(JSON_Byte(jsn))
return true
}