mirror of
https://github.com/fatedier/frp.git
synced 2024-12-01 15:49:00 +08:00
commit
456ce09061
3
Makefile
3
Makefile
@ -53,6 +53,3 @@ clean:
|
||||
rm -f ./bin/frpc
|
||||
rm -f ./bin/frps
|
||||
cd ./tests && ./clean_test.sh && cd -
|
||||
|
||||
save:
|
||||
godep save ./...
|
||||
|
48
README.md
48
README.md
@ -20,6 +20,7 @@ frp is a fast reverse proxy to help you expose a local server behind a NAT or fi
|
||||
* [Visit your web service in LAN by custom domains](#visit-your-web-service-in-lan-by-custom-domains)
|
||||
* [Forward DNS query request](#forward-dns-query-request)
|
||||
* [Forward unix domain socket](#forward-unix-domain-socket)
|
||||
* [Expose a simple http file server](#expose-a-simple-http-file-server)
|
||||
* [Expose your service in security](#expose-your-service-in-security)
|
||||
* [P2P Mode](#p2p-mode)
|
||||
* [Connect website through frpc's network](#connect-website-through-frpcs-network)
|
||||
@ -214,6 +215,32 @@ Configure frps same as above.
|
||||
|
||||
`curl http://x.x.x.x:6000/version`
|
||||
|
||||
### Expose a simple http file server
|
||||
|
||||
A simple way to visit files in the LAN.
|
||||
|
||||
Configure frps same as above.
|
||||
|
||||
1. Start frpc with configurations:
|
||||
|
||||
```ini
|
||||
# frpc.ini
|
||||
[common]
|
||||
server_addr = x.x.x.x
|
||||
server_port = 7000
|
||||
|
||||
[test_static_file]
|
||||
type = tcp
|
||||
remote_port = 6000
|
||||
plugin = static_file
|
||||
plugin_local_path = /tmp/file
|
||||
plugin_strip_prefix = static
|
||||
plugin_http_user = abc
|
||||
plugin_http_passwd = abc
|
||||
```
|
||||
|
||||
2. Visit `http://x.x.x.x:6000/static/` by your browser, set correct user and password, so you can see files in `/tmp/file`.
|
||||
|
||||
### Expose your service in security
|
||||
|
||||
For some services, if expose them to the public network directly will be a security risk.
|
||||
@ -576,11 +603,26 @@ server_port = 7000
|
||||
http_proxy = http://user:pwd@192.168.1.128:8080
|
||||
```
|
||||
|
||||
### Range ports mapping
|
||||
|
||||
Proxy name has prefix `range:` will support mapping range ports.
|
||||
|
||||
```ini
|
||||
# frpc.ini
|
||||
[range:test_tcp]
|
||||
type = tcp
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 6000-6006,6007
|
||||
remote_port = 6000-6006,6007
|
||||
```
|
||||
|
||||
frpc will generate 6 proxies like `test_tcp_0, test_tcp_1 ... test_tcp_5`.
|
||||
|
||||
### Plugin
|
||||
|
||||
frpc only forward request to local tcp or udp port by default.
|
||||
|
||||
Plugin is used for providing rich features. There are built-in plugins such as **unix_domain_socket**, **http_proxy**, **socks5** and you can see [example usage](#example-usage).
|
||||
Plugin is used for providing rich features. There are built-in plugins such as `unix_domain_socket`, `http_proxy`, `socks5`, `static_file` and you can see [example usage](#example-usage).
|
||||
|
||||
Specify which plugin to use by `plugin` parameter. Configuration parameters of plugin should be started with `plugin_`. `local_ip` and `local_port` is useless for plugin.
|
||||
|
||||
@ -598,17 +640,13 @@ plugin_http_passwd = abc
|
||||
|
||||
`plugin_http_user` and `plugin_http_passwd` are configuration parameters used in `http_proxy` plugin.
|
||||
|
||||
|
||||
## Development Plan
|
||||
|
||||
* Log http request information in frps.
|
||||
* Direct reverse proxy, like haproxy.
|
||||
* Load balance to different service in frpc.
|
||||
* Frpc can directly be a webserver for static files.
|
||||
* P2p communicate by making udp hole to penetrate NAT.
|
||||
* kubernetes ingress support.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Interested in getting involved? We would like to help you!
|
||||
|
57
README_zh.md
57
README_zh.md
@ -18,6 +18,7 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp
|
||||
* [通过自定义域名访问部署于内网的 web 服务](#通过自定义域名访问部署于内网的-web-服务)
|
||||
* [转发 DNS 查询请求](#转发-dns-查询请求)
|
||||
* [转发 Unix域套接字](#转发-unix域套接字)
|
||||
* [对外提供简单的文件访问服务](#对外提供简单的文件访问服务)
|
||||
* [安全地暴露内网服务](#安全地暴露内网服务)
|
||||
* [点对点内网穿透](#点对点内网穿透)
|
||||
* [通过 frpc 所在机器访问外网](#通过-frpc-所在机器访问外网)
|
||||
@ -39,6 +40,7 @@ frp 是一个可用于内网穿透的高性能的反向代理应用,支持 tcp
|
||||
* [自定义二级域名](#自定义二级域名)
|
||||
* [URL 路由](#url-路由)
|
||||
* [通过代理连接 frps](#通过代理连接-frps)
|
||||
* [范围端口映射](#范围端口映射)
|
||||
* [插件](#插件)
|
||||
* [开发计划](#开发计划)
|
||||
* [为 frp 做贡献](#为-frp-做贡献)
|
||||
@ -192,11 +194,11 @@ DNS 查询请求通常使用 UDP 协议,frp 支持对内网 UDP 服务的穿
|
||||
|
||||
### 转发 Unix域套接字
|
||||
|
||||
通过 tcp 端口访问内网的 unix域套接字(和 docker daemon 通信)。
|
||||
通过 tcp 端口访问内网的 unix域套接字(例如和 docker daemon 通信)。
|
||||
|
||||
frps 的部署步骤同上。
|
||||
|
||||
1. 启动 frpc,启用 unix_domain_socket 插件,配置如下:
|
||||
1. 启动 frpc,启用 `unix_domain_socket` 插件,配置如下:
|
||||
|
||||
```ini
|
||||
# frpc.ini
|
||||
@ -215,6 +217,34 @@ frps 的部署步骤同上。
|
||||
|
||||
`curl http://x.x.x.x:6000/version`
|
||||
|
||||
### 对外提供简单的文件访问服务
|
||||
|
||||
通过 `static_file` 插件可以对外提供一个简单的基于 HTTP 的文件访问服务。
|
||||
|
||||
frps 的部署步骤同上。
|
||||
|
||||
1. 启动 frpc,启用 `static_file` 插件,配置如下:
|
||||
|
||||
```ini
|
||||
# frpc.ini
|
||||
[common]
|
||||
server_addr = x.x.x.x
|
||||
server_port = 7000
|
||||
|
||||
[test_static_file]
|
||||
type = tcp
|
||||
remote_port = 6000
|
||||
plugin = static_file
|
||||
# 要对外暴露的文件目录
|
||||
plugin_local_path = /tmp/file
|
||||
# 访问 url 中会被去除的前缀,保留的内容即为要访问的文件路径
|
||||
plugin_strip_prefix = static
|
||||
plugin_http_user = abc
|
||||
plugin_http_passwd = abc
|
||||
```
|
||||
|
||||
2. 通过浏览器访问 `http://x.x.x.x:6000/static/` 来查看位于 `/tmp/file` 目录下的文件,会要求输入已设置好的用户名和密码。
|
||||
|
||||
### 安全地暴露内网服务
|
||||
|
||||
对于某些服务来说如果直接暴露于公网上将会存在安全隐患。
|
||||
@ -609,11 +639,30 @@ server_port = 7000
|
||||
http_proxy = http://user:pwd@192.168.1.128:8080
|
||||
```
|
||||
|
||||
### 范围端口映射
|
||||
|
||||
在 frpc 的配置文件中可以指定映射多个端口,目前只支持 tcp 和 udp 的类型。
|
||||
|
||||
这一功能通过 `range:` 段落标记来实现,客户端会解析这个标记中的配置,将其拆分成多个 proxy,每一个 proxy 以数字为后缀命名。
|
||||
|
||||
例如要映射本地 6000-6005, 6007 这6个端口,主要配置如下:
|
||||
|
||||
```ini
|
||||
# frpc.ini
|
||||
[range:test_tcp]
|
||||
type = tcp
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 6000-6006,6007
|
||||
remote_port = 6000-6006,6007
|
||||
```
|
||||
|
||||
实际连接成功后会创建 6 个 proxy,命名为 `test_tcp_0, test_tcp_1 ... test_tcp_5`。
|
||||
|
||||
### 插件
|
||||
|
||||
默认情况下,frpc 只会转发请求到本地 tcp 或 udp 端口。
|
||||
|
||||
插件模式是为了在客户端提供更加丰富的功能,目前内置的插件有 **unix_domain_socket**、**http_proxy**、**socks5**。具体使用方式请查看[使用示例](#使用示例)。
|
||||
插件模式是为了在客户端提供更加丰富的功能,目前内置的插件有 `unix_domain_socket`、`http_proxy`、`socks5`、`static_file`。具体使用方式请查看[使用示例](#使用示例)。
|
||||
|
||||
通过 `plugin` 指定需要使用的插件,插件的配置参数都以 `plugin_` 开头。使用插件后 `local_ip` 和 `local_port` 不再需要配置。
|
||||
|
||||
@ -638,8 +687,6 @@ plugin_http_passwd = abc
|
||||
* frps 记录 http 请求日志。
|
||||
* frps 支持直接反向代理,类似 haproxy。
|
||||
* frpc 支持负载均衡到后端不同服务。
|
||||
* frpc 支持直接作为 webserver 访问指定静态页面。
|
||||
* 支持 udp 打洞的方式,提供两边内网机器直接通信,流量不经过服务器转发。
|
||||
* 集成对 k8s 等平台的支持。
|
||||
|
||||
## 为 frp 做贡献
|
||||
|
@ -89,8 +89,8 @@ func NewControl(svr *Service, pxyCfgs map[string]config.ProxyConf, visitorCfgs m
|
||||
ctl := &Control{
|
||||
svr: svr,
|
||||
loginMsg: loginMsg,
|
||||
sendCh: make(chan msg.Message, 10),
|
||||
readCh: make(chan msg.Message, 10),
|
||||
sendCh: make(chan msg.Message, 100),
|
||||
readCh: make(chan msg.Message, 100),
|
||||
closedCh: make(chan int),
|
||||
readerShutdown: shutdown.New(),
|
||||
writerShutdown: shutdown.New(),
|
||||
@ -98,7 +98,7 @@ func NewControl(svr *Service, pxyCfgs map[string]config.ProxyConf, visitorCfgs m
|
||||
Logger: log.NewPrefixLogger(""),
|
||||
}
|
||||
ctl.pm = NewProxyManager(ctl, ctl.sendCh, "")
|
||||
ctl.pm.Reload(pxyCfgs, visitorCfgs)
|
||||
ctl.pm.Reload(pxyCfgs, visitorCfgs, false)
|
||||
return ctl
|
||||
}
|
||||
|
||||
@ -124,7 +124,7 @@ func (ctl *Control) Run() (err error) {
|
||||
|
||||
// start all local visitors and send NewProxy message for all configured proxies
|
||||
ctl.pm.Reset(ctl.sendCh, ctl.runId)
|
||||
ctl.pm.CheckAndStartProxy()
|
||||
ctl.pm.CheckAndStartProxy([]string{ProxyStatusNew})
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -360,20 +360,20 @@ func (ctl *Control) msgHandler() {
|
||||
// If controler is notified by closedCh, reader and writer and handler will exit, then recall these functions.
|
||||
func (ctl *Control) worker() {
|
||||
go ctl.msgHandler()
|
||||
go ctl.writer()
|
||||
go ctl.reader()
|
||||
go ctl.writer()
|
||||
|
||||
var err error
|
||||
maxDelayTime := 20 * time.Second
|
||||
delayTime := time.Second
|
||||
|
||||
checkInterval := 10 * time.Second
|
||||
checkInterval := 60 * time.Second
|
||||
checkProxyTicker := time.NewTicker(checkInterval)
|
||||
for {
|
||||
select {
|
||||
case <-checkProxyTicker.C:
|
||||
// every 10 seconds, check which proxy registered failed and reregister it to server
|
||||
ctl.pm.CheckAndStartProxy()
|
||||
// check which proxy registered failed and reregister it to server
|
||||
ctl.pm.CheckAndStartProxy([]string{ProxyStatusStartErr, ProxyStatusClosed})
|
||||
case _, ok := <-ctl.closedCh:
|
||||
// we won't get any variable from this channel
|
||||
if !ok {
|
||||
@ -413,8 +413,8 @@ func (ctl *Control) worker() {
|
||||
}
|
||||
|
||||
// init related channels and variables
|
||||
ctl.sendCh = make(chan msg.Message, 10)
|
||||
ctl.readCh = make(chan msg.Message, 10)
|
||||
ctl.sendCh = make(chan msg.Message, 100)
|
||||
ctl.readCh = make(chan msg.Message, 100)
|
||||
ctl.closedCh = make(chan int)
|
||||
ctl.readerShutdown = shutdown.New()
|
||||
ctl.writerShutdown = shutdown.New()
|
||||
@ -427,7 +427,7 @@ func (ctl *Control) worker() {
|
||||
go ctl.reader()
|
||||
|
||||
// start all configured proxies
|
||||
ctl.pm.CheckAndStartProxy()
|
||||
ctl.pm.CheckAndStartProxy([]string{ProxyStatusNew, ProxyStatusClosed})
|
||||
|
||||
checkProxyTicker.Stop()
|
||||
checkProxyTicker = time.NewTicker(checkInterval)
|
||||
@ -437,6 +437,6 @@ func (ctl *Control) worker() {
|
||||
}
|
||||
|
||||
func (ctl *Control) reloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.ProxyConf) error {
|
||||
err := ctl.pm.Reload(pxyCfgs, visitorCfgs)
|
||||
err := ctl.pm.Reload(pxyCfgs, visitorCfgs, true)
|
||||
return err
|
||||
}
|
||||
|
@ -12,10 +12,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ProxyStatusNew = "new"
|
||||
ProxyStatusStartErr = "start error"
|
||||
ProxyStatusRunning = "running"
|
||||
ProxyStatusClosed = "closed"
|
||||
ProxyStatusNew = "new"
|
||||
ProxyStatusStartErr = "start error"
|
||||
ProxyStatusWaitStart = "wait start"
|
||||
ProxyStatusRunning = "running"
|
||||
ProxyStatusClosed = "closed"
|
||||
)
|
||||
|
||||
type ProxyManager struct {
|
||||
@ -69,14 +70,10 @@ func NewProxyWrapper(cfg config.ProxyConf) *ProxyWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
func (pw *ProxyWrapper) IsRunning() bool {
|
||||
func (pw *ProxyWrapper) GetStatusStr() string {
|
||||
pw.mu.RLock()
|
||||
defer pw.mu.RUnlock()
|
||||
if pw.Status == ProxyStatusRunning {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return pw.Status
|
||||
}
|
||||
|
||||
func (pw *ProxyWrapper) GetStatus() *ProxyStatus {
|
||||
@ -93,6 +90,12 @@ func (pw *ProxyWrapper) GetStatus() *ProxyStatus {
|
||||
return ps
|
||||
}
|
||||
|
||||
func (pw *ProxyWrapper) WaitStart() {
|
||||
pw.mu.Lock()
|
||||
defer pw.mu.Unlock()
|
||||
pw.Status = ProxyStatusWaitStart
|
||||
}
|
||||
|
||||
func (pw *ProxyWrapper) Start(remoteAddr string, serverRespErr string) error {
|
||||
if pw.pxy != nil {
|
||||
pw.pxy.Close()
|
||||
@ -210,7 +213,8 @@ func (pm *ProxyManager) CloseProxies() {
|
||||
}
|
||||
}
|
||||
|
||||
func (pm *ProxyManager) CheckAndStartProxy() {
|
||||
// pxyStatus: check and start proxies in which status
|
||||
func (pm *ProxyManager) CheckAndStartProxy(pxyStatus []string) {
|
||||
pm.mu.RLock()
|
||||
defer pm.mu.RUnlock()
|
||||
if pm.closed {
|
||||
@ -219,13 +223,18 @@ func (pm *ProxyManager) CheckAndStartProxy() {
|
||||
}
|
||||
|
||||
for _, pxy := range pm.proxies {
|
||||
if !pxy.IsRunning() {
|
||||
var newProxyMsg msg.NewProxy
|
||||
pxy.Cfg.UnMarshalToMsg(&newProxyMsg)
|
||||
err := pm.sendMsg(&newProxyMsg)
|
||||
if err != nil {
|
||||
pm.Warn("[%s] proxy send NewProxy message error")
|
||||
return
|
||||
status := pxy.GetStatusStr()
|
||||
for _, s := range pxyStatus {
|
||||
if status == s {
|
||||
var newProxyMsg msg.NewProxy
|
||||
pxy.Cfg.UnMarshalToMsg(&newProxyMsg)
|
||||
err := pm.sendMsg(&newProxyMsg)
|
||||
if err != nil {
|
||||
pm.Warn("[%s] proxy send NewProxy message error")
|
||||
return
|
||||
}
|
||||
pxy.WaitStart()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -245,9 +254,14 @@ func (pm *ProxyManager) CheckAndStartProxy() {
|
||||
}
|
||||
}
|
||||
|
||||
func (pm *ProxyManager) Reload(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.ProxyConf) error {
|
||||
func (pm *ProxyManager) Reload(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.ProxyConf, startNow bool) error {
|
||||
pm.mu.Lock()
|
||||
defer pm.mu.Unlock()
|
||||
defer func() {
|
||||
pm.mu.Unlock()
|
||||
if startNow {
|
||||
go pm.CheckAndStartProxy([]string{ProxyStatusNew})
|
||||
}
|
||||
}()
|
||||
if pm.closed {
|
||||
err := fmt.Errorf("Reload error: ProxyManager is closed now")
|
||||
pm.Warn(err.Error())
|
||||
|
@ -99,7 +99,7 @@ func main() {
|
||||
if args["status"] != nil {
|
||||
if args["status"].(bool) {
|
||||
if err = CmdStatus(); err != nil {
|
||||
fmt.Printf("frps get status error: %v\n", err)
|
||||
fmt.Printf("frpc get status error: %v\n", err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
os.Exit(0)
|
||||
|
@ -73,6 +73,16 @@ local_port = 22
|
||||
# if remote_port is 0, frps will assgin a random port for you
|
||||
remote_port = 0
|
||||
|
||||
# if you want tp expose multiple ports, add 'range:' prefix to the section name
|
||||
# frpc will generate multiple proxies such as 'tcp_port_6010', 'tcp_port_6011' and so on.
|
||||
[range:tcp_port]
|
||||
type = tcp
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 6010-6020,6022,6024-6028
|
||||
remote_port = 6010-6020,6022,6024-6028
|
||||
use_encryption = false
|
||||
use_compression = false
|
||||
|
||||
[dns]
|
||||
type = udp
|
||||
local_ip = 114.114.114.114
|
||||
@ -81,6 +91,14 @@ remote_port = 6002
|
||||
use_encryption = false
|
||||
use_compression = false
|
||||
|
||||
[range:udp_port]
|
||||
type = udp
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 6010-6020
|
||||
remote_port = 6010-6020
|
||||
use_encryption = false
|
||||
use_compression = false
|
||||
|
||||
# Resolve your domain names to [server_addr] so you can use http://web01.yourdomain.com to browse web01 and http://web02.yourdomain.com to browse web02
|
||||
[web01]
|
||||
type = http
|
||||
@ -124,6 +142,22 @@ plugin = http_proxy
|
||||
plugin_http_user = abc
|
||||
plugin_http_passwd = abc
|
||||
|
||||
[plugin_socks5]
|
||||
type = tcp
|
||||
remote_port = 6005
|
||||
plugin = socks5
|
||||
plugin_user = abc
|
||||
plugin_passwd = abc
|
||||
|
||||
[plugin_static_file]
|
||||
type = tcp
|
||||
remote_port = 6006
|
||||
plugin = static_file
|
||||
plugin_local_path = /var/www/blog
|
||||
plugin_strip_prefix = static
|
||||
plugin_http_user = abc
|
||||
plugin_http_passwd = abc
|
||||
|
||||
[secret_tcp]
|
||||
# If the type is secret tcp, remote_port is useless
|
||||
# Who want to connect local port should deploy another frpc with stcp proxy and role is visitor
|
||||
|
@ -52,6 +52,9 @@ privilege_allow_ports = 2000-3000,3001,3003,4000-50000
|
||||
# pool_count in each proxy will change to max_pool_count if they exceed the maximum value
|
||||
max_pool_count = 5
|
||||
|
||||
# max ports can be used for each client, default value is 0 means no limit
|
||||
max_ports_per_client = 0
|
||||
|
||||
# authentication_timeout means the timeout interval (seconds) when the frpc connects frps
|
||||
# if authentication_timeout is zero, the time is not verified, default is 900s
|
||||
authentication_timeout = 900
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/fatedier/frp/models/consts"
|
||||
"github.com/fatedier/frp/models/msg"
|
||||
"github.com/fatedier/frp/utils/util"
|
||||
|
||||
ini "github.com/vaughan0/go-ini"
|
||||
)
|
||||
@ -770,6 +771,38 @@ func (cfg *XtcpProxyConf) Check() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func ParseRangeSection(name string, section ini.Section) (sections map[string]ini.Section, err error) {
|
||||
localPorts, errRet := util.ParseRangeNumbers(section["local_port"])
|
||||
if errRet != nil {
|
||||
err = fmt.Errorf("Parse conf error: range section [%s] local_port invalid, %v", name, errRet)
|
||||
return
|
||||
}
|
||||
|
||||
remotePorts, errRet := util.ParseRangeNumbers(section["remote_port"])
|
||||
if errRet != nil {
|
||||
err = fmt.Errorf("Parse conf error: range section [%s] remote_port invalid, %v", name, errRet)
|
||||
return
|
||||
}
|
||||
if len(localPorts) != len(remotePorts) {
|
||||
err = fmt.Errorf("Parse conf error: range section [%s] local ports number should be same with remote ports number", name)
|
||||
return
|
||||
}
|
||||
if len(localPorts) == 0 {
|
||||
err = fmt.Errorf("Parse conf error: range section [%s] local_port and remote_port is necessary")
|
||||
return
|
||||
}
|
||||
|
||||
sections = make(map[string]ini.Section)
|
||||
for i, port := range localPorts {
|
||||
subName := fmt.Sprintf("%s_%d", name, i)
|
||||
subSection := copySection(section)
|
||||
subSection["local_port"] = fmt.Sprintf("%d", port)
|
||||
subSection["remote_port"] = fmt.Sprintf("%d", remotePorts[i])
|
||||
sections[subName] = subSection
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// if len(startProxy) is 0, start all
|
||||
// otherwise just start proxies in startProxy map
|
||||
func LoadProxyConfFromFile(prefix string, conf ini.File, startProxy map[string]struct{}) (
|
||||
@ -786,22 +819,51 @@ func LoadProxyConfFromFile(prefix string, conf ini.File, startProxy map[string]s
|
||||
proxyConfs = make(map[string]ProxyConf)
|
||||
visitorConfs = make(map[string]ProxyConf)
|
||||
for name, section := range conf {
|
||||
if name == "common" {
|
||||
continue
|
||||
}
|
||||
|
||||
_, shouldStart := startProxy[name]
|
||||
if name != "common" && (startAll || shouldStart) {
|
||||
if !startAll && !shouldStart {
|
||||
continue
|
||||
}
|
||||
|
||||
subSections := make(map[string]ini.Section)
|
||||
|
||||
if strings.HasPrefix(name, "range:") {
|
||||
// range section
|
||||
rangePrefix := strings.TrimSpace(strings.TrimPrefix(name, "range:"))
|
||||
subSections, err = ParseRangeSection(rangePrefix, section)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
subSections[name] = section
|
||||
}
|
||||
|
||||
for subName, subSection := range subSections {
|
||||
// some proxy or visotr configure may be used this prefix
|
||||
section["prefix"] = prefix
|
||||
cfg, err := NewProxyConfFromFile(name, section)
|
||||
subSection["prefix"] = prefix
|
||||
cfg, err := NewProxyConfFromFile(subName, subSection)
|
||||
if err != nil {
|
||||
return proxyConfs, visitorConfs, err
|
||||
}
|
||||
|
||||
role := section["role"]
|
||||
role := subSection["role"]
|
||||
if role == "visitor" {
|
||||
visitorConfs[prefix+name] = cfg
|
||||
visitorConfs[prefix+subName] = cfg
|
||||
} else {
|
||||
proxyConfs[prefix+name] = cfg
|
||||
proxyConfs[prefix+subName] = cfg
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func copySection(section ini.Section) (out ini.Section) {
|
||||
out = make(ini.Section)
|
||||
for k, v := range section {
|
||||
out[k] = v
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ import (
|
||||
"strings"
|
||||
|
||||
ini "github.com/vaughan0/go-ini"
|
||||
|
||||
"github.com/fatedier/frp/utils/util"
|
||||
)
|
||||
|
||||
var ServerCommonCfg *ServerCommonConf
|
||||
@ -57,6 +59,7 @@ type ServerCommonConf struct {
|
||||
|
||||
PrivilegeAllowPorts map[int]struct{}
|
||||
MaxPoolCount int64
|
||||
MaxPortsPerClient int64
|
||||
HeartBeatTimeout int64
|
||||
UserConnTimeout int64
|
||||
}
|
||||
@ -87,6 +90,7 @@ func GetDefaultServerCommonConf() *ServerCommonConf {
|
||||
TcpMux: true,
|
||||
PrivilegeAllowPorts: make(map[int]struct{}),
|
||||
MaxPoolCount: 5,
|
||||
MaxPortsPerClient: 0,
|
||||
HeartBeatTimeout: 90,
|
||||
UserConnTimeout: 10,
|
||||
}
|
||||
@ -238,55 +242,46 @@ func LoadServerCommonConf(conf ini.File) (cfg *ServerCommonConf, err error) {
|
||||
allowPortsStr, ok := conf.Get("common", "privilege_allow_ports")
|
||||
if ok {
|
||||
// e.g. 1000-2000,2001,2002,3000-4000
|
||||
portRanges := strings.Split(allowPortsStr, ",")
|
||||
for _, portRangeStr := range portRanges {
|
||||
// 1000-2000 or 2001
|
||||
portArray := strings.Split(portRangeStr, "-")
|
||||
// length: only 1 or 2 is correct
|
||||
rangeType := len(portArray)
|
||||
if rangeType == 1 {
|
||||
// single port
|
||||
singlePort, errRet := strconv.ParseInt(portArray[0], 10, 64)
|
||||
if errRet != nil {
|
||||
err = fmt.Errorf("Parse conf error: privilege_allow_ports is incorrect, %v", errRet)
|
||||
return
|
||||
}
|
||||
cfg.PrivilegeAllowPorts[int(singlePort)] = struct{}{}
|
||||
} else if rangeType == 2 {
|
||||
// range ports
|
||||
min, errRet := strconv.ParseInt(portArray[0], 10, 64)
|
||||
if errRet != nil {
|
||||
err = fmt.Errorf("Parse conf error: privilege_allow_ports is incorrect, %v", errRet)
|
||||
return
|
||||
}
|
||||
max, errRet := strconv.ParseInt(portArray[1], 10, 64)
|
||||
if errRet != nil {
|
||||
err = fmt.Errorf("Parse conf error: privilege_allow_ports is incorrect, %v", errRet)
|
||||
return
|
||||
}
|
||||
if max < min {
|
||||
err = fmt.Errorf("Parse conf error: privilege_allow_ports range incorrect")
|
||||
return
|
||||
}
|
||||
for i := min; i <= max; i++ {
|
||||
cfg.PrivilegeAllowPorts[int(i)] = struct{}{}
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf("Parse conf error: privilege_allow_ports is incorrect")
|
||||
return
|
||||
}
|
||||
ports, errRet := util.ParseRangeNumbers(allowPortsStr)
|
||||
if errRet != nil {
|
||||
err = fmt.Errorf("Parse conf error: privilege_allow_ports: %v", errRet)
|
||||
return
|
||||
}
|
||||
|
||||
for _, port := range ports {
|
||||
cfg.PrivilegeAllowPorts[int(port)] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tmpStr, ok = conf.Get("common", "max_pool_count")
|
||||
if ok {
|
||||
v, err = strconv.ParseInt(tmpStr, 10, 64)
|
||||
if err == nil && v >= 0 {
|
||||
if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil {
|
||||
err = fmt.Errorf("Parse conf error: invalid max_pool_count")
|
||||
return
|
||||
} else {
|
||||
if v < 0 {
|
||||
err = fmt.Errorf("Parse conf error: invalid max_pool_count")
|
||||
return
|
||||
}
|
||||
cfg.MaxPoolCount = v
|
||||
}
|
||||
}
|
||||
|
||||
tmpStr, ok = conf.Get("common", "max_ports_per_client")
|
||||
if ok {
|
||||
if v, err = strconv.ParseInt(tmpStr, 10, 64); err != nil {
|
||||
err = fmt.Errorf("Parse conf error: invalid max_ports_per_client")
|
||||
return
|
||||
} else {
|
||||
if v < 0 {
|
||||
err = fmt.Errorf("Parse conf error: invalid max_ports_per_client")
|
||||
return
|
||||
}
|
||||
cfg.MaxPortsPerClient = v
|
||||
}
|
||||
}
|
||||
|
||||
tmpStr, ok = conf.Get("common", "authentication_timeout")
|
||||
if ok {
|
||||
v, errRet := strconv.ParseInt(tmpStr, 10, 64)
|
||||
|
@ -17,14 +17,11 @@ package plugin
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/fatedier/frp/utils/errors"
|
||||
frpIo "github.com/fatedier/frp/utils/io"
|
||||
frpNet "github.com/fatedier/frp/utils/net"
|
||||
)
|
||||
@ -35,47 +32,6 @@ func init() {
|
||||
Register(PluginHttpProxy, NewHttpProxyPlugin)
|
||||
}
|
||||
|
||||
type Listener struct {
|
||||
conns chan net.Conn
|
||||
closed bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewProxyListener() *Listener {
|
||||
return &Listener{
|
||||
conns: make(chan net.Conn, 64),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) Accept() (net.Conn, error) {
|
||||
conn, ok := <-l.conns
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("listener closed")
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (l *Listener) PutConn(conn net.Conn) error {
|
||||
err := errors.PanicToError(func() {
|
||||
l.conns <- conn
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *Listener) Close() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if !l.closed {
|
||||
close(l.conns)
|
||||
l.closed = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) Addr() net.Addr {
|
||||
return (*net.TCPAddr)(nil)
|
||||
}
|
||||
|
||||
type HttpProxy struct {
|
||||
l *Listener
|
||||
s *http.Server
|
||||
|
@ -17,7 +17,10 @@ package plugin
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/fatedier/frp/utils/errors"
|
||||
frpNet "github.com/fatedier/frp/utils/net"
|
||||
)
|
||||
|
||||
@ -45,3 +48,44 @@ type Plugin interface {
|
||||
Handle(conn io.ReadWriteCloser, realConn frpNet.Conn)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type Listener struct {
|
||||
conns chan net.Conn
|
||||
closed bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewProxyListener() *Listener {
|
||||
return &Listener{
|
||||
conns: make(chan net.Conn, 64),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) Accept() (net.Conn, error) {
|
||||
conn, ok := <-l.conns
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("listener closed")
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (l *Listener) PutConn(conn net.Conn) error {
|
||||
err := errors.PanicToError(func() {
|
||||
l.conns <- conn
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *Listener) Close() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if !l.closed {
|
||||
close(l.conns)
|
||||
l.closed = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) Addr() net.Addr {
|
||||
return (*net.TCPAddr)(nil)
|
||||
}
|
||||
|
@ -32,13 +32,23 @@ func init() {
|
||||
|
||||
type Socks5Plugin struct {
|
||||
Server *gosocks5.Server
|
||||
|
||||
user string
|
||||
passwd string
|
||||
}
|
||||
|
||||
func NewSocks5Plugin(params map[string]string) (p Plugin, err error) {
|
||||
sp := &Socks5Plugin{}
|
||||
sp.Server, err = gosocks5.New(&gosocks5.Config{
|
||||
user := params["plugin_user"]
|
||||
passwd := params["plugin_passwd"]
|
||||
|
||||
cfg := &gosocks5.Config{
|
||||
Logger: log.New(ioutil.Discard, "", log.LstdFlags),
|
||||
})
|
||||
}
|
||||
if user != "" || passwd != "" {
|
||||
cfg.Credentials = gosocks5.StaticCredentials(map[string]string{user: passwd})
|
||||
}
|
||||
sp := &Socks5Plugin{}
|
||||
sp.Server, err = gosocks5.New(cfg)
|
||||
p = sp
|
||||
return
|
||||
}
|
||||
|
87
models/plugin/static_file.go
Normal file
87
models/plugin/static_file.go
Normal file
@ -0,0 +1,87 @@
|
||||
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
frpNet "github.com/fatedier/frp/utils/net"
|
||||
)
|
||||
|
||||
const PluginStaticFile = "static_file"
|
||||
|
||||
func init() {
|
||||
Register(PluginStaticFile, NewStaticFilePlugin)
|
||||
}
|
||||
|
||||
type StaticFilePlugin struct {
|
||||
localPath string
|
||||
stripPrefix string
|
||||
httpUser string
|
||||
httpPasswd string
|
||||
|
||||
l *Listener
|
||||
s *http.Server
|
||||
}
|
||||
|
||||
func NewStaticFilePlugin(params map[string]string) (Plugin, error) {
|
||||
localPath := params["plugin_local_path"]
|
||||
stripPrefix := params["plugin_strip_prefix"]
|
||||
httpUser := params["plugin_http_user"]
|
||||
httpPasswd := params["plugin_http_passwd"]
|
||||
|
||||
listener := NewProxyListener()
|
||||
|
||||
sp := &StaticFilePlugin{
|
||||
localPath: localPath,
|
||||
stripPrefix: stripPrefix,
|
||||
httpUser: httpUser,
|
||||
httpPasswd: httpPasswd,
|
||||
|
||||
l: listener,
|
||||
}
|
||||
var prefix string
|
||||
if stripPrefix != "" {
|
||||
prefix = "/" + stripPrefix + "/"
|
||||
} else {
|
||||
prefix = "/"
|
||||
}
|
||||
router := httprouter.New()
|
||||
router.Handler("GET", prefix+"*filepath", frpNet.MakeHttpGzipHandler(
|
||||
frpNet.NewHttpBasicAuthWraper(http.StripPrefix(prefix, http.FileServer(http.Dir(localPath))), httpUser, httpPasswd)))
|
||||
sp.s = &http.Server{
|
||||
Handler: router,
|
||||
}
|
||||
go sp.s.Serve(listener)
|
||||
return sp, nil
|
||||
}
|
||||
|
||||
func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn frpNet.Conn) {
|
||||
wrapConn := frpNet.WrapReadWriteCloserToConn(conn, realConn)
|
||||
sp.l.PutConn(wrapConn)
|
||||
}
|
||||
|
||||
func (sp *StaticFilePlugin) Name() string {
|
||||
return PluginStaticFile
|
||||
}
|
||||
|
||||
func (sp *StaticFilePlugin) Close() error {
|
||||
sp.s.Close()
|
||||
sp.l.Close()
|
||||
return nil
|
||||
}
|
@ -55,6 +55,9 @@ type Control struct {
|
||||
// pool count
|
||||
poolCount int
|
||||
|
||||
// ports used, for limitations
|
||||
portsUsedNum int
|
||||
|
||||
// last time got the Ping message
|
||||
lastPing time.Time
|
||||
|
||||
@ -84,6 +87,7 @@ func NewControl(svr *Service, ctlConn net.Conn, loginMsg *msg.Login) *Control {
|
||||
workConnCh: make(chan net.Conn, loginMsg.PoolCount+10),
|
||||
proxies: make(map[string]Proxy),
|
||||
poolCount: loginMsg.PoolCount,
|
||||
portsUsedNum: 0,
|
||||
lastPing: time.Now(),
|
||||
runId: loginMsg.RunId,
|
||||
status: consts.Working,
|
||||
@ -348,6 +352,26 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
|
||||
return remoteAddr, err
|
||||
}
|
||||
|
||||
// Check ports used number in each client
|
||||
if config.ServerCommonCfg.MaxPortsPerClient > 0 {
|
||||
ctl.mu.Lock()
|
||||
if ctl.portsUsedNum+pxy.GetUsedPortsNum() > int(config.ServerCommonCfg.MaxPortsPerClient) {
|
||||
ctl.mu.Unlock()
|
||||
err = fmt.Errorf("exceed the max_ports_per_client")
|
||||
return
|
||||
}
|
||||
ctl.portsUsedNum = ctl.portsUsedNum + pxy.GetUsedPortsNum()
|
||||
ctl.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
ctl.mu.Lock()
|
||||
ctl.portsUsedNum = ctl.portsUsedNum - pxy.GetUsedPortsNum()
|
||||
ctl.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
remoteAddr, err = pxy.Run()
|
||||
if err != nil {
|
||||
return
|
||||
@ -371,16 +395,21 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
|
||||
|
||||
func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) {
|
||||
ctl.mu.Lock()
|
||||
defer ctl.mu.Unlock()
|
||||
|
||||
pxy, ok := ctl.proxies[closeMsg.ProxyName]
|
||||
if !ok {
|
||||
ctl.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if config.ServerCommonCfg.MaxPortsPerClient > 0 {
|
||||
ctl.portsUsedNum = ctl.portsUsedNum - pxy.GetUsedPortsNum()
|
||||
}
|
||||
pxy.Close()
|
||||
ctl.svr.DelProxy(pxy.GetName())
|
||||
delete(ctl.proxies, closeMsg.ProxyName)
|
||||
ctl.mu.Unlock()
|
||||
|
||||
StatsCloseProxy(pxy.GetName(), pxy.GetConf().GetBaseInfo().ProxyType)
|
||||
return
|
||||
}
|
||||
|
@ -40,15 +40,18 @@ type Proxy interface {
|
||||
GetName() string
|
||||
GetConf() config.ProxyConf
|
||||
GetWorkConnFromPool() (workConn frpNet.Conn, err error)
|
||||
GetUsedPortsNum() int
|
||||
Close()
|
||||
log.Logger
|
||||
}
|
||||
|
||||
type BaseProxy struct {
|
||||
name string
|
||||
ctl *Control
|
||||
listeners []frpNet.Listener
|
||||
mu sync.RWMutex
|
||||
name string
|
||||
ctl *Control
|
||||
listeners []frpNet.Listener
|
||||
usedPortsNum int
|
||||
|
||||
mu sync.RWMutex
|
||||
log.Logger
|
||||
}
|
||||
|
||||
@ -60,6 +63,10 @@ func (pxy *BaseProxy) GetControl() *Control {
|
||||
return pxy.ctl
|
||||
}
|
||||
|
||||
func (pxy *BaseProxy) GetUsedPortsNum() int {
|
||||
return pxy.usedPortsNum
|
||||
}
|
||||
|
||||
func (pxy *BaseProxy) Close() {
|
||||
pxy.Info("proxy closing")
|
||||
for _, l := range pxy.listeners {
|
||||
@ -126,6 +133,7 @@ func NewProxy(ctl *Control, pxyConf config.ProxyConf) (pxy Proxy, err error) {
|
||||
}
|
||||
switch cfg := pxyConf.(type) {
|
||||
case *config.TcpProxyConf:
|
||||
basePxy.usedPortsNum = 1
|
||||
pxy = &TcpProxy{
|
||||
BaseProxy: basePxy,
|
||||
cfg: cfg,
|
||||
@ -141,6 +149,7 @@ func NewProxy(ctl *Control, pxyConf config.ProxyConf) (pxy Proxy, err error) {
|
||||
cfg: cfg,
|
||||
}
|
||||
case *config.UdpProxyConf:
|
||||
basePxy.usedPortsNum = 1
|
||||
pxy = &UdpProxy{
|
||||
BaseProxy: basePxy,
|
||||
cfg: cfg,
|
||||
|
@ -161,3 +161,9 @@ remote_port = 0
|
||||
type = tcp
|
||||
plugin = http_proxy
|
||||
remote_port = 0
|
||||
|
||||
[range:range_tcp]
|
||||
type = tcp
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 30000-30001,30003
|
||||
remote_port = 30000-30001,30003
|
||||
|
@ -5,5 +5,5 @@ vhost_http_port = 10804
|
||||
log_file = ./frps.log
|
||||
log_level = debug
|
||||
privilege_token = 123456
|
||||
privilege_allow_ports = 10000-20000,20002,30000-40000
|
||||
privilege_allow_ports = 10000-20000,20002,30000-50000
|
||||
subdomain_host = sub.com
|
||||
|
@ -53,8 +53,9 @@ var (
|
||||
ProxyUdpPortNotAllowed string = "udp_port_not_allowed"
|
||||
ProxyUdpPortNormal string = "udp_port_normal"
|
||||
ProxyUdpRandomPort string = "udp_random_port"
|
||||
ProxyHttpProxy string = "http_proxy"
|
||||
|
||||
ProxyHttpProxy string = "http_proxy"
|
||||
ProxyRangeTcpPrefix string = "range_tcp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -286,3 +287,15 @@ func TestPluginHttpProxy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRangePortsMapping(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
name := fmt.Sprintf("%s_%d", ProxyRangeTcpPrefix, i)
|
||||
status, err := getProxyStatus(name)
|
||||
if assert.NoError(err) {
|
||||
assert.Equal(client.ProxyStatusRunning, status.Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RandId return a rand string used in frp.
|
||||
@ -54,3 +56,48 @@ func CanonicalAddr(host string, port int) (addr string) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseRangeNumbers(rangeStr string) (numbers []int64, err error) {
|
||||
rangeStr = strings.TrimSpace(rangeStr)
|
||||
numbers = make([]int64, 0)
|
||||
// e.g. 1000-2000,2001,2002,3000-4000
|
||||
numRanges := strings.Split(rangeStr, ",")
|
||||
for _, numRangeStr := range numRanges {
|
||||
// 1000-2000 or 2001
|
||||
numArray := strings.Split(numRangeStr, "-")
|
||||
// length: only 1 or 2 is correct
|
||||
rangeType := len(numArray)
|
||||
if rangeType == 1 {
|
||||
// single number
|
||||
singleNum, errRet := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64)
|
||||
if errRet != nil {
|
||||
err = fmt.Errorf("range number is invalid, %v", errRet)
|
||||
return
|
||||
}
|
||||
numbers = append(numbers, singleNum)
|
||||
} else if rangeType == 2 {
|
||||
// range numbers
|
||||
min, errRet := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64)
|
||||
if errRet != nil {
|
||||
err = fmt.Errorf("range number is invalid, %v", errRet)
|
||||
return
|
||||
}
|
||||
max, errRet := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64)
|
||||
if errRet != nil {
|
||||
err = fmt.Errorf("range number is invalid, %v", errRet)
|
||||
return
|
||||
}
|
||||
if max < min {
|
||||
err = fmt.Errorf("range number is invalid")
|
||||
return
|
||||
}
|
||||
for i := min; i <= max; i++ {
|
||||
numbers = append(numbers, i)
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf("range number is invalid")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -20,3 +20,29 @@ func TestGetAuthKey(t *testing.T) {
|
||||
t.Log(key)
|
||||
assert.Equal("6df41a43725f0c770fd56379e12acf8c", key)
|
||||
}
|
||||
|
||||
func TestParseRangeNumbers(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
numbers, err := ParseRangeNumbers("2-5")
|
||||
if assert.NoError(err) {
|
||||
assert.Equal([]int64{2, 3, 4, 5}, numbers)
|
||||
}
|
||||
|
||||
numbers, err = ParseRangeNumbers("1")
|
||||
if assert.NoError(err) {
|
||||
assert.Equal([]int64{1}, numbers)
|
||||
}
|
||||
|
||||
numbers, err = ParseRangeNumbers("3-5,8")
|
||||
if assert.NoError(err) {
|
||||
assert.Equal([]int64{3, 4, 5, 8}, numbers)
|
||||
}
|
||||
|
||||
numbers, err = ParseRangeNumbers(" 3-5,8, 10-12 ")
|
||||
if assert.NoError(err) {
|
||||
assert.Equal([]int64{3, 4, 5, 8, 10, 11, 12}, numbers)
|
||||
}
|
||||
|
||||
_, err = ParseRangeNumbers("3-a")
|
||||
assert.Error(err)
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var version string = "0.15.1"
|
||||
var version string = "0.16.0"
|
||||
|
||||
func Full() string {
|
||||
return version
|
||||
|
Loading…
Reference in New Issue
Block a user