使用Golang实现Nginx代理功能

由于业务需要实现对多个web应用做同域二级目录代理,用NGINX的又感觉太重了,而且不好做配置页面,用golang来实现代理功能

  • 支持正则表达式匹配机制
  • 支持多应用多级目录代理。
  • 支持应用子路由代理
  • 支持webapi代理
  • 支持websocket代理
  • 支持禁用缓存设置
  • 支持本地文件服务
  • 支持http、https混合使用
  • 支持/dir/app 重定向为 /dir/app/
  • 支持简单的路由热度升级

定义处理器选项并初始化默认数据记录

package main

const (
	/* 本地文件系统 */
	LocalFileSystem HandlerType = 10000
	/* 远程Http服务 */
	RemoteHttpServer HandlerType = 20000
)

type HandlerOptions struct {
	Id int32
	/* 处理器名称 */
	Name string
	/* 处理器类型 */
	Type HandlerType
	/* 处理路径 */
	Path string
	/* 禁用缓存 */
	DisableCache bool
	/* 目标服务器,或本地文件目录 */
	Target string
	/* 子处理器列表 */
	SubHandlers []*HandlerOptions
	/* 是否启用 */
	Enabled bool
}

var handlers = []*HandlerOptions{
	{
		Id:           1,
		Name:         "My First App",
		Path:         "^/app1",
		Type:         RemoteHttpServer,
		Target:       "https://192.168.1.20:8000/",
		Enabled:      true,
		DisableCache: true,
		SubHandlers: []*HandlerOptions{
			{
				Id:           11,
				Name:         "API接口",
				Path:         "^/app1/api",
				Type:         RemoteHttpServer,
				Target:       "http://192.168.1.30:8100/api",
				Enabled:      true,
				DisableCache: true,
				SubHandlers:  []*HandlerOptions{},
			},
			{
				Id:           12,
				Name:         "静态文件服务器",
				Path:         "^/app1/static/",
				Type:         RemoteHttpServer,
				Target:       "https://192.168.1.70:8200/static/",
				Enabled:      true,
				DisableCache: false,
				SubHandlers:  []*HandlerOptions{},
			},
			{
				Id:           13,
				Name:         "其他系统接口",
				Path:         "^/app1/others/api/",
				Type:         RemoteHttpServer,
				Target:       "http://192.168.1.50:8300/api/",
				Enabled:      true,
				DisableCache: true,
				SubHandlers:  []*HandlerOptions{},
			},
		},
	},
	{
		Id:           20,
		Name:         "Local File App",
		Path:         "/test",
		Type:         LocalFileSystem,
		Target:       "/root/workspace/test/static/www",
		Enabled:      true,
		DisableCache: true,
		SubHandlers: []*HandlerOptions{
			{
				Id:           21,
				Name:         "api",
				Path:         "^/test/api/",
				Type:         RemoteHttpServer,
				Target:       "https://192.168.1.100:8000/api/",
				Enabled:      true,
				DisableCache: true,
				SubHandlers:  []*HandlerOptions{},
			},
			{
				Id:           22,
				Name:         "静态文件",
				Path:         "^/test/static/",
				Type:         LocalFileSystem,
				Target:       "/root/workspace/test/static",
				Enabled:      true,
				DisableCache: true,
				SubHandlers:  []*HandlerOptions{},
			},
			{
				Id:           23,
				Name:         "websocket相关",
				Path:         "^/test/ws",
				Type:         RemoteHttpServer,
				Target:       "https://192.168.1.100:8000/ws",
				Enabled:      true,
				DisableCache: true,
				SubHandlers:  []*HandlerOptions{},
			},
	},
}

处理器节点的实现

通过root节点的Options方法更新全部处理器节点。

package main

import (
	"fmt"
	"net/http"
	"net/http/httputil"
	"net/url"
	"regexp"
	"strings"
	"sync"
)

type ProxyHandlerNode struct {
	// 节点选项
	options *HandlerOptions
	// 节点处理器列表
	nodes []*ProxyHandlerNode
	// 匹配正则
	regxExpression *regexp.Regexp
	// 匹配计数器, 用于热度排序
	Popularity int
	// 目标服务器URL
	TargetURL *url.URL
	// 目标服务器路径(这段路径需要放到请求Path前面)
	TargetPath string
	// 操作锁
	locker sync.Mutex
	// 本地文件系统
	fileSystem *http.Handler
}

// 配置节点选项
func (node *ProxyHandlerNode) Options(options []*HandlerOptions) {
	node.locker.Lock()
	defer node.locker.Unlock()
	node.nodes = make([]*ProxyHandlerNode, 0)
	for i := 0; i < len(options); i++ {
		opt := options[i]
		if !opt.Enabled {
			continue
		}
		newNode := ProxyHandlerNode{}
		newNode.Popularity = 0
		newNode.options = opt
		var err error
		newNode.regxExpression, err = regexp.Compile(opt.Path)
		if err != nil {
			fmt.Printf("Regexp Compile Error:%v\n", err)
			continue
		}
		newNode.TargetURL, err = url.Parse(opt.Target)
		if err != nil {
			fmt.Printf("URL Parse Error:%v\n", err)
			continue
		}
		if newNode.options.Type == LocalFileSystem {
			var fs = http.FileServer(LocalFile(newNode.options.Target, false))
			newNode.fileSystem = &fs
		}
		newNode.TargetPath = newNode.TargetURL.Path
		newNode.TargetURL.Path = ""
		newNode.Options(opt.SubHandlers)
		node.nodes = append(node.nodes, &newNode)
	}
}


/*
 * 随着匹配次数热度爬升
 */
func popularityUp(nodes []*ProxyHandlerNode, node *ProxyHandlerNode, index int) {
	node.Popularity++
	if index > 0 {
		if node.Popularity > nodes[index-1].Popularity {
			nodes[index], nodes[index-1] = nodes[index-1], nodes[index]
		}
	}
}

/*
 * 匹配请求URL的处理器
 */
func (node *ProxyHandlerNode) MatchNode(url string) *ProxyHandlerNode {
	node.locker.Lock()
	defer node.locker.Unlock()
	for i := 0; i < len(node.nodes); i++ {
		var cnode = node.nodes[i]
		if cnode.regxExpression.Match([]byte(url)) {
			popularityUp(node.nodes, cnode, i)
			var ret = cnode.MatchNode(url)
			if ret != nil {
				return ret
			}
			return cnode
		}
	}
	return nil
}

/*
 * 代理请求
 */
func (node *ProxyHandlerNode) ProxyRequest(req *http.Request, res http.ResponseWriter) {
	// 替换url
	var requestPath = node.regxExpression.ReplaceAllString(req.URL.Path, "")
	// http重定向
	if len(requestPath) == 0 && len(req.URL.RawQuery) == 0 {
		// 把 /dir/app 重定向为 /dir/app/
		if !strings.HasSuffix(req.URL.Path, "/") {
			http.Redirect(res, req, req.URL.Path+"/", http.StatusTemporaryRedirect)
			return
		}
	}

	// 节点是本地文件系统
	if node.fileSystem != nil {
		req.URL.Path = requestPath
		(*node.fileSystem).ServeHTTP(res, req)
		return
	}

	// 修复路径
	req.URL.Path = node.TargetPath + requestPath

	// 代理
	proxy := httputil.NewSingleHostReverseProxy(node.TargetURL)
	proxy.ModifyResponse = func(r *http.Response) error {
		if node.options.DisableCache {
			r.Header.Set("Cache-Control", "no-store,no-cache")
			r.Header.Set("Pragma", "no-store,no-cache")
			r.Header.Set("Expires", "0")
			r.Header.Del("Last-Modified")
		}
		r.Header.Del("Server")
		return nil
	}
	proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
		fmt.Printf("Serve Error %v {%v}\n", r.URL, err)
	}
	if node.options.DisableCache {
		req.Header.Set("Cache-Control", "no-store,no-cache")
		req.Header.Set("Pragma", "no-store,no-cache")
		req.Header.Del("if-modified-since")
		req.Header.Del("if-none-match")
	}
	proxy.ServeHTTP(res, req)
}

本地文件系统服务

package main

import (
	"net/http"
	"os"
	"path"
	"strings"

	"github.com/gin-gonic/gin"
)

const INDEX = "index.html"

type ServeFileSystem interface {
	http.FileSystem
	Exists(prefix string, path string) bool
}

type localFileSystem struct {
	http.FileSystem
	root    string
	indexes bool
}

func LocalFile(root string, indexes bool) *localFileSystem {
	return &localFileSystem{
		FileSystem: gin.Dir(root, indexes),
		root:       root,
		indexes:    indexes,
	}
}

func (l *localFileSystem) Exists(prefix string, filepath string) bool {
	if p := strings.TrimPrefix(filepath, prefix); len(p) < len(filepath) {
		name := path.Join(l.root, p)
		stats, err := os.Stat(name)
		if err != nil {
			return false
		}
		if stats.IsDir() {
			if !l.indexes {
				index := path.Join(name, INDEX)
				_, err := os.Stat(index)
				if err != nil {
					return false
				}
			}
		}
		return true
	}
	return false
}

食用方式

package main

import (
	"crypto/tls"
	"fmt"
	"log"
	"net/http"
)

var rootHandler = &ProxyHandlerNode{}

func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
	//
	var node = rootHandler.MatchNode(req.URL.Path)
	if node != nil {
		node.ProxyRequest(req, res)
		return
	}
	fmt.Printf("Not Handle Request %v\n", req.RequestURI)
	res.WriteHeader(http.StatusBadGateway)
	res.Write([]byte("Proxy Error: No suitable forwarding processor matched."))
}

func main() {
	// 初始化处理器
	rootHandler.Options(handlers)
	// 跳过tsl证书验证
	http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
	http.HandleFunc("/", handleRequestAndRedirect)
	// http
	if err := http.ListenAndServe(":8888", nil); err != nil {
		log.Fatal("ListenAndServe: ", err)
	}
}

posted @   土豆赛叩  阅读(355)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示