mirror of
https://github.com/ehang-io/nps.git
synced 2025-09-02 03:16:53 +00:00
Port mux| https|tls crypt
This commit is contained in:
@@ -22,6 +22,7 @@ const (
|
||||
NEW_HOST = "host"
|
||||
CONN_TCP = "tcp"
|
||||
CONN_UDP = "udp"
|
||||
CONN_TEST = "TST"
|
||||
UnauthorizedBytes = `HTTP/1.1 401 Unauthorized
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
WWW-Authenticate: Basic realm="easyProxy"
|
||||
|
@@ -7,7 +7,9 @@ import (
|
||||
"errors"
|
||||
"github.com/cnlh/nps/lib/common"
|
||||
"github.com/cnlh/nps/lib/config"
|
||||
"github.com/cnlh/nps/lib/crypt"
|
||||
"github.com/cnlh/nps/lib/file"
|
||||
"github.com/cnlh/nps/lib/mux"
|
||||
"github.com/cnlh/nps/lib/pool"
|
||||
"github.com/cnlh/nps/lib/rate"
|
||||
"github.com/cnlh/nps/vender/github.com/xtaci/kcp"
|
||||
@@ -119,46 +121,31 @@ func (s *Conn) ReadFlag() (string, error) {
|
||||
|
||||
//设置连接为长连接
|
||||
func (s *Conn) SetAlive(tp string) {
|
||||
if tp == "kcp" {
|
||||
s.setKcpAlive()
|
||||
} else {
|
||||
s.setTcpAlive()
|
||||
switch s.Conn.(type) {
|
||||
case *kcp.UDPSession:
|
||||
s.Conn.(*kcp.UDPSession).SetReadDeadline(time.Time{})
|
||||
case *net.TCPConn:
|
||||
conn := s.Conn.(*net.TCPConn)
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
conn.SetKeepAlive(true)
|
||||
conn.SetKeepAlivePeriod(time.Duration(2 * time.Second))
|
||||
case *mux.PortConn:
|
||||
s.Conn.(*mux.PortConn).SetReadDeadline(time.Time{})
|
||||
}
|
||||
}
|
||||
|
||||
//设置连接为长连接
|
||||
func (s *Conn) setTcpAlive() {
|
||||
conn := s.Conn.(*net.TCPConn)
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
conn.SetKeepAlive(true)
|
||||
conn.SetKeepAlivePeriod(time.Duration(2 * time.Second))
|
||||
}
|
||||
|
||||
//设置连接为长连接
|
||||
func (s *Conn) setKcpAlive() {
|
||||
conn := s.Conn.(*kcp.UDPSession)
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
}
|
||||
|
||||
//设置连接为长连接
|
||||
func (s *Conn) SetReadDeadline(t time.Duration, tp string) {
|
||||
if tp == "kcp" {
|
||||
s.SetKcpReadDeadline(t)
|
||||
} else {
|
||||
s.SetTcpReadDeadline(t)
|
||||
switch s.Conn.(type) {
|
||||
case *kcp.UDPSession:
|
||||
s.Conn.(*kcp.UDPSession).SetReadDeadline(time.Now().Add(time.Duration(t) * time.Second))
|
||||
case *net.TCPConn:
|
||||
s.Conn.(*net.TCPConn).SetReadDeadline(time.Now().Add(time.Duration(t) * time.Second))
|
||||
case *mux.PortConn:
|
||||
s.Conn.(*mux.PortConn).SetReadDeadline(time.Now().Add(time.Duration(t) * time.Second))
|
||||
}
|
||||
}
|
||||
|
||||
//set read dead time
|
||||
func (s *Conn) SetTcpReadDeadline(t time.Duration) {
|
||||
s.Conn.(*net.TCPConn).SetReadDeadline(time.Now().Add(time.Duration(t) * time.Second))
|
||||
}
|
||||
|
||||
//set read dead time
|
||||
func (s *Conn) SetKcpReadDeadline(t time.Duration) {
|
||||
s.Conn.(*kcp.UDPSession).SetReadDeadline(time.Now().Add(time.Duration(t) * time.Second))
|
||||
}
|
||||
|
||||
//send info for link
|
||||
func (s *Conn) SendLinkInfo(link *Link) (int, error) {
|
||||
raw := bytes.NewBuffer([]byte{})
|
||||
@@ -402,19 +389,19 @@ func SetUdpSession(sess *kcp.UDPSession) {
|
||||
}
|
||||
|
||||
//conn1 mux conn
|
||||
func CopyWaitGroup(conn1, conn2 io.ReadWriteCloser, crypt bool, snappy bool, rate *rate.Rate, flow *file.Flow) {
|
||||
func CopyWaitGroup(conn1, conn2 net.Conn, crypt bool, snappy bool, rate *rate.Rate, flow *file.Flow, isServer bool) {
|
||||
var in, out int64
|
||||
var wg sync.WaitGroup
|
||||
conn1 = GetConn(conn1, crypt, snappy, rate)
|
||||
connHandle := GetConn(conn1, crypt, snappy, rate, isServer)
|
||||
go func(in *int64) {
|
||||
wg.Add(1)
|
||||
*in, _ = common.CopyBuffer(conn1, conn2)
|
||||
conn1.Close()
|
||||
*in, _ = common.CopyBuffer(connHandle, conn2)
|
||||
connHandle.Close()
|
||||
conn2.Close()
|
||||
wg.Done()
|
||||
}(&in)
|
||||
out, _ = common.CopyBuffer(conn2, conn1)
|
||||
conn1.Close()
|
||||
out, _ = common.CopyBuffer(conn2, connHandle)
|
||||
connHandle.Close()
|
||||
conn2.Close()
|
||||
wg.Wait()
|
||||
if flow != nil {
|
||||
@@ -423,11 +410,14 @@ func CopyWaitGroup(conn1, conn2 io.ReadWriteCloser, crypt bool, snappy bool, rat
|
||||
}
|
||||
|
||||
//get crypt or snappy conn
|
||||
func GetConn(conn io.ReadWriteCloser, crypt, snappy bool, rate *rate.Rate) (io.ReadWriteCloser) {
|
||||
if crypt {
|
||||
conn = NewCryptConn(conn, true, rate)
|
||||
func GetConn(conn net.Conn, cpt, snappy bool, rate *rate.Rate, isServer bool) (io.ReadWriteCloser) {
|
||||
if cpt {
|
||||
if isServer {
|
||||
return crypt.NewTlsServerConn(conn)
|
||||
}
|
||||
return crypt.NewTlsClientConn(conn)
|
||||
} else if snappy {
|
||||
conn = NewSnappyConn(conn, crypt, rate)
|
||||
return NewSnappyConn(conn, cpt, rate)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
@@ -1,72 +0,0 @@
|
||||
package conn
|
||||
|
||||
import (
|
||||
"github.com/cnlh/nps/lib/crypt"
|
||||
"github.com/cnlh/nps/lib/pool"
|
||||
"github.com/cnlh/nps/lib/rate"
|
||||
"io"
|
||||
)
|
||||
|
||||
type CryptConn struct {
|
||||
conn io.ReadWriteCloser
|
||||
crypt bool
|
||||
rate *rate.Rate
|
||||
}
|
||||
|
||||
func NewCryptConn(conn io.ReadWriteCloser, crypt bool, rate *rate.Rate) *CryptConn {
|
||||
c := new(CryptConn)
|
||||
c.conn = conn
|
||||
c.crypt = crypt
|
||||
c.rate = rate
|
||||
return c
|
||||
}
|
||||
|
||||
//加密写
|
||||
func (s *CryptConn) Write(b []byte) (n int, err error) {
|
||||
n = len(b)
|
||||
if s.crypt {
|
||||
if b, err = crypt.AesEncrypt(b, []byte(cryptKey)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if b, err = GetLenBytes(b); err != nil {
|
||||
return
|
||||
}
|
||||
_, err = s.conn.Write(b)
|
||||
if s.rate != nil {
|
||||
s.rate.Get(int64(n))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//解密读
|
||||
func (s *CryptConn) Read(b []byte) (n int, err error) {
|
||||
var lens int
|
||||
var buf []byte
|
||||
var rb []byte
|
||||
if lens, err = GetLen(s.conn); err != nil || lens > len(b) || lens < 0 {
|
||||
return
|
||||
}
|
||||
buf = pool.BufPool.Get().([]byte)
|
||||
defer pool.BufPool.Put(buf)
|
||||
if n, err = io.ReadFull(s.conn, buf[:lens]); err != nil {
|
||||
return
|
||||
}
|
||||
if s.crypt {
|
||||
if rb, err = crypt.AesDecrypt(buf[:lens], []byte(cryptKey)); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
rb = buf[:lens]
|
||||
}
|
||||
copy(b, rb)
|
||||
n = len(rb)
|
||||
if s.rate != nil {
|
||||
s.rate.Get(int64(n))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *CryptConn) Close() error {
|
||||
return s.conn.Close()
|
||||
}
|
28
lib/crypt/tls.go
Normal file
28
lib/crypt/tls.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package crypt
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"github.com/cnlh/nps/vender/github.com/astaxie/beego"
|
||||
"github.com/cnlh/nps/vender/github.com/astaxie/beego/logs"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func NewTlsServerConn(conn net.Conn) net.Conn {
|
||||
cert, err := tls.LoadX509KeyPair(filepath.Join(beego.AppPath, "conf", "server.pem"), filepath.Join(beego.AppPath, "conf", "server.key"))
|
||||
if err != nil {
|
||||
logs.Error(err)
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
config := &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
return tls.Server(conn, config)
|
||||
}
|
||||
|
||||
func NewTlsClientConn(conn net.Conn) net.Conn {
|
||||
conf := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
return tls.Client(conn, conf)
|
||||
}
|
@@ -225,6 +225,7 @@ func (s *Csv) StoreHostToCsv() {
|
||||
strconv.Itoa(host.Id),
|
||||
strconv.Itoa(int(host.Flow.ExportFlow)),
|
||||
strconv.Itoa(int(host.Flow.InletFlow)),
|
||||
host.Scheme,
|
||||
}
|
||||
err1 := writer.Write(record)
|
||||
if err1 != nil {
|
||||
@@ -298,6 +299,11 @@ func (s *Csv) LoadHostFromCsv() {
|
||||
post.Flow = new(Flow)
|
||||
post.Flow.ExportFlow = int64(common.GetIntNoErrByStr(item[8]))
|
||||
post.Flow.InletFlow = int64(common.GetIntNoErrByStr(item[9]))
|
||||
if len(item) > 10 {
|
||||
post.Scheme = item[10]
|
||||
} else {
|
||||
post.Scheme = "all"
|
||||
}
|
||||
hosts = append(hosts, post)
|
||||
if post.Id > s.HostIncreaseId {
|
||||
s.HostIncreaseId = post.Id
|
||||
@@ -319,7 +325,7 @@ func (s *Csv) DelHost(id int) error {
|
||||
|
||||
func (s *Csv) IsHostExist(h *Host) bool {
|
||||
for _, v := range s.Hosts {
|
||||
if v.Host == h.Host && h.Location == v.Location {
|
||||
if v.Host == h.Host && h.Location == v.Location && (v.Scheme == "all" || v.Scheme == h.Scheme) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -497,7 +503,7 @@ func (s *Csv) GetInfoByHost(host string, r *http.Request) (h *Host, err error) {
|
||||
if re, err = regexp.Compile(tmp); err != nil {
|
||||
return
|
||||
}
|
||||
if len(re.FindAllString(host, -1)) > 0 {
|
||||
if len(re.FindAllString(host, -1)) > 0 && (v.Scheme == "all" || v.Scheme == r.URL.Scheme) {
|
||||
//URL routing
|
||||
hosts = append(hosts, v)
|
||||
}
|
||||
|
@@ -133,6 +133,7 @@ type Host struct {
|
||||
NowIndex int
|
||||
TargetArr []string
|
||||
NoStore bool
|
||||
Scheme string //http https all
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
|
63
lib/mux/pconn.go
Normal file
63
lib/mux/pconn.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PortConn struct {
|
||||
Conn net.Conn
|
||||
rs []byte
|
||||
start int
|
||||
}
|
||||
|
||||
func newPortConn(conn net.Conn, rs []byte) *PortConn {
|
||||
return &PortConn{
|
||||
Conn: conn,
|
||||
rs: rs,
|
||||
}
|
||||
}
|
||||
|
||||
func (pConn *PortConn) Read(b []byte) (n int, err error) {
|
||||
if len(b) < len(pConn.rs)-pConn.start {
|
||||
defer func() {
|
||||
pConn.start = pConn.start + len(b)
|
||||
}()
|
||||
return copy(b, pConn.rs), nil
|
||||
}
|
||||
if pConn.start < len(pConn.rs) {
|
||||
defer func() {
|
||||
pConn.start = len(pConn.rs)
|
||||
}()
|
||||
return copy(b, pConn.rs[pConn.start:]), nil
|
||||
}
|
||||
return pConn.Conn.Read(b)
|
||||
}
|
||||
|
||||
func (pConn *PortConn) Write(b []byte) (n int, err error) {
|
||||
return pConn.Conn.Write(b)
|
||||
}
|
||||
|
||||
func (pConn *PortConn) Close() error {
|
||||
return pConn.Conn.Close()
|
||||
}
|
||||
|
||||
func (pConn *PortConn) LocalAddr() net.Addr {
|
||||
return pConn.Conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (pConn *PortConn) RemoteAddr() net.Addr {
|
||||
return pConn.Conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func (pConn *PortConn) SetDeadline(t time.Time) error {
|
||||
return pConn.Conn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (pConn *PortConn) SetReadDeadline(t time.Time) error {
|
||||
return pConn.Conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (pConn *PortConn) SetWriteDeadline(t time.Time) error {
|
||||
return pConn.Conn.SetWriteDeadline(t)
|
||||
}
|
44
lib/mux/plistener.go
Normal file
44
lib/mux/plistener.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
type PortListener struct {
|
||||
net.Listener
|
||||
connCh chan *PortConn
|
||||
addr net.Addr
|
||||
isClose bool
|
||||
}
|
||||
|
||||
func NewPortListener(connCh chan *PortConn, addr net.Addr) *PortListener {
|
||||
return &PortListener{
|
||||
connCh: connCh,
|
||||
addr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
func (pListener *PortListener) Accept() (net.Conn, error) {
|
||||
if pListener.isClose {
|
||||
return nil, errors.New("the listener has closed")
|
||||
}
|
||||
conn := <-pListener.connCh
|
||||
if conn != nil {
|
||||
return conn, nil
|
||||
}
|
||||
return nil, errors.New("the listener has closed")
|
||||
}
|
||||
|
||||
func (pListener *PortListener) Close() error {
|
||||
//close
|
||||
if pListener.isClose {
|
||||
return errors.New("the listener has closed")
|
||||
}
|
||||
pListener.isClose = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pListener *PortListener) Addr() net.Addr {
|
||||
return pListener.addr
|
||||
}
|
163
lib/mux/pmux.go
Normal file
163
lib/mux/pmux.go
Normal file
@@ -0,0 +1,163 @@
|
||||
// This module is used for port reuse
|
||||
// Distinguish client, web manager , HTTP and HTTPS according to the difference of protocol
|
||||
package mux
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"github.com/cnlh/nps/vender/github.com/astaxie/beego/logs"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
HTTP_GET = 716984
|
||||
HTTP_POST = 807983
|
||||
HTTP_HEAD = 726965
|
||||
HTTP_PUT = 808585
|
||||
HTTP_DELETE = 686976
|
||||
HTTP_CONNECT = 677978
|
||||
HTTP_OPTIONS = 798084
|
||||
HTTP_TRACE = 848265
|
||||
CLIENT = 848384
|
||||
)
|
||||
|
||||
type PortMux struct {
|
||||
net.Listener
|
||||
port int
|
||||
isClose bool
|
||||
managerHost string
|
||||
clientConn chan *PortConn
|
||||
httpConn chan *PortConn
|
||||
httpsConn chan *PortConn
|
||||
managerConn chan *PortConn
|
||||
}
|
||||
|
||||
func NewPortMux(port int, managerHost string) *PortMux {
|
||||
pMux := &PortMux{
|
||||
managerHost: managerHost,
|
||||
port: port,
|
||||
clientConn: make(chan *PortConn),
|
||||
httpConn: make(chan *PortConn),
|
||||
httpsConn: make(chan *PortConn),
|
||||
managerConn: make(chan *PortConn),
|
||||
}
|
||||
pMux.Start()
|
||||
return pMux
|
||||
}
|
||||
|
||||
func (pMux *PortMux) Start() error {
|
||||
// Port multiplexing is based on TCP only
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:"+strconv.Itoa(pMux.port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pMux.Listener, err = net.ListenTCP("tcp", tcpAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
conn, err := pMux.Listener.Accept()
|
||||
if err != nil {
|
||||
logs.Warn(err)
|
||||
//close
|
||||
pMux.Close()
|
||||
}
|
||||
go pMux.process(conn)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pMux *PortMux) process(conn net.Conn) {
|
||||
// Recognition according to different signs
|
||||
// read 3 byte
|
||||
buf := make([]byte, 3)
|
||||
if n, err := io.ReadFull(conn, buf); err != nil || n != 3 {
|
||||
return
|
||||
}
|
||||
var ch chan *PortConn
|
||||
var rs []byte
|
||||
var buffer bytes.Buffer
|
||||
switch bytesToNum(buf) {
|
||||
case HTTP_CONNECT, HTTP_DELETE, HTTP_GET, HTTP_HEAD, HTTP_OPTIONS, HTTP_POST, HTTP_PUT, HTTP_TRACE: //http and manager
|
||||
buffer.Reset()
|
||||
r := bufio.NewReader(conn)
|
||||
buffer.Write(buf)
|
||||
for {
|
||||
b, _, err := r.ReadLine()
|
||||
if err != nil {
|
||||
logs.Warn("read line error", err.Error())
|
||||
conn.Close()
|
||||
break
|
||||
}
|
||||
buffer.Write(b)
|
||||
buffer.Write([]byte("\r\n"))
|
||||
if strings.Index(string(b), "Host:") == 0 || strings.Index(string(b), "host:") == 0 {
|
||||
// Remove host and space effects
|
||||
str := strings.Replace(string(b), "Host:", "", -1)
|
||||
str = strings.Replace(str, "host:", "", -1)
|
||||
str = strings.TrimSpace(str)
|
||||
// Determine whether it is the same as the manager domain name
|
||||
if str == pMux.managerHost {
|
||||
ch = pMux.managerConn
|
||||
} else {
|
||||
ch = pMux.httpConn
|
||||
}
|
||||
b, _ := r.Peek(r.Buffered())
|
||||
buffer.Write(b)
|
||||
rs = buffer.Bytes()
|
||||
break
|
||||
}
|
||||
}
|
||||
case CLIENT: // client connection
|
||||
ch = pMux.clientConn
|
||||
default: // https
|
||||
ch = pMux.httpsConn
|
||||
}
|
||||
if len(rs) == 0 {
|
||||
rs = buf
|
||||
}
|
||||
ch <- newPortConn(conn, rs)
|
||||
}
|
||||
|
||||
func (pMux *PortMux) Close() error {
|
||||
if pMux.isClose {
|
||||
return errors.New("the port mux has closed")
|
||||
}
|
||||
pMux.isClose = true
|
||||
close(pMux.clientConn)
|
||||
close(pMux.httpsConn)
|
||||
close(pMux.httpConn)
|
||||
close(pMux.managerConn)
|
||||
return pMux.Listener.Close()
|
||||
}
|
||||
|
||||
func (pMux *PortMux) GetClientListener() net.Listener {
|
||||
return NewPortListener(pMux.clientConn, pMux.Listener.Addr())
|
||||
}
|
||||
|
||||
func (pMux *PortMux) GetHttpListener() net.Listener {
|
||||
return NewPortListener(pMux.httpConn, pMux.Listener.Addr())
|
||||
}
|
||||
|
||||
func (pMux *PortMux) GetHttpsListener() net.Listener {
|
||||
return NewPortListener(pMux.httpsConn, pMux.Listener.Addr())
|
||||
}
|
||||
|
||||
func (pMux *PortMux) GetManagerListener() net.Listener {
|
||||
return NewPortListener(pMux.managerConn, pMux.Listener.Addr())
|
||||
}
|
||||
|
||||
func bytesToNum(b []byte) int {
|
||||
var str string
|
||||
for i := 0; i < len(b); i++ {
|
||||
str += strconv.Itoa(int(b[i]))
|
||||
}
|
||||
x, _ := strconv.Atoi(str)
|
||||
return int(x)
|
||||
}
|
39
lib/mux/pmux_test.go
Normal file
39
lib/mux/pmux_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"github.com/cnlh/nps/vender/github.com/astaxie/beego/logs"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPortMux_Close(t *testing.T) {
|
||||
logs.Reset()
|
||||
logs.EnableFuncCallDepth(true)
|
||||
logs.SetLogFuncCallDepth(3)
|
||||
|
||||
pMux := NewPortMux(8888)
|
||||
go func() {
|
||||
if pMux.Start() != nil {
|
||||
logs.Warn("Error")
|
||||
}
|
||||
}()
|
||||
time.Sleep(time.Second * 3)
|
||||
go func() {
|
||||
l := pMux.GetHttpsAccept()
|
||||
conn, err := l.Accept()
|
||||
logs.Warn(conn, err)
|
||||
}()
|
||||
go func() {
|
||||
l := pMux.GetHttpAccept()
|
||||
conn, err := l.Accept()
|
||||
logs.Warn(conn, err)
|
||||
}()
|
||||
go func() {
|
||||
l := pMux.GetClientAccept()
|
||||
conn, err := l.Accept()
|
||||
logs.Warn(conn, err)
|
||||
}()
|
||||
l := pMux.GetManagerAccept()
|
||||
conn, err := l.Accept()
|
||||
logs.Warn(conn, err)
|
||||
}
|
@@ -1,7 +1,8 @@
|
||||
package version
|
||||
|
||||
const VERSION = "0.17.1"
|
||||
const VERSION = "0.18.0"
|
||||
|
||||
// Compulsory minimum version, Minimum downward compatibility to this version
|
||||
func GetVersion() string {
|
||||
return VERSION
|
||||
return "0.18.0"
|
||||
}
|
||||
|
Reference in New Issue
Block a user