397 lines
10 KiB
Go
397 lines
10 KiB
Go
/*
|
||
=================================================================================
|
||
* 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
|
||
}
|