package client
|
|
import (
|
"bytes"
|
"context"
|
"encoding/json"
|
"errors"
|
"io"
|
"io/ioutil"
|
"mime"
|
"net/http"
|
"net/url"
|
"os"
|
"reflect"
|
"strings"
|
"time"
|
"vamicro/api-gateway/auth"
|
"vamicro/api-gateway/models"
|
"vamicro/api-gateway/traces"
|
"vamicro/config"
|
"vamicro/extend/code"
|
"vamicro/extend/util"
|
|
"basic.com/valib/bhomeclient.git"
|
"basic.com/valib/c_bhomebus.git/proto/source/bhome_msg"
|
"basic.com/valib/logger.git"
|
"github.com/gin-gonic/gin"
|
"github.com/goinggo/mapstructure"
|
)
|
|
type Client struct {
|
ms *bhomeclient.MicroNode
|
}
|
|
var (
|
procName = "api-gateway"
|
proc = bhomeclient.ProcInfo{
|
Name: procName, //进程名称
|
ID: procName, //进程id
|
Info: "", //进程的描述信息,用于区分同一进程名称下多个进程
|
}
|
firstCall = true
|
)
|
|
func NewClient(ctx context.Context, q chan os.Signal) (*Client, error) {
|
var reg = &bhomeclient.RegisterInfo{
|
Proc: proc,
|
Channel: nil,
|
PubTopic: []string{},
|
SubTopic: []string{},
|
}
|
ms, err := bhomeclient.NewMicroNode(ctx, q, config.Server.AnalyServerId, reg, logger.Debug)
|
if err != nil {
|
logger.Debug("NewMicroNode err:", err)
|
return nil, err
|
}
|
ms.StartClient()
|
|
return &Client{
|
ms: ms,
|
}, nil
|
}
|
|
func (c *Client) PublishNetTimeout(nodes []bhome_msg.BHAddress, topic string, msg []byte, timeout int) int {
|
return c.ms.PublishNetTimeout(nodes, topic, msg, timeout)
|
}
|
|
func (c *Client) DeRegister() {
|
if c != nil && c.ms != nil {
|
c.ms.DeRegister()
|
}
|
}
|
|
func (c *Client) Free() {
|
if c != nil && c.ms != nil {
|
c.ms.Free()
|
}
|
}
|
|
//过期后允许通过的url
|
var expired_NoAuthUrls = []string{
|
"/data/api-v/authorize",
|
"/data/api-v/token",
|
"/data/api-v/newClient",
|
"/data/api-u/users/current",
|
"/data/api-u/sysmenus/me",
|
"/data/api-v/clients",
|
"/data/api-u/sys/login",
|
"/data/api-u/sys/logout",
|
"/data/api-v/info/getServerName",
|
"/data/api-v/license/show",
|
"/data/api-u/users/profile",
|
"/data/api-v/gb28181/findAreaByParentId",
|
"/data/api-v/sysinit/getInitInfo",
|
"/data/api-v/sysinit/savePassword",
|
"/data/api-v/sysinit/networkList",
|
"/data/api-v/sysinit/initNetwork",
|
"/data/api-v/sysinit/saveRegInfo",
|
"/data/api-v/sysinit/getRegInfo",
|
}
|
|
func (c *Client) AnyRequest(ctx *gin.Context) {
|
contentType := ctx.Request.Header.Get("Content-Type")
|
// RFC 7231, section 3.1.1.5 - empty type
|
// MAY be treated as application/octet-stream
|
if contentType == "" {
|
contentType = "application/octet-stream"
|
}
|
|
if util.SysExpired {
|
//试用过期之后,只允许登录入系统做更新授权处理
|
up := ctx.Request.URL.Path
|
if util.ArrayContains(expired_NoAuthUrls, up) {
|
//授权过期之后,只允许指定的url通过
|
logger.Info("expired!!!! current url:", up)
|
} else {
|
ctx.JSON(http.StatusBadRequest, "使用已过期,请购买授权")
|
return
|
}
|
}
|
if ctx.Request.URL.Path == "/data/api-u/users/current" {
|
authDriver := auth.GenerateAuthDriver()
|
user := (*authDriver).User(ctx)
|
if user != nil {
|
ctx.JSON(http.StatusOK, user)
|
} else {
|
ctx.JSON(http.StatusUnauthorized, "")
|
}
|
return
|
} else if ctx.Request.URL.Path == "/data/api-u/sysmenus/me" {
|
authDriver := auth.GenerateAuthDriver()
|
userM := (*authDriver).User(ctx)
|
if userM != nil {
|
menus := userM["permissions"]
|
util.ResponseFormat(ctx, code.Success, menus)
|
} else {
|
util.ResponseFormat(ctx, code.NotLogin, "")
|
}
|
return
|
} else if ctx.Request.URL.Path == "/data/api-v/authorize" {
|
err := auth.Oauth2Serv.HandleAuthorizeRequest(ctx.Writer, ctx.Request)
|
if err != nil {
|
ctx.JSON(http.StatusBadRequest, err)
|
}
|
return
|
} else if ctx.Request.URL.Path == "/data/api-v/token" {
|
err := auth.Oauth2Serv.HandleTokenRequest(ctx.Writer, ctx.Request)
|
if err != nil {
|
ctx.JSON(http.StatusInternalServerError, err)
|
}
|
return
|
} else if ctx.Request.URL.Path == "/data/api-v/newClient" {
|
err := auth.NewClient(ctx.Request.FormValue("domain"), ctx.Request.FormValue("intro"))
|
if err != nil {
|
util.ResponseFormat(ctx, code.SaveFail, err)
|
} else {
|
util.ResponseFormat(ctx, code.Success, "")
|
}
|
return
|
} else if ctx.Request.URL.Path == "/data/api-v/clients" {
|
var oauth2Model models.Oauth2Client
|
oauth2Clients, err := oauth2Model.FindAll()
|
if err != nil {
|
util.ResponseFormat(ctx, code.DdSelectNotFindError, "")
|
} else {
|
util.ResponseFormat(ctx, code.Success, oauth2Clients)
|
}
|
return
|
} else if ctx.Request.URL.Path == "/data/api-v/delClient" {
|
err := auth.DelClient(ctx.Request.FormValue("id"))
|
if err != nil {
|
util.ResponseFormat(ctx, code.DeleteFail, "")
|
} else {
|
util.ResponseFormat(ctx, code.Success, "")
|
}
|
return
|
}
|
|
t := time.Now()
|
var buffer = new(bytes.Buffer)
|
buffer.ReadFrom(ctx.Request.Body)
|
//ctx.Request.ParseForm()
|
pfValues, parseE := parsePostForm(ctx.Request, buffer.Bytes())
|
formValues, _ := url.ParseQuery(ctx.Request.URL.RawQuery)
|
/** 日志modules和operations **/
|
/**
|
if ctx.Request.URL.Path == "/data/api-v/log/modules" {
|
var moduleMod models.Modules
|
modules,err := moduleMod.FindAll()
|
if nil == err {
|
util.ResponseFormat(ctx,code.Success,modules)
|
} else {
|
util.ResponseFormat(ctx,code.DdSelectNotFindError,err.Error())
|
}
|
return
|
} else if ("/data/api-v/log/operations" == ctx.Request.URL.Path) {
|
var opModel models.Operations
|
mod,ok := ctx.Request.URL.Query()["module"]
|
if ok {
|
opers,err := opModel.FindAll(mod[0])
|
if nil == err {
|
util.ResponseFormat(ctx,code.Success,opers)
|
} else {
|
util.ResponseFormat(ctx,code.DdSelectNotFindError,err.Error())
|
}
|
} else {
|
util.ResponseFormat(ctx,code.DdSelectNotFindError,"")
|
}
|
return
|
}**/
|
/** modules和operations结束 **/
|
|
//if parseMultiErr := ctx.Request.ParseMultipartForm(32 << 20);parseMultiErr != nil && parseMultiErr.Error() != "request Content-Type isn't multipart/form-data" {
|
// logger.Debug("ParseMultipartForm err:", parseMultiErr)
|
//}
|
req := bhomeclient.Request{
|
Path: ctx.Request.URL.Path,
|
Method: ctx.Request.Method,
|
ContentType: ctx.ContentType(),
|
HeaderMap: ctx.Request.Header,
|
QueryMap: ctx.Request.URL.Query(),
|
FormMap: formValues,
|
PostFormMap: pfValues,
|
//Body: buffer.Bytes(),
|
}
|
if req.FormMap == nil {
|
req.FormMap = make(map[string][]string)
|
}
|
if req.PostFormMap == nil {
|
req.PostFormMap = make(map[string][]string)
|
}
|
|
//gin框架的body参数只允许读取一次
|
if strings.HasPrefix(contentType, "multipart/form-data") {
|
logger.Debug("equal, contentType:", contentType)
|
ctx.Request.Body = ioutil.NopCloser(buffer)
|
pe := ctx.Request.ParseMultipartForm(defaultMultipartMemory)
|
if pe != nil {
|
logger.Error("r.ParseMultipartForm err:", pe)
|
} else {
|
appendSignleFileArgs(ctx, &req)
|
|
appendMultiArgs(ctx, &req)
|
}
|
} else {
|
req.Body = buffer.Bytes()
|
}
|
|
if len(req.PostFormMap) > 0 {
|
copyValues(req.FormMap, req.PostFormMap)
|
}
|
|
if _, exist := req.HeaderMap["Authorization"]; exist {
|
delete(req.HeaderMap, "Authorization")
|
}
|
|
logger.Debug("请求解析完成耗时:", time.Since(t))
|
logger.Debug("ParseForm err:", parseE, " postForm:", req.PostFormMap, " queryMap:", req.QueryMap)
|
t = time.Now()
|
timeOut := 15 * 1000
|
if req.Path == "/data/api-v/sdk/sdkDownload" ||
|
req.Path == "/data/api-v/sysset/upgrade" ||
|
req.Path == "/data/api-v/sysset/getDeviceInfo" ||
|
req.Path == "/data/api-v/sysset/getDeviceAuthInfo" ||
|
req.Path == "/data/api-v/sdk/install" ||
|
req.Path == "/data/api-v/sdk/upload" ||
|
req.Path == "/data/api-v/dbperson/searchByPhoto" ||
|
req.Path == "/data/api-v/cluster/joinCluster" {
|
timeOut = 60 * 1000
|
}
|
if req.Path == "/data/api-v/sdk/findAllSdk" ||
|
req.Path == "/data/api-v/app/findAllApp" {
|
timeOut = 20 * 1000
|
}
|
|
rewriteUploadPath(&req)
|
r, err := c.ms.Request(config.Server.AnalyServerId, req, timeOut)
|
if r != nil {
|
logger.Debug("请求服务耗时:", time.Since(t), " reqSize:", SizeOf(req), " repSize:", SizeOf(*r))
|
} else {
|
logger.Debug("请求服务耗时:", time.Since(t), " reqPath:", req.Path, " err:", err, " r:", r)
|
}
|
if (req.Path == "/data/api-v/sdk/upload" || req.Path == "/data/api-v/sysset/patchUpdate") && strings.EqualFold(req.Method, "GET") {
|
//这两个方法前端返回的内容是特殊的,只有状态码和内容
|
if err != nil {
|
ctx.JSON(500, gin.H{
|
"code": 500,
|
"success": false,
|
"msg": err.Error(),
|
"data": "",
|
})
|
} else {
|
if r.Success {
|
if r.Data.(string) == "found" {
|
ctx.String(http.StatusOK, "found")
|
} else {
|
ctx.String(http.StatusNoContent, "")
|
}
|
|
// readme: 目的是为日志统计的时候格式统一
|
r.Success = true
|
} else {
|
ctx.JSON(500, gin.H{
|
"code": 500,
|
"success": false,
|
"msg": r.Msg,
|
"data": r.Data,
|
})
|
}
|
}
|
} else {
|
if err != nil {
|
ctx.JSON(500, gin.H{
|
"code": 500,
|
"success": false,
|
"msg": err.Error(),
|
"data": "",
|
})
|
} else {
|
if ctx.Request.URL.Path == "/data/api-u/sys/login" && r.Success {
|
loginedM := util.Struct2Map(r.Data)
|
tokenM := make(map[string]interface{}, 5)
|
tokenM["id"] = loginedM["id"]
|
tokenM["username"] = loginedM["username"]
|
tokenM["permissions"] = loginedM["permissions"]
|
tokenM["roleName"] = loginedM["roleName"]
|
tokenM["roles"] = loginedM["sysRoles"]
|
//tokenM["headpic"] = loginedM["headpic"]
|
tokenM["headpic"] = ""
|
authDriver := auth.GenerateAuthDriver()
|
tokenStr := (*authDriver).Login(ctx.Request, ctx.Writer, tokenM)
|
|
userId := loginedM["id"].(string)
|
auth.RemoveOutUser(userId)
|
|
ctx.JSON(200, map[string]interface{}{
|
"userInfo": loginedM,
|
"access_token": tokenStr,
|
"refresh_token": tokenStr,
|
"scope": "app",
|
"token_type": "Bearer",
|
"expires_in": time.Now().Add(time.Hour * 8).Unix(),
|
})
|
} else {
|
cod := 200
|
if !r.Success {
|
cod = 500
|
}
|
ctx.JSON(cod, gin.H{
|
"code": cod,
|
"success": r.Success,
|
"msg": r.Msg,
|
"data": r.Data,
|
})
|
}
|
}
|
|
op := &traces.OperationPara{
|
Context: ctx,
|
Error: err,
|
Reply: r,
|
}
|
traces.TraceOperation(op)
|
}
|
}
|
|
func SizeOf(d interface{}) int {
|
b, _ := json.Marshal(d)
|
return len(b)
|
}
|
|
func rewriteUploadPath(req *bhomeclient.Request) {
|
if req.Path == "/data/api-v/sdk/upload" && strings.EqualFold(req.Method, "POST") {
|
req.Path = "/data/api-v/sdk/uploadPack"
|
} else if req.Path == "/data/api-v/sysset/patchUpdate" && strings.EqualFold(req.Method, "POST") {
|
req.Path = "/data/api-v/sysset/patchUpdatePack"
|
}
|
}
|
|
func copyValues(dst, src url.Values) {
|
for k, vs := range src {
|
dst[k] = append(dst[k], vs...)
|
}
|
}
|
|
type maxBytesReader struct {
|
w http.ResponseWriter
|
r io.ReadCloser // underlying reader
|
n int64 // max bytes remaining
|
err error // sticky error
|
}
|
|
func (l *maxBytesReader) Read(p []byte) (n int, err error) {
|
if l.err != nil {
|
return 0, l.err
|
}
|
if len(p) == 0 {
|
return 0, nil
|
}
|
// If they asked for a 32KB byte read but only 5 bytes are
|
// remaining, no need to read 32KB. 6 bytes will answer the
|
// question of the whether we hit the limit or go past it.
|
if int64(len(p)) > l.n+1 {
|
p = p[:l.n+1]
|
}
|
n, err = l.r.Read(p)
|
|
if int64(n) <= l.n {
|
l.n -= int64(n)
|
l.err = err
|
return n, err
|
}
|
|
n = int(l.n)
|
l.n = 0
|
|
// The server code and client code both use
|
// maxBytesReader. This "requestTooLarge" check is
|
// only used by the server code. To prevent binaries
|
// which only using the HTTP Client code (such as
|
// cmd/go) from also linking in the HTTP server, don't
|
// use a static type assertion to the server
|
// "*response" type. Check this interface instead:
|
type requestTooLarger interface {
|
requestTooLarge()
|
}
|
if res, ok := l.w.(requestTooLarger); ok {
|
res.requestTooLarge()
|
}
|
l.err = errors.New("http: request body too large")
|
return n, l.err
|
}
|
|
func (l *maxBytesReader) Close() error {
|
return l.r.Close()
|
}
|
|
func parsePostForm(r *http.Request, bufferBytes []byte) (vs url.Values, err error) {
|
if r.Body == nil {
|
err = errors.New("missing form body")
|
return
|
}
|
ct := r.Header.Get("Content-Type")
|
// RFC 7231, section 3.1.1.5 - empty type
|
// MAY be treated as application/octet-stream
|
if ct == "" {
|
ct = "application/octet-stream"
|
}
|
ct, _, err = mime.ParseMediaType(ct)
|
logger.Debug("ct:", ct)
|
switch {
|
case ct == "application/x-www-form-urlencoded":
|
//var reader io.Reader = r.Body
|
//maxFormSize := int64(1<<63 - 1)
|
//if _, ok := r.Body.(*maxBytesReader); !ok {
|
// maxFormSize = int64(10 << 20) // 10 MB is a lot of text.
|
// reader = io.LimitReader(r.Body, maxFormSize+1)
|
//}
|
//b, e := ioutil.ReadAll(reader)
|
//logger.Debug("ReadAll body:", string(b), " e:", e)
|
//
|
//var buffer = new(bytes.Buffer)
|
//buffer.ReadFrom(r.Body)
|
//logger.Debug("buffer:", string(buffer.Bytes()))
|
//
|
//if e != nil {
|
// if err == nil {
|
// err = e
|
// }
|
// break
|
//}
|
//if int64(len(b)) > maxFormSize {
|
// err = errors.New("http: POST too large")
|
// return
|
//}
|
var e error
|
vs, e = url.ParseQuery(string(bufferBytes))
|
if err == nil {
|
err = e
|
}
|
case ct == "multipart/form-data":
|
// handled by ParseMultipartForm (which is calling us, or should be)
|
// TODO(bradfitz): there are too many possible
|
// orders to call too many functions here.
|
// Clean this up and write more tests.
|
// request_test.go contains the start of this,
|
// in TestParseMultipartFormOrder and others.
|
}
|
return
|
}
|
|
const defaultMultipartMemory = 32 << 20 // 32 MB
|
func parseMultipartForm(r *http.Request) {
|
|
}
|
|
//处理请求中的上传文件参数
|
func appendSignleFileArgs(ctx *gin.Context, req *bhomeclient.Request) {
|
//single file arg
|
file, header, err := ctx.Request.FormFile("file")
|
|
if file != nil && header != nil && err == nil {
|
logger.Debug("single file header.Filename:", header.Filename)
|
|
defer file.Close()
|
fileBytes, err := ioutil.ReadAll(file)
|
if err != nil {
|
logger.Debug("ioutil.ReadAll err:", err)
|
return
|
}
|
req.File = bhomeclient.FileArg{
|
Name: header.Filename,
|
Size: header.Size,
|
Bytes: fileBytes,
|
}
|
} else {
|
logger.Debug("single FormFile err:", err)
|
}
|
}
|
|
func appendMultiArgs(ctx *gin.Context, req *bhomeclient.Request) {
|
//multi file arg
|
form, err := ctx.MultipartForm()
|
if err != nil {
|
logger.Debug("appendFileArgs err:", err)
|
return
|
}
|
for _, fhs := range form.File {
|
if len(fhs) > 0 {
|
for _, fh := range fhs {
|
f, err := fh.Open()
|
if err != nil {
|
logger.Debug("fh.Open err:", err)
|
continue
|
}
|
fileBytes, err := ioutil.ReadAll(f)
|
if err != nil {
|
logger.Debug("ioutil.ReadAll err:", err)
|
f.Close()
|
continue
|
}
|
req.MultiFiles = append(req.MultiFiles, bhomeclient.FileArg{
|
Name: fh.Filename,
|
Size: fh.Size,
|
Bytes: fileBytes,
|
})
|
f.Close()
|
}
|
}
|
}
|
|
if len(form.Value) > 0 {
|
for k, v := range form.Value {
|
req.PostFormMap[k] = v
|
}
|
}
|
}
|
|
func (c *Client) ModuleMapInit() {
|
var operations models.Operations
|
req := bhomeclient.Request{
|
Path: "/data/api-v/log/operations",
|
Method: "POST",
|
ContentType: "application/x-www-form-urlencoded",
|
HeaderMap: make(map[string][]string),
|
QueryMap: make(map[string][]string),
|
FormMap: make(map[string][]string),
|
PostFormMap: make(map[string][]string),
|
//Body: buffer.Bytes(),
|
}
|
timeOut := 5 * 1000
|
r, err := c.ms.Request(config.Server.AnalyServerId, req, timeOut)
|
if nil == err && r.Success {
|
logger.Debug(r)
|
logger.Debug("operations map kind", reflect.TypeOf(r.Data).Kind())
|
//list := r.Data.([]models.Operations)
|
//traces.SetBlockMethod(list)
|
setBlockMethod(r.Data.([]interface{}))
|
} else {
|
for {
|
time.Sleep(3 * time.Second)
|
r, err := c.ms.Request(config.Server.AnalyServerId, req, timeOut)
|
if nil == err && r.Success {
|
logger.Debug(r)
|
logger.Debug("operations map kind", reflect.TypeOf(r.Data).Kind())
|
//list := r.Data.([]models.Operations)
|
//traces.SetBlockMethod(list)
|
setBlockMethod(r.Data.([]interface{}))
|
break
|
}
|
}
|
}
|
operations.Name = "test"
|
|
req = bhomeclient.Request{
|
Path: "/data/api-v/log/find_all_modules_map",
|
Method: "POST",
|
ContentType: "application/x-www-form-urlencoded",
|
HeaderMap: make(map[string][]string),
|
QueryMap: make(map[string][]string),
|
FormMap: make(map[string][]string),
|
PostFormMap: make(map[string][]string),
|
//Body: buffer.Bytes(),
|
}
|
timeOut = 5 * 1000
|
r, err = c.ms.Request(config.Server.AnalyServerId, req, timeOut)
|
if nil == err && r.Success {
|
//logger.Debug(r)
|
//logger.Debug("module map kind",reflect.TypeOf(r.Data).Kind())
|
setModuleMap(r.Data.(map[string]interface{}))
|
//traces.SetModuleMap((r.Data).(map[string]string))
|
} else {
|
for {
|
time.Sleep(3 * time.Second)
|
r, err := c.ms.Request(config.Server.AnalyServerId, req, timeOut)
|
if nil == err && r.Success {
|
//logger.Debug(r)
|
//logger.Debug("module map kind",reflect.TypeOf(r.Data).Kind())
|
setModuleMap(r.Data.(map[string]interface{}))
|
break
|
}
|
logger.Error(err.Error())
|
}
|
}
|
}
|
|
func setModuleMap(mapItem map[string]interface{}) {
|
moduleMap := make(map[string]string)
|
for key, item := range mapItem {
|
moduleMap[key] = item.(string)
|
}
|
logger.Debug(moduleMap)
|
traces.SetModuleMap(moduleMap)
|
}
|
|
func setBlockMethod(res []interface{}) {
|
var operations []models.Operations
|
for _, item := range res {
|
operation, err := transOperationsStruct(item.(map[string]interface{}))
|
if nil == err {
|
operations = append(operations, operation)
|
}
|
}
|
logger.Debug("operation extract")
|
logger.Debug(operations)
|
traces.SetBlockMethod(operations)
|
auth.AclInit(operations)
|
}
|
|
func transOperationsStruct(mapitem map[string]interface{}) (models.Operations, error) {
|
var operation models.Operations
|
if err := mapstructure.Decode(mapitem, &operation); err != nil {
|
return operation, err
|
}
|
|
return operation, nil
|
}
|
|
/*
|
func (c *Client) Forward(ctx *gin.Context) {
|
targetHost := &TargetHost{
|
Host: "localhost",
|
IsHttps: false,
|
}
|
HostReverseProxy(ctx.Writer, ctx.Request, targetHost)
|
}
|
|
func GetVerTLSConfig(CAPath string) (*tls.Config, error) {
|
caData, err := ioutil.ReadFile(CAPath)
|
if err != nil {
|
return nil, err
|
}
|
pool := x509.NewCertPool()
|
pool.AppendCertsFromPEM(caData)
|
tlsConfig := &tls.Config{
|
RootCAs: pool,
|
}
|
return tlsConfig, nil
|
}
|
|
func HostReverseProxy(w http.ResponseWriter, req *http.Request, targetHost *TargetHost) {
|
host := ""
|
if targetHost.IsHttps {
|
host = host + "https://"
|
} else {
|
host = host + "http://"
|
}
|
remote, err := url.Parse(host + targetHost.Host)
|
if err != nil {
|
w.WriteHeader(http.StatusInternalServerError)
|
return
|
}
|
proxy := httputil.NewSingleHostReverseProxy(remote)
|
if targetHost.IsHttps {
|
tls, err := GetVerTLSConfig(targetHost.CAPath)
|
if err != nil {
|
w.WriteHeader(http.StatusInternalServerError)
|
return
|
}
|
var pTransport http.RoundTripper = &http.Transport{
|
Dial: func(netw, addr string) (net.Conn, error) {
|
c, err := net.DialTimeout(netw, addr, time.Second * 5)
|
if err != nil {
|
return nil, err
|
}
|
return c, nil
|
},
|
ResponseHeaderTimeout: time.Second * 3,
|
TLSClientConfig: tls,
|
}
|
proxy.Transport = pTransport
|
}
|
proxy.ServeHTTP(w, req)
|
}
|
|
type TargetHost struct {
|
Host string
|
IsHttps bool
|
CAPath string
|
}*/
|