Commit ea0ee1b1 by 孙龙

up

parent 398cb541
...@@ -20,7 +20,7 @@ type Admin struct { ...@@ -20,7 +20,7 @@ type Admin struct {
} }
func (t *Admin) TableName() string { func (t *Admin) TableName() string {
return "gateway_admin" return "lie_admin"
} }
func (t *Admin) LoginCheck(c *gin.Context, tx *gorm.DB, param *dto.AdminLoginInput) (*Admin, error) { func (t *Admin) LoginCheck(c *gin.Context, tx *gorm.DB, param *dto.AdminLoginInput) (*Admin, error) {
......
...@@ -25,7 +25,7 @@ type App struct { ...@@ -25,7 +25,7 @@ type App struct {
} }
func (t *App) TableName() string { func (t *App) TableName() string {
return "gateway_app" return "lie_app"
} }
func (t *App) Find(c *gin.Context, tx *gorm.DB, search *App) (*App, error) { func (t *App) Find(c *gin.Context, tx *gorm.DB, search *App) (*App, error) {
......
...@@ -78,7 +78,9 @@ func (s *ServiceManager) HTTPAccessMode(c *gin.Context) (*ServiceDetail, error) ...@@ -78,7 +78,9 @@ func (s *ServiceManager) HTTPAccessMode(c *gin.Context) (*ServiceDetail, error)
//192.168.2.246 //192.168.2.246
path := c.Request.URL.Path path := c.Request.URL.Path
// /ichuntMicroService/test // /ichuntMicroService/test
//fmt.Printf("%+v",s.ServiceSlice)
for _, serviceItem := range s.ServiceSlice { for _, serviceItem := range s.ServiceSlice {
//fmt.Printf("%+v",serviceItem)
//负载类型 0=http 1=tcp 2=grpc //负载类型 0=http 1=tcp 2=grpc
//不等于http //不等于http
if serviceItem.Info.LoadType != public.LoadTypeHTTP { if serviceItem.Info.LoadType != public.LoadTypeHTTP {
...@@ -98,7 +100,7 @@ func (s *ServiceManager) HTTPAccessMode(c *gin.Context) (*ServiceDetail, error) ...@@ -98,7 +100,7 @@ func (s *ServiceManager) HTTPAccessMode(c *gin.Context) (*ServiceDetail, error)
} }
} }
} }
return nil, errors.New(fmt.Sprintf("没有匹配的服务 %s,%s",host,path)) return nil, errors.New(fmt.Sprintf("没有匹配的服务 HOST :%s, PATH :%s",host,path))
} }
func (s *ServiceManager) LoadOnce() error { func (s *ServiceManager) LoadOnce() error {
......
...@@ -18,7 +18,7 @@ type AccessControl struct { ...@@ -18,7 +18,7 @@ type AccessControl struct {
} }
func (t *AccessControl) TableName() string { func (t *AccessControl) TableName() string {
return "gateway_service_access_control" return "lie_service_access_control"
} }
func (t *AccessControl) Find(c *gin.Context, tx *gorm.DB, search *AccessControl) (*AccessControl, error) { func (t *AccessControl) Find(c *gin.Context, tx *gorm.DB, search *AccessControl) (*AccessControl, error) {
......
...@@ -14,7 +14,7 @@ type GrpcRule struct { ...@@ -14,7 +14,7 @@ type GrpcRule struct {
} }
func (t *GrpcRule) TableName() string { func (t *GrpcRule) TableName() string {
return "gateway_service_grpc_rule" return "lie_service_grpc_rule"
} }
func (t *GrpcRule) Find(c *gin.Context, tx *gorm.DB, search *GrpcRule) (*GrpcRule, error) { func (t *GrpcRule) Find(c *gin.Context, tx *gorm.DB, search *GrpcRule) (*GrpcRule, error) {
......
...@@ -19,7 +19,7 @@ type HttpRule struct { ...@@ -19,7 +19,7 @@ type HttpRule struct {
} }
func (t *HttpRule) TableName() string { func (t *HttpRule) TableName() string {
return "gateway_service_http_rule" return "lie_service_http_rule"
} }
func (t *HttpRule) Find(c *gin.Context, tx *gorm.DB, search *HttpRule) (*HttpRule, error) { func (t *HttpRule) Find(c *gin.Context, tx *gorm.DB, search *HttpRule) (*HttpRule, error) {
......
package dao package dao
import ( import (
"ichunt-micro/dto"
"ichunt-micro/public"
"github.com/e421083458/gorm" "github.com/e421083458/gorm"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"time" "ichunt-micro/dto"
"ichunt-micro/public"
) )
type ServiceInfo struct { type ServiceInfo struct {
...@@ -13,13 +12,13 @@ type ServiceInfo struct { ...@@ -13,13 +12,13 @@ type ServiceInfo struct {
LoadType int `json:"load_type" gorm:"column:load_type" description:"负载类型 0=http 1=tcp 2=grpc"` LoadType int `json:"load_type" gorm:"column:load_type" description:"负载类型 0=http 1=tcp 2=grpc"`
ServiceName string `json:"service_name" gorm:"column:service_name" description:"服务名称"` ServiceName string `json:"service_name" gorm:"column:service_name" description:"服务名称"`
ServiceDesc string `json:"service_desc" gorm:"column:service_desc" description:"服务描述"` ServiceDesc string `json:"service_desc" gorm:"column:service_desc" description:"服务描述"`
UpdatedAt time.Time `json:"create_at" gorm:"column:create_at" description:"更新时间"` UpdatedAt int64 `json:"create_at" gorm:"column:create_time" description:"更新时间"`
CreatedAt time.Time `json:"update_at" gorm:"column:update_at" description:"添加时间"` CreatedAt int64 `json:"update_at" gorm:"column:update_time" description:"添加时间"`
IsDelete int8 `json:"is_delete" gorm:"column:is_delete" description:"是否已删除;0:否;1:是"` IsDelete int8 `json:"is_delete" gorm:"column:is_delete" description:"是否已删除;0:否;1:是"`
} }
func (t *ServiceInfo) TableName() string { func (t *ServiceInfo) TableName() string {
return "gateway_service_info" return "lie_service_info"
} }
func (t *ServiceInfo) ServiceDetail(c *gin.Context, tx *gorm.DB, search *ServiceInfo) (*ServiceDetail, error) { func (t *ServiceInfo) ServiceDetail(c *gin.Context, tx *gorm.DB, search *ServiceInfo) (*ServiceDetail, error) {
......
...@@ -30,7 +30,7 @@ type LoadBalance struct { ...@@ -30,7 +30,7 @@ type LoadBalance struct {
} }
func (t *LoadBalance) TableName() string { func (t *LoadBalance) TableName() string {
return "gateway_service_load_balance" return "lie_service_load_balance"
} }
func (t *LoadBalance) Find(c *gin.Context, tx *gorm.DB, search *LoadBalance) (*LoadBalance, error) { func (t *LoadBalance) Find(c *gin.Context, tx *gorm.DB, search *LoadBalance) (*LoadBalance, error) {
......
...@@ -13,7 +13,7 @@ type TcpRule struct { ...@@ -13,7 +13,7 @@ type TcpRule struct {
} }
func (t *TcpRule) TableName() string { func (t *TcpRule) TableName() string {
return "gateway_service_tcp_rule" return "lie_service_tcp_rule"
} }
func (t *TcpRule) Find(c *gin.Context, tx *gorm.DB, search *TcpRule) (*TcpRule, error) { func (t *TcpRule) Find(c *gin.Context, tx *gorm.DB, search *TcpRule) (*TcpRule, error) {
......
No preview for this file type
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"regexp"
"strings" "strings"
"time" "time"
) )
...@@ -375,3 +376,32 @@ func Substr(str string, start int64, end int64) string { ...@@ -375,3 +376,32 @@ func Substr(str string, start int64, end int64) string {
} }
return string(str[start:end]) return string(str[start:end])
} }
//利用正则表达式压缩字符串,去除空格或制表符
func CompressStr(str string) string {
if str == "" {
return ""
}
//匹配一个或多个空白符的正则表达式
reg := regexp.MustCompile("\\s+")
return reg.ReplaceAllString(str, "")
}
func ClientIP(r *http.Request) string {
xForwardedFor := r.Header.Get("X-Forwarded-For")
ip := strings.TrimSpace(strings.Split(xForwardedFor, ",")[0])
if ip != "" {
return ip
}
ip = strings.TrimSpace(r.Header.Get("X-Real-Ip"))
if ip != "" {
return ip
}
if ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {
return ip
}
return ""
}
\ No newline at end of file
...@@ -107,7 +107,7 @@ func (w *FileWriter) CreateLogFile() error { ...@@ -107,7 +107,7 @@ func (w *FileWriter) CreateLogFile() error {
} }
} }
if file, err := os.OpenFile(w.filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644); err != nil { if file, err := os.OpenFile(w.filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0755); err != nil {
return err return err
} else { } else {
w.file = file w.file = file
......
package http_proxy_middleware package http_proxy_middleware
import ( import (
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/syyongx/php2go"
"ichunt-micro/dao" "ichunt-micro/dao"
"ichunt-micro/middleware" "ichunt-micro/middleware"
"ichunt-micro/public" "ichunt-micro/public"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"strings" "strings"
) )
...@@ -23,7 +24,7 @@ func HTTPStripUriMiddleware() gin.HandlerFunc { ...@@ -23,7 +24,7 @@ func HTTPStripUriMiddleware() gin.HandlerFunc {
if serviceDetail.HTTPRule.RuleType==public.HTTPRuleTypePrefixURL && serviceDetail.HTTPRule.NeedStripUri==1{ if serviceDetail.HTTPRule.RuleType==public.HTTPRuleTypePrefixURL && serviceDetail.HTTPRule.NeedStripUri==1{
//fmt.Println("c.Request.URL.Path",c.Request.URL.Path) //fmt.Println("c.Request.URL.Path",c.Request.URL.Path)
c.Request.URL.Path = strings.Replace(c.Request.URL.Path,serviceDetail.HTTPRule.Rule,"",1) c.Request.URL.Path = strings.Replace(c.Request.URL.Path,serviceDetail.HTTPRule.Rule,"",1)
//fmt.Println("c.Request.URL.Path",c.Request.URL.Path) c.Request.URL.Path = php2go.Rtrim(c.Request.URL.Path,"/")
} }
//http://127.0.0.1:8080/test_http_string/abbb //http://127.0.0.1:8080/test_http_string/abbb
//http://127.0.0.1:2004/abbb //http://127.0.0.1:2004/abbb
......
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
...@@ -24,8 +24,8 @@ const ( ...@@ -24,8 +24,8 @@ const (
) )
type Response struct { type Response struct {
ErrorCode ResponseCode `json:"errno"` ErrorCode ResponseCode `json:"err_code"`
ErrorMsg string `json:"errmsg"` ErrorMsg string `json:"err_msg"`
Data interface{} `json:"data"` Data interface{} `json:"data"`
TraceId interface{} `json:"trace_id"` TraceId interface{} `json:"trace_id"`
Stack interface{} `json:"stack"` Stack interface{} `json:"stack"`
...@@ -44,7 +44,11 @@ func ResponseError(c *gin.Context, code ResponseCode, err error) { ...@@ -44,7 +44,11 @@ func ResponseError(c *gin.Context, code ResponseCode, err error) {
stack = strings.Replace(fmt.Sprintf("%+v", err), err.Error()+"\n", "", -1) stack = strings.Replace(fmt.Sprintf("%+v", err), err.Error()+"\n", "", -1)
} }
resp := &Response{ErrorCode: code, ErrorMsg: err.Error(), Data: "", TraceId: traceId, Stack: stack} traceId=traceId
stack=stack
//resp := &Response{ErrorCode: code, ErrorMsg: err.Error(), Data: "", TraceId: traceId, Stack: stack}
resp := &Response{ErrorCode: code, ErrorMsg: err.Error(), Data: ""}
c.JSON(200, resp) c.JSON(200, resp)
response, _ := json.Marshal(resp) response, _ := json.Marshal(resp)
c.Set("response", string(response)) c.Set("response", string(response))
......
...@@ -5,9 +5,11 @@ import ( ...@@ -5,9 +5,11 @@ import (
"compress/gzip" "compress/gzip"
"context" "context"
"errors" "errors"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/syyongx/php2go" "github.com/syyongx/php2go"
"ichunt-micro/dao" "ichunt-micro/dao"
"ichunt-micro/golang_common/lib"
"ichunt-micro/golang_common/log" "ichunt-micro/golang_common/log"
"ichunt-micro/middleware" "ichunt-micro/middleware"
"ichunt-micro/proxy/load_balance" "ichunt-micro/proxy/load_balance"
...@@ -33,11 +35,17 @@ var ( ...@@ -33,11 +35,17 @@ var (
MaxIdleConns: 100, //最大空闲连接 MaxIdleConns: 100, //最大空闲连接
IdleConnTimeout: 60 * time.Second, //空闲超时时间 IdleConnTimeout: 60 * time.Second, //空闲超时时间
TLSHandshakeTimeout: 10 * time.Second, //tls握手超时时间 TLSHandshakeTimeout: 10 * time.Second, //tls握手超时时间
ResponseHeaderTimeout: 30*time.Second,
ExpectContinueTimeout: 1 * time.Second, //100-continue状态码超时时间 ExpectContinueTimeout: 1 * time.Second, //100-continue状态码超时时间
} }
) )
func NewMultipleHostsReverseProxy(c *gin.Context) (*httputil.ReverseProxy, error) { func NewMultipleHostsReverseProxy(c *gin.Context) (*httputil.ReverseProxy, error) {
var (
ServiceName string
target *url.URL
err error
)
//请求协调者 //请求协调者
service,exists := c.Get("service") service,exists := c.Get("service")
if !exists { if !exists {
...@@ -52,17 +60,23 @@ func NewMultipleHostsReverseProxy(c *gin.Context) (*httputil.ReverseProxy, error ...@@ -52,17 +60,23 @@ func NewMultipleHostsReverseProxy(c *gin.Context) (*httputil.ReverseProxy, error
return nil,err return nil,err
} }
//数据库中的rule字段 前缀匹配 //数据库中的rule字段 前缀匹配
rule := service_.HTTPRule.Rule ServiceName = service_.Info.ServiceName
rule_ := php2go.Trim(rule,"/") ServiceName = php2go.Trim(ServiceName,"/")
ServiceName = lib.CompressStr(ServiceName)
nextAddr, err := load_balance.LoadBalanceConfig.GetService(context.TODO(), rule_) //服务的 负载均衡方式 轮询方式 0=random 1=round-robin 2=weight_round-robin 3=ip_hash
round_type := service_.LoadBalance.RoundType
nextAddr, err := load_balance.LoadBalanceConfig.GetService(context.TODO(),round_type, ServiceName,lib.ClientIP(c.Request))
if err != nil || nextAddr == "" { if err != nil || nextAddr == "" {
err = errors.New("get next addr fail") err = errors.New(fmt.Sprintf("从etcd中获取服务 %s 失败",ServiceName))
log.Error("%s", err) log.Error("%s", err)
return nil,err return nil,err
} }
director := func(req *http.Request) { director := func(req *http.Request) {
target, err := url.Parse("http://"+nextAddr) if strings.HasPrefix(nextAddr,"http://") || strings.HasPrefix(nextAddr,"https://") {
target, err = url.Parse(nextAddr)
}else{
target, err = url.Parse("http://"+nextAddr)
}
if err != nil { if err != nil {
log.Error("func NewMultipleHostsReverseProxy 匿名函数director url.Parse地址解析失败 失败 %s",err) log.Error("func NewMultipleHostsReverseProxy 匿名函数director url.Parse地址解析失败 失败 %s",err)
} }
......
...@@ -29,9 +29,15 @@ var( ...@@ -29,9 +29,15 @@ var(
func Init(reg registry.Registry) *LoadBalanceEtcdConf{ func Init(reg registry.Registry) *LoadBalanceEtcdConf{
once.Do(func() { once.Do(func() {
LoadBalanceConfig = &LoadBalanceEtcdConf{} LoadBalanceConfig = &LoadBalanceEtcdConf{}
rb := LoadBanlanceFactory(LbWeightRoundRobin) rb0 := LoadBanlanceFactory(LbRandom)
rb1 := LoadBanlanceFactory(LbRoundRobin)
rb2 := LoadBanlanceFactory(LbWeightRoundRobin)
rb3 := LoadBanlanceFactory(LbConsistentHash)
//添加负载均衡器到配置中 //添加负载均衡器到配置中
LoadBalanceConfig.Attach(rb) LoadBalanceConfig.Attach(rb0)
LoadBalanceConfig.Attach(rb1)
LoadBalanceConfig.Attach(rb2)
LoadBalanceConfig.Attach(rb3)
LoadBalanceConfig.registry = reg LoadBalanceConfig.registry = reg
allServiceInfo := &registry.AllServiceInfo{ allServiceInfo := &registry.AllServiceInfo{
ServiceMap: make(map[string]*registry.Service), ServiceMap: make(map[string]*registry.Service),
...@@ -61,22 +67,26 @@ func (s *LoadBalanceEtcdConf) GetLoadBalanceList() *registry.AllServiceInfo{ ...@@ -61,22 +67,26 @@ func (s *LoadBalanceEtcdConf) GetLoadBalanceList() *registry.AllServiceInfo{
return s.value.Load().(*registry.AllServiceInfo) return s.value.Load().(*registry.AllServiceInfo)
} }
func (s *LoadBalanceEtcdConf) GetLoadBalance() LoadBalance{ func (s *LoadBalanceEtcdConf) GetLoadBalance(round_type int) LoadBalance{
return s.observers[0].(LoadBalance) return s.observers[round_type].(LoadBalance)
} }
func (s *LoadBalanceEtcdConf) GetRegistry() registry.Registry{ func (s *LoadBalanceEtcdConf) GetRegistry() registry.Registry{
return s.registry return s.registry
} }
func (s *LoadBalanceEtcdConf) GetService(ctx context.Context,name string)(node string,err error){ func (s *LoadBalanceEtcdConf) GetService(ctx context.Context,round_type int,name string,ip string)(node string,err error){
regService := s.GetRegistry() regService := s.GetRegistry()
_, err = regService.GetService(context.TODO(),name) _, err = regService.GetService(context.TODO(),name)
if err != nil { if err != nil {
fmt.Printf("get service failed, err:%v", err) fmt.Printf("get service failed, err:%v", err)
return return
} }
node ,err =s.GetLoadBalance().Get(name) //轮询方式 0=random 1=round-robin 2=weight_round-robin 3=ip_hash
if round_type > 3{
round_type=2
}
node ,err =s.GetLoadBalance(round_type).Get(name,ip)
return node,err return node,err
} }
......
...@@ -5,6 +5,7 @@ import ( ...@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/syyongx/php2go" "github.com/syyongx/php2go"
"hash/crc32" "hash/crc32"
"log"
"net" "net"
"sort" "sort"
"strconv" "strconv"
...@@ -116,17 +117,13 @@ func (c *ConsistentHashBanlance) getLocalIP() (ipv4 string, err error) { ...@@ -116,17 +117,13 @@ func (c *ConsistentHashBanlance) getLocalIP() (ipv4 string, err error) {
// Get 方法根据给定的对象获取最靠近它的那个节点 // Get 方法根据给定的对象获取最靠近它的那个节点
func (c *ConsistentHashBanlance) Get(key ...string) (string, error) { func (c *ConsistentHashBanlance) Get(key ...string) (string, error) {
if c.IsEmpty() { if c.IsEmpty() {
return "", errors.New(" node is empty") return "", errors.New(" node is empty")
} }
//hash := c.hash([]byte(key[1])) key1 := key[1];
localIP ,err := c.getLocalIP() hash := c.hash([]byte(key1))
//fmt.Println(localIP)
if err != nil{
localIP = "127.0.0.1"
}
hash := c.hash([]byte(localIP))
serviceName := key[0] serviceName := key[0]
...@@ -150,6 +147,7 @@ func (c *ConsistentHashBanlance) Get(key ...string) (string, error) { ...@@ -150,6 +147,7 @@ func (c *ConsistentHashBanlance) Get(key ...string) (string, error) {
func (c *ConsistentHashBanlance) Update() { func (c *ConsistentHashBanlance) Update() {
c.lock.Lock() c.lock.Lock()
defer c.lock.Unlock() defer c.lock.Unlock()
log.Print("[INFO] 负载均衡器 ip_hash模式 更新负载均衡配置...... ")
//fmt.Println("更新负载均衡配置.....") //fmt.Println("更新负载均衡配置.....")
allServiceInfo := LoadBalanceConfig.GetLoadBalanceList() allServiceInfo := LoadBalanceConfig.GetLoadBalanceList()
if allServiceInfo == nil || allServiceInfo.ServiceMap == nil{ if allServiceInfo == nil || allServiceInfo.ServiceMap == nil{
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/syyongx/php2go" "github.com/syyongx/php2go"
"log"
"math/rand" "math/rand"
"strconv" "strconv"
"sync" "sync"
...@@ -74,6 +75,7 @@ func (r *RandomBalance) Get(key ...string) (string, error) { ...@@ -74,6 +75,7 @@ func (r *RandomBalance) Get(key ...string) (string, error) {
func (r *RandomBalance) Update() { func (r *RandomBalance) Update() {
r.lock.Lock() r.lock.Lock()
defer r.lock.Unlock() defer r.lock.Unlock()
log.Print("[INFO] 负载均衡器 随机模式 更新负载均衡配置...... ")
//fmt.Println("更新负载均衡配置.....") //fmt.Println("更新负载均衡配置.....")
allServiceInfo := LoadBalanceConfig.GetLoadBalanceList() allServiceInfo := LoadBalanceConfig.GetLoadBalanceList()
if allServiceInfo == nil || allServiceInfo.ServiceMap == nil{ if allServiceInfo == nil || allServiceInfo.ServiceMap == nil{
......
...@@ -3,6 +3,7 @@ package load_balance ...@@ -3,6 +3,7 @@ package load_balance
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"sync" "sync"
//_ "github.com/ichunt2019/ichunt-micro-service/registry" //_ "github.com/ichunt2019/ichunt-micro-service/registry"
...@@ -90,7 +91,7 @@ func (r *RoundRobinBalance) Get(key ...string) (string, error) { ...@@ -90,7 +91,7 @@ func (r *RoundRobinBalance) Get(key ...string) (string, error) {
func (r *RoundRobinBalance) Update() { func (r *RoundRobinBalance) Update() {
r.lock.Lock() r.lock.Lock()
defer r.lock.Unlock() defer r.lock.Unlock()
log.Print("[INFO] 负载均衡器 轮询模式 更新负载均衡配置...... ")
//fmt.Println("更新负载均衡配置.....") //fmt.Println("更新负载均衡配置.....")
allServiceInfo := LoadBalanceConfig.GetLoadBalanceList() allServiceInfo := LoadBalanceConfig.GetLoadBalanceList()
if allServiceInfo == nil || allServiceInfo.ServiceMap == nil{ if allServiceInfo == nil || allServiceInfo.ServiceMap == nil{
......
...@@ -102,7 +102,7 @@ func (r *WeightRoundRobinBalance) Update() { ...@@ -102,7 +102,7 @@ func (r *WeightRoundRobinBalance) Update() {
r.lock.Lock() r.lock.Lock()
defer r.lock.Unlock() defer r.lock.Unlock()
log.Print("[INFO] 更新负载均衡配置 ") log.Print("[INFO] 负载均衡器 加权轮询模式 更新负载均衡配置...... ")
allServiceInfo := LoadBalanceConfig.GetLoadBalanceList() allServiceInfo := LoadBalanceConfig.GetLoadBalanceList()
if allServiceInfo == nil || allServiceInfo.ServiceMap == nil{ if allServiceInfo == nil || allServiceInfo.ServiceMap == nil{
return return
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment