@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2021 Li4n0
|
||||
|
||||
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.
|
||||
@@ -1,4 +1,244 @@
|
||||
# RevSuit - A Reverse Platform Developed in Golang
|
||||
# RevSuit - A Flexible and Powerful Reverse Connection Platform
|
||||
|
||||
## Overview
|
||||
RevSuit is an open-sourced Reverse Platform designed for receive connection from target host in bug bounty.
|
||||
|
||||
RevSuit is a flexible and powerful reverse connection platform designed for receiving connection from target host in
|
||||
penetration. It currently supports HTTP, DNS, RMI, MySQL and FTP protocols.
|
||||
|
||||
Flexible:
|
||||
|
||||
1. Revsuit can set flexible rules to capture different connections.
|
||||
2. Deploy and run via binaries without worrying about nasty dependency errors.
|
||||
|
||||
Powerful:
|
||||
|
||||
1. Revsuit allows users to customize the response for different connections through rules and supports dynamic response
|
||||
generation using template variables.
|
||||
2. Support a variety of protocols, and support the in-depth utilization of some protocols, such as DNS rebinding, MySQL
|
||||
Load Local Data, JDBC Deserialize Exploit, FTP Passive Mode SSRF, etc.
|
||||
3. Native support for use with scanners.
|
||||
4. Support push notifications to popular office software.
|
||||
5. Other thoughtful and detailed features.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Preparation
|
||||
|
||||
Download the latest release firstly.
|
||||
|
||||
RevSuit will generate default configuration file on first run. Modify the configuration file according to your needs,
|
||||
then re-run.
|
||||
|
||||
To confirm the location of the connection IP, if there is no qqwry.dat in the current directory or its modification time
|
||||
is more than 5 days ago. RevSuit will download it.(Support for GeoIP is in the planning stages).
|
||||
|
||||
```bash
|
||||
$ ./revsuit
|
||||
2021/05/16 22:55:10 [ INFO] Downloading qqwry.dat...
|
||||
|
||||
____ _____ _ __
|
||||
/ __ \___ _ __/ ___/__ __(_) /_
|
||||
/ /_/ / _ \ | / /\__ \/ / / / / __/
|
||||
/ _, _/ __/ |/ /___/ / /_/ / / /_
|
||||
/_/ |_|\___/|___//____/\__,_/_/\__/
|
||||
vBeta0.1
|
||||
https://revsuit.pro
|
||||
2021/05/16 22:55:22 [ INFO] Starting HTTP Server at :80, token:your_token
|
||||
2021/05/16 22:55:22 [ INFO] Start to listen FTP PASV port at :2020, PasvIP is 10.9.8.7
|
||||
2021/05/16 22:55:22 [ INFO] Starting FTP Server at :21
|
||||
2021/05/16 22:55:22 [ INFO] Starting MySQL Server at :3306
|
||||
2021/05/16 22:55:22 [ INFO] Starting RMI Server at :1099
|
||||
2021/05/16 22:55:22 [ INFO] Starting DNS Server at :53
|
||||
```
|
||||
|
||||
After running, you can visit the `/revsuit/admin/` path of HTTP Server and enter the token to access the control panel.
|
||||
|
||||
### Create Rules and Receive Connection
|
||||
|
||||
Take the creation of HTTP rules as an example:
|
||||
|
||||
A few notes are as follows:
|
||||
|
||||
1. The `name` and `flagFormat` of rule are unique.
|
||||
2. `FlagFormat` uses regular expression syntax, and for different protocols, the fields matched by flagFormat are
|
||||
different, you can check the corresponding hints to get details when creating rules.
|
||||
3. Rules with high rank will take effect first.
|
||||
4. For different protocol rules, you can make different custom configurations, and for some protocols, template
|
||||
variables can be used.
|
||||
5. You can set the named grouping of regular expressions in `flagFormat`, and the result of the matching grouping will
|
||||
also be used as a template variable.
|
||||
|
||||
As shown below, we create a rule that uses the built-in template variables and custom template variables of the http
|
||||
protocol, and names it `test_create_rule`:
|
||||
|
||||

|
||||
|
||||
Then make a request that satisfies the rule and view the response.
|
||||
|
||||

|
||||
|
||||
The request will be logged on the platform at the same time.
|
||||
|
||||

|
||||
|
||||
### Notice
|
||||
|
||||
If you want to be notified of new connections on your office software, you can configure the webhook address of the
|
||||
relevant software in the configuration file and turn on the `Notice` switch for the corresponding rule. Only support to
|
||||
four software types currently: dingtalk,wechat,lark,slack.(Discord and telegram support is in the plan.)
|
||||
|
||||
### Import/Export Rules
|
||||
|
||||
If you're migrating platforms or purging data, it can be a hassle to recreate rules. That's why the platform supports
|
||||
import and export of rules.
|
||||
|
||||
The entry point for this function is located at `Settings>RULES`.
|
||||
|
||||
The rules are stored in yaml format for import and export, like the following:
|
||||
|
||||
```yaml
|
||||
http:
|
||||
- name: test_create_rule
|
||||
flag_format: (?P<what>\w+)\?
|
||||
rank: 0
|
||||
push_to_client: false
|
||||
notice: false
|
||||
response_status_code: "302"
|
||||
response_headers:
|
||||
Location: ${query.url}
|
||||
response_body: ${header.say} ${what}
|
||||
- name: other_rule
|
||||
flag_format: other
|
||||
rank: 1
|
||||
push_to_client: false
|
||||
notice: true
|
||||
response_status_code: "200"
|
||||
response_headers: { }
|
||||
response_body: Hello Revsuit!
|
||||
dns:
|
||||
... ...
|
||||
```
|
||||
|
||||
## Advance Usage
|
||||
|
||||
### Use with Scanner
|
||||
|
||||
RevSuit was split from my scanner project, so its native support works with scanners.
|
||||
|
||||
From RevSuit's perspective, we call a scanner a client.
|
||||
|
||||
#### Create Connection
|
||||
|
||||
RevSuit
|
||||
uses [HTTP Server-sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
|
||||
(SSE) to establish a one-way communication channel with the client.
|
||||
|
||||
The API for the channel is: `/revsuit/api/events?message` .The client first needs to add the `Token: your token` header
|
||||
to the Header, and then access the API to establish the channel. When the platform receives a new connection, the `flag`
|
||||
captured by the rule will be passed to the client through this channel.
|
||||

|
||||
|
||||
Here is a [simple demo](https://gist.github.com/Li4n0/21aa0bec2d626114a729ca2677efb05a) using Golang's sse library as an
|
||||
example.
|
||||
|
||||
#### Multi-client
|
||||
|
||||
RevSuit supports multiple clients, and each client in the connected state receives a push of `flag`, so distributed
|
||||
scanning can be supported.
|
||||
|
||||
#### Temporary storage queue
|
||||
|
||||
RevSuit will store `flag` temporarily in the queue when there is no client connection and send it when the client
|
||||
connects, so you don't have to worry about missing the vulnerability because the client disconnects. (This is especially
|
||||
helpful for discovering delay-triggered vulnerabilities.)
|
||||
|
||||
#### Use flagGroup
|
||||
|
||||
`FlagGroup` is the content matched by the anonymous group in the `flagFormat` field of the rule. The platform will check
|
||||
the content matched in the grouping,and the flag is only pushed to the client when then content(`flagGroup`) captured
|
||||
for the first time.
|
||||
|
||||
In a real-world vulnerability scanning scenario, you may send a large number of different payloads for a single
|
||||
vulnerability point, and they may all be valid, which can result in the backlink platform receiving many requests, yet
|
||||
they are caused by the same vulnerability. If you don't want the client to receive so many `flags` for the same
|
||||
vulnerability, you can take advantage of the `flagGroup` feature of rule's flagFormat.
|
||||
|
||||
For example, SSRF scanning.
|
||||
|
||||
Firstly create a rule like the following:
|
||||
|
||||
```yaml
|
||||
http:
|
||||
- name: ssrf
|
||||
flag_format: (ssrf[a-z0-9]{6})[0-9]{1,3}
|
||||
rank: 0
|
||||
push_to_client: false
|
||||
notice: false
|
||||
response_status_code: "200"
|
||||
response_headers: { }
|
||||
response_body: "Here is a SSRF!"
|
||||
```
|
||||
|
||||
Suppose our target is `https://www.testvuln.com?url=api.com&p=useless`, and for SSRF we have 5 payloads. The final
|
||||
request sent by the scanner may
|
||||
be `['https://www.testvuln.com?url=http://revsuit.com/ssrfa98oni1&p=useless, ... ,'https://www.testvuln.com?url=//revsuit.com/ssrfa98oni5&p=useless`']
|
||||
. They may all attack successfully.
|
||||
|
||||
However, because anonymous grouping is used in the rule's `flagFormat`, The platform will query the
|
||||
connected `flagGroup`, in this case `ssrfa98oni`, and push `flag` to the client only at its first appearance, so the
|
||||
client will only receive one flag:`ssrfa98oni1`. But this is enough, because it already proves that the `url` parameter
|
||||
of the target is vulnerable.
|
||||
|
||||
### Multi-module collocation
|
||||
|
||||
In actual penetration testing scenarios, certain tasks can be done easily and quickly by combining and matching various
|
||||
modules of RevSuit. The following is an example of a blind XXE scenario, showing how to use RevSuit's HTTP and FTP
|
||||
modules, combined with template variables, to quickly complete a port scan.
|
||||
|
||||
First use create an HTTP rule to return evil.dtd, customize the response to the contents of dtd so that it goes to
|
||||
connect to RevSuit's FTP service, and use template variables to pass the Host and Port to be scanned to FTP via FTP's
|
||||
user and password.
|
||||
|
||||

|
||||
|
||||
Then create an FTP rule that receives the Host and port to be scanned from the user and password template variables, set
|
||||
to Pasv Address.
|
||||
|
||||

|
||||
|
||||
Then use BurpSuit to launch the scan, by setting the host and port parameters in the evil.dtd URL to set the target of
|
||||
the port scan.
|
||||
|
||||

|
||||
|
||||
The running effect is as follows:
|
||||
|
||||

|
||||
|
||||
It because the FTP connection will be crashed if `Passive Address` is not accessible, we can determine whether the port
|
||||
is open or not based on whether the connection exits normally.For this example, we successfully detected that port 8005
|
||||
and 8080 are open.
|
||||
|
||||
## More Usage
|
||||
|
||||
A more detailed wiki is being prepared, you can explore by yourself before it.
|
||||
|
||||
## Advice and Communication
|
||||
|
||||
Submit an issue or contact me through Weixin: `Li4n06`
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
### Reference
|
||||
|
||||
* [https://github.com/rmb122/rogue_mysql_server](https://github.com/rmb122/rogue_mysql_server)
|
||||
* [https://github.com/256dpi/newdns](https://github.com/256dpi/newdns)
|
||||
|
||||
### Specifically
|
||||
|
||||
Thanks to my friend [@E99plant](https://github.com/wuhan005/) for all the help and advice I received during the
|
||||
development of this project.
|
||||
|
||||
## License
|
||||
|
||||
@Apache License 2.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
.custom-icons-list[data-v-05a908c2] .anticon{margin-right:6px}body[data-v-4ae4fda9],html[data-v-4ae4fda9]{height:100%;margin:0}.fade-transform-enter-active[data-v-4ae4fda9],.fade-transform-leave-active[data-v-4ae4fda9]{transition:all .3s;opacity:0}.fade-transform-enter[data-v-4ae4fda9],.fade-transform-leave[data-v-4ae4fda9]{opacity:0}.fade-transform-enter-to[data-v-4ae4fda9]{opacity:0}.ant-menu-item>span>a[data-v-4ae4fda9]{color:hsla(0,0%,100%,.65)}.ant-menu-item-selected>span>a[data-v-4ae4fda9]{color:#fff}#nav[data-v-4ae4fda9]{height:100%}#nav .trigger[data-v-4ae4fda9]{font-size:18px;line-height:64px;padding:0 24px;cursor:pointer;transition:color .3s}#nav .trigger[data-v-4ae4fda9]:hover{color:#1890ff}#nav .logo[data-v-4ae4fda9]{height:32px;background:#0a1d2d;margin:16px;text-align:center;font-size:1.2rem;color:#fff;padding-bottom:5px;border-bottom:2px solid #b6befa}.copyright[data-v-4ae4fda9]{color:#888;text-align:right;margin-right:1rem}
|
||||
.custom-icons-list[data-v-05a908c2] .anticon{margin-right:6px}body[data-v-07ed630b],html[data-v-07ed630b]{height:100%;margin:0}.fade-transform-enter-active[data-v-07ed630b],.fade-transform-leave-active[data-v-07ed630b]{transition:all .3s;opacity:0}.fade-transform-enter[data-v-07ed630b],.fade-transform-leave[data-v-07ed630b]{opacity:0}.fade-transform-enter-to[data-v-07ed630b]{opacity:0}.ant-menu-item>span>a[data-v-07ed630b]{color:hsla(0,0%,100%,.65)}.ant-menu-item-selected>span>a[data-v-07ed630b]{color:#fff}#nav[data-v-07ed630b]{height:100%}#nav .trigger[data-v-07ed630b]{font-size:18px;line-height:64px;padding:0 24px;cursor:pointer;transition:color .3s}#nav .trigger[data-v-07ed630b]:hover{color:#1890ff}#nav .logo[data-v-07ed630b]{height:32px;background:#0a1d2d;margin:16px;text-align:center;font-size:1.2rem;color:#fff;padding-bottom:5px;border-bottom:2px solid #b6befa}.copyright[data-v-07ed630b]{color:#888;text-align:right;margin-right:1rem}
|
||||
@@ -0,0 +1 @@
|
||||
#add-rule[data-v-50ca3b7c]{margin-bottom:10px}
|
||||
@@ -0,0 +1 @@
|
||||
#add-rule[data-v-4fed8448]{margin-bottom:10px}
|
||||
@@ -0,0 +1 @@
|
||||
#add-rule[data-v-1f4c0376]{margin-bottom:10px}.full-screen-icon[data-v-1f4c0376]{position:absolute;z-index:5;color:#9e9e9e;right:1px;font-size:1rem;cursor:pointer;transition:font-size .1s}.full-screen-icon[data-v-1f4c0376]:hover{font-size:1.1rem;transition:font-size .1s}
|
||||
@@ -1 +0,0 @@
|
||||
#add-rule[data-v-ae6501fc]{margin-bottom:10px}
|
||||
@@ -1 +0,0 @@
|
||||
#add-rule[data-v-5aa75eae]{margin-bottom:10px}
|
||||
@@ -0,0 +1 @@
|
||||
#add-rule[data-v-1361da51]{margin-bottom:10px}
|
||||
@@ -0,0 +1 @@
|
||||
#add-rule[data-v-6f29426c]{margin-bottom:10px}
|
||||
@@ -1 +0,0 @@
|
||||
#add-rule[data-v-4bcc6b83]{margin-bottom:10px}
|
||||
@@ -1 +0,0 @@
|
||||
#add-rule[data-v-79ad429e]{margin-bottom:10px}.full-screen-icon[data-v-79ad429e]{position:absolute;z-index:5;color:#9e9e9e;right:1px;font-size:1rem;cursor:pointer;transition:font-size .1s}.full-screen-icon[data-v-79ad429e]:hover{font-size:1.1rem;transition:font-size .1s}
|
||||
@@ -1 +0,0 @@
|
||||
#add-rule[data-v-376729b6]{margin-bottom:10px}
|
||||
@@ -1 +1 @@
|
||||
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>RevSuit Control Panel</title><link href="css/chunk-1da7f23b.css" rel="prefetch"><link href="css/chunk-330a7e95.css" rel="prefetch"><link href="css/chunk-36391b77.css" rel="prefetch"><link href="css/chunk-3d13e56e.css" rel="prefetch"><link href="css/chunk-42105840.css" rel="prefetch"><link href="css/chunk-65a35ecf.css" rel="prefetch"><link href="css/chunk-7bcb83e6.css" rel="prefetch"><link href="css/chunk-b8f538aa.css" rel="prefetch"><link href="css/chunk-e039e75a.css" rel="prefetch"><link href="css/chunk-fef965c2.css" rel="prefetch"><link href="js/chunk-08f9fbe8.js" rel="prefetch"><link href="js/chunk-1da7f23b.js" rel="prefetch"><link href="js/chunk-330a7e95.js" rel="prefetch"><link href="js/chunk-36391b77.js" rel="prefetch"><link href="js/chunk-3d13e56e.js" rel="prefetch"><link href="js/chunk-42105840.js" rel="prefetch"><link href="js/chunk-65a35ecf.js" rel="prefetch"><link href="js/chunk-7bcb83e6.js" rel="prefetch"><link href="js/chunk-b241c712.js" rel="prefetch"><link href="js/chunk-b8f538aa.js" rel="prefetch"><link href="js/chunk-e039e75a.js" rel="prefetch"><link href="js/chunk-fef965c2.js" rel="prefetch"><link href="css/app.css" rel="preload" as="style"><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="js/app.js" rel="preload" as="script"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/app.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but revsuit-frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.js"></script><script src="js/app.js"></script></body></html>
|
||||
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>RevSuit Control Panel</title><link href="css/chunk-016c45cf.css" rel="prefetch"><link href="css/chunk-0cb5e9e6.css" rel="prefetch"><link href="css/chunk-1da7f23b.css" rel="prefetch"><link href="css/chunk-247d6284.css" rel="prefetch"><link href="css/chunk-330a7e95.css" rel="prefetch"><link href="css/chunk-3d13e56e.css" rel="prefetch"><link href="css/chunk-43748c28.css" rel="prefetch"><link href="css/chunk-49fbda44.css" rel="prefetch"><link href="css/chunk-e039e75a.css" rel="prefetch"><link href="css/chunk-fef965c2.css" rel="prefetch"><link href="js/chunk-016c45cf.js" rel="prefetch"><link href="js/chunk-08f9fbe8.js" rel="prefetch"><link href="js/chunk-0cb5e9e6.js" rel="prefetch"><link href="js/chunk-1da7f23b.js" rel="prefetch"><link href="js/chunk-247d6284.js" rel="prefetch"><link href="js/chunk-330a7e95.js" rel="prefetch"><link href="js/chunk-3d13e56e.js" rel="prefetch"><link href="js/chunk-43748c28.js" rel="prefetch"><link href="js/chunk-49fbda44.js" rel="prefetch"><link href="js/chunk-b241c712.js" rel="prefetch"><link href="js/chunk-e039e75a.js" rel="prefetch"><link href="js/chunk-fef965c2.js" rel="prefetch"><link href="css/app.css" rel="preload" as="style"><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="js/app.js" rel="preload" as="script"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/app.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but revsuit-frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.js"></script><script src="js/app.js"></script></body></html>
|
||||
@@ -148,6 +148,9 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (localStorage.getItem("autoRefresh") === null) {
|
||||
this.autoRefresh = true;
|
||||
}
|
||||
if (this.autoRefresh && this.isLogMode) {
|
||||
this.timing()
|
||||
}
|
||||
@@ -176,8 +179,6 @@ export default {
|
||||
isLogMode(val) {
|
||||
if (!val) {
|
||||
clearInterval(this.timer)
|
||||
} else {
|
||||
this.timing()
|
||||
}
|
||||
},
|
||||
refreshInterval(val) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<a-table
|
||||
style="overflow-x: auto;"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
@@ -139,14 +140,9 @@ export default {
|
||||
this.pagination = pagination;
|
||||
this.loading = false
|
||||
}).catch(e => {
|
||||
this.$notification.error({
|
||||
message: 'Unknown error: ' + e.response.status,
|
||||
style: {
|
||||
width: '100px',
|
||||
marginLeft: `${335 - 600}px`,
|
||||
},
|
||||
duration: 4
|
||||
});
|
||||
if (e.response.status !== 403) {
|
||||
this.$message.error('Unknown error with status code: ' + e.response.status)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<a-table
|
||||
style="overflow-x: auto;"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
@@ -228,14 +229,9 @@ export default {
|
||||
this.pagination = pagination;
|
||||
this.loading = false
|
||||
}).catch(e => {
|
||||
this.$notification.error({
|
||||
message: 'Unknown error: ' + e.response.status,
|
||||
style: {
|
||||
width: '100px',
|
||||
marginLeft: `${335 - 600}px`,
|
||||
},
|
||||
duration: 4
|
||||
});
|
||||
if (e.response.status !== 403) {
|
||||
this.$message.error('Unknown error with status code: ' + e.response.status)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<a-table
|
||||
style="overflow-x: auto;"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
@@ -104,9 +105,9 @@ const columns = [
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'PATH',
|
||||
dataIndex: 'path',
|
||||
key: 'path',
|
||||
title: 'URI',
|
||||
dataIndex: 'uri',
|
||||
key: 'uri',
|
||||
ellipsis: true,
|
||||
scopedSlots: {
|
||||
filterDropdown: 'filterDropdown',
|
||||
@@ -173,14 +174,9 @@ export default {
|
||||
this.pagination = pagination;
|
||||
this.loading = false
|
||||
}).catch(e => {
|
||||
this.$notification.error({
|
||||
message: 'Unknown error: ' + e.response.status,
|
||||
style: {
|
||||
width: '100px',
|
||||
marginLeft: `${335 - 600}px`,
|
||||
},
|
||||
duration: 4
|
||||
});
|
||||
if (e.response.status !== 403) {
|
||||
this.$message.error('Unknown error with status code: ' + e.response.status)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<a-table
|
||||
style="overflow-x: auto;"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
@@ -120,6 +121,15 @@ const columns = [
|
||||
filterIcon: 'filterIcon',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'SCHEMA',
|
||||
dataIndex: 'schema',
|
||||
key: 'schema',
|
||||
scopedSlots: {
|
||||
filterDropdown: 'filterDropdown',
|
||||
filterIcon: 'filterIcon',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'LOAD DATA',
|
||||
dataIndex: 'load_local_data',
|
||||
@@ -216,14 +226,9 @@ export default {
|
||||
this.pagination = pagination;
|
||||
this.loading = false
|
||||
}).catch(e => {
|
||||
this.$notification.error({
|
||||
message: 'Unknown error: ' + e.response.status,
|
||||
style: {
|
||||
width: '100px',
|
||||
marginLeft: `${335 - 600}px`,
|
||||
},
|
||||
duration: 4
|
||||
});
|
||||
if (e.response.status !== 403) {
|
||||
this.$message.error('Unknown error with status code: ' + e.response.status)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<a-table
|
||||
style="overflow-x: auto;"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
@@ -109,7 +110,7 @@ export default {
|
||||
onShowSizeChange: (current, size) => {
|
||||
store.pageSize = size
|
||||
}
|
||||
}, filters: {},
|
||||
}, filters: {},
|
||||
order: "desc",
|
||||
loading: false,
|
||||
columns,
|
||||
@@ -140,14 +141,9 @@ export default {
|
||||
this.pagination = pagination;
|
||||
this.loading = false
|
||||
}).catch(e => {
|
||||
this.$notification.error({
|
||||
message: 'Unknown error: ' + e.response.status,
|
||||
style: {
|
||||
width: '100px',
|
||||
marginLeft: `${335 - 600}px`,
|
||||
},
|
||||
duration: 4
|
||||
});
|
||||
if (e.response.status !== 403) {
|
||||
this.$message.error('Unknown error with status code: ' + e.response.status)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
@close="closeDrawer"
|
||||
>
|
||||
<a-form-model :model="form" ref="form" layout="vertical" @submit="handleSubmit">
|
||||
<BasicRule :form="form" :readOnly="formReadOnly"/>
|
||||
<BasicRule :form="form" :readOnly="formReadOnly" flagFormat="domain"/>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form-model-item label="Type" :rules="rules.type">
|
||||
@@ -90,6 +90,7 @@
|
||||
</a-drawer>
|
||||
<!-- rule table -->
|
||||
<a-table
|
||||
style="overflow-x: auto;"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
@@ -296,7 +297,7 @@ export default {
|
||||
onShowSizeChange: (current, size) => {
|
||||
store.pageSize = size
|
||||
}
|
||||
}, filters: {},
|
||||
}, filters: {},
|
||||
loading: false,
|
||||
columns,
|
||||
colors,
|
||||
@@ -332,14 +333,9 @@ export default {
|
||||
this.pagination = pagination;
|
||||
this.loading = false
|
||||
}).catch(e => {
|
||||
this.$notification.error({
|
||||
message: 'Unknown error: ' + e.response.status,
|
||||
style: {
|
||||
width: '100px',
|
||||
marginLeft: `${335 - 600}px`,
|
||||
},
|
||||
duration: 4
|
||||
});
|
||||
if (e.response.status !== 403) {
|
||||
this.$message.error('Unknown error with status code: ' + e.response.status)
|
||||
}
|
||||
})
|
||||
},
|
||||
clickSwitch(record, prop) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
@close="closeDrawer"
|
||||
>
|
||||
<a-form-model :model="form" ref="form" layout="vertical" @submit="handleSubmit">
|
||||
<BasicRule :form="form" :readOnly="formReadOnly"/>
|
||||
<BasicRule :form="form" :readOnly="formReadOnly" flagFormat="user or password"/>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<a-form-model-item>
|
||||
@@ -28,7 +28,7 @@
|
||||
<a-input
|
||||
v-model="form.pasv_address"
|
||||
style="width: 100%"
|
||||
placeholder="Use external IP by default"
|
||||
placeholder="Use `external_ip:pasv_port` by default"
|
||||
:readOnly="formReadOnly"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
@@ -78,6 +78,7 @@
|
||||
</a-drawer>
|
||||
<!-- rule table -->
|
||||
<a-table
|
||||
style="overflow-x: auto;"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
@@ -267,14 +268,9 @@ export default {
|
||||
this.pagination = pagination;
|
||||
this.loading = false
|
||||
}).catch(e => {
|
||||
this.$notification.error({
|
||||
message: 'Unknown error: ' + e.response.status,
|
||||
style: {
|
||||
width: '100px',
|
||||
marginLeft: `${335 - 600}px`,
|
||||
},
|
||||
duration: 4
|
||||
});
|
||||
if (e.response.status !== 403) {
|
||||
this.$message.error('Unknown error with status code: ' + e.response.status)
|
||||
}
|
||||
})
|
||||
},
|
||||
clickSwitch(record, prop) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
@close="closeDrawer"
|
||||
>
|
||||
<a-form-model :model="form" ref="form" layout="vertical" @submit="handleSubmit">
|
||||
<BasicRule :form="form" :readOnly="formReadOnly"/>
|
||||
<BasicRule :form="form" :readOnly="formReadOnly" flagFormat="raw message"/>
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="24">
|
||||
<a-form-model-item :rules="rules.response_status_code"
|
||||
@@ -130,6 +130,7 @@
|
||||
</a-drawer>
|
||||
<!-- rule table -->
|
||||
<a-table
|
||||
style="overflow-x: auto;"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
@@ -374,14 +375,9 @@ export default {
|
||||
this.pagination = pagination;
|
||||
this.loading = false
|
||||
}).catch(e => {
|
||||
this.$notification.error({
|
||||
message: 'Unknown error: ' + e.response.status,
|
||||
style: {
|
||||
width: '100px',
|
||||
marginLeft: `${335 - 600}px`,
|
||||
},
|
||||
duration: 4
|
||||
});
|
||||
if (e.response.status !== 403) {
|
||||
this.$message.error('Unknown error with status code: ' + e.response.status)
|
||||
}
|
||||
})
|
||||
},
|
||||
clickSwitch(record, prop) {
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
</a-drawer>
|
||||
<!-- rule table -->
|
||||
<a-table
|
||||
style="overflow-x: auto;"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
@@ -300,7 +301,7 @@ export default {
|
||||
this.$notification.error({
|
||||
message: 'Unknown error: ' + e.response.status,
|
||||
style: {
|
||||
width: '100px',
|
||||
width: '400px',
|
||||
marginLeft: `${335 - 600}px`,
|
||||
},
|
||||
duration: 4
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
@close="closeDrawer"
|
||||
>
|
||||
<a-form-model :model="form" ref="form" layout="vertical" @submit="handleSubmit">
|
||||
<BasicRule :form="form" :readOnly="formReadOnly"/>
|
||||
<BasicRule :form="form" :readOnly="formReadOnly" flagFormat="path"/>
|
||||
</a-form-model>
|
||||
<div
|
||||
:style="{
|
||||
@@ -38,6 +38,7 @@
|
||||
</a-drawer>
|
||||
<!-- rule table -->
|
||||
<a-table
|
||||
style="overflow-x: auto;"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
@@ -193,7 +194,8 @@ export default {
|
||||
onShowSizeChange: (current, size) => {
|
||||
store.pageSize = size
|
||||
}
|
||||
}, filters: {},
|
||||
},
|
||||
filters: {},
|
||||
loading: false,
|
||||
columns,
|
||||
form: {},
|
||||
@@ -225,14 +227,9 @@ export default {
|
||||
this.pagination = pagination;
|
||||
this.loading = false
|
||||
}).catch(e => {
|
||||
this.$notification.error({
|
||||
message: 'Unknown error: ' + e.response.status,
|
||||
style: {
|
||||
width: '100px',
|
||||
marginLeft: `${335 - 600}px`,
|
||||
},
|
||||
duration: 4
|
||||
});
|
||||
if (e.response.status !== 403) {
|
||||
this.$message.error('Unknown error with status code: ' + e.response.status)
|
||||
}
|
||||
})
|
||||
},
|
||||
clickSwitch(record, prop) {
|
||||
|
||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 895 KiB |
|
After Width: | Height: | Size: 135 KiB |
@@ -1,12 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- "1.12"
|
||||
- tip
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
before_install:
|
||||
- go get github.com/mattn/goveralls
|
||||
install:
|
||||
- go get -t ./...
|
||||
script:
|
||||
- $HOME/gopath/bin/goveralls -service=travis-ci
|
||||
@@ -1,10 +0,0 @@
|
||||
all: fmt vet lint
|
||||
|
||||
fmt:
|
||||
go fmt .
|
||||
|
||||
vet:
|
||||
go vet .
|
||||
|
||||
lint:
|
||||
golint .
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var recordChan = make(chan BaseRecord, 100)
|
||||
var recordChan = make(chan BaseRecord, 1000)
|
||||
|
||||
type Record interface {
|
||||
GetFlag() string
|
||||
|
||||
@@ -53,7 +53,7 @@ func (s *Server) getRules() []*Rule {
|
||||
return s.rules
|
||||
}
|
||||
|
||||
func (s *Server) updateRules() error {
|
||||
func (s *Server) UpdateRules() error {
|
||||
db := database.DB.Model(new(Rule))
|
||||
defer s.rulesLock.Unlock()
|
||||
s.rulesLock.Lock()
|
||||
@@ -209,7 +209,7 @@ func (s *Server) Run() {
|
||||
s.livingLock.Unlock()
|
||||
}()
|
||||
|
||||
if err := s.updateRules(); err != nil {
|
||||
if err := s.UpdateRules(); err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func (r *Rule) CreateOrUpdate() (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = GetServer().updateRules()
|
||||
err = GetServer().UpdateRules()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func (r *Rule) Delete() (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = GetServer().updateRules()
|
||||
err = GetServer().UpdateRules()
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ func (s *Server) getRules() []*Rule {
|
||||
return s.rules
|
||||
}
|
||||
|
||||
func (s *Server) updateRules() error {
|
||||
func (s *Server) UpdateRules() error {
|
||||
db := database.DB.Model(new(Rule))
|
||||
defer s.rulesLock.Unlock()
|
||||
s.rulesLock.Lock()
|
||||
@@ -94,6 +94,7 @@ func (s *Server) authenticate(user, password string) (_rule *Rule, flag, flagGro
|
||||
}
|
||||
|
||||
func (s *Server) getPasvAddressFromCache(ip, pasvAddressTpl string) (pasvAddress string) {
|
||||
pasvAddress = pasvAddressTpl
|
||||
if strings.Contains(pasvAddressTpl, ",") {
|
||||
values, ok := rebindingCache.Get(ip)
|
||||
if !ok {
|
||||
@@ -202,8 +203,8 @@ loop:
|
||||
if pasvAddress = s.getPasvAddressFromCache(ip, _rule.PasvAddress); pasvAddress == "" {
|
||||
pasvAddress = fmt.Sprintf("%s:%d", s.pasvIP, s.PasvPort)
|
||||
}
|
||||
|
||||
isRedirect = rule.CompileTpl(pasvAddress, vars) != fmt.Sprintf("%s:%d", s.pasvIP, s.PasvPort)
|
||||
pasvAddress = rule.CompileTpl(pasvAddress, vars)
|
||||
isRedirect = pasvAddress != fmt.Sprintf("%s:%d", s.pasvIP, s.PasvPort)
|
||||
case "SIZE":
|
||||
path += strings.TrimLeft(args, "/")
|
||||
if _rule == nil || isRedirect || len(_rule.Data) == 0 {
|
||||
@@ -217,8 +218,6 @@ loop:
|
||||
case "PASV":
|
||||
//Just so that ide does not prompt that there may be a nil value
|
||||
if _rule != nil {
|
||||
// return rule's pasv_address or default pasv address
|
||||
pasvAddress := rule.CompileTpl(pasvAddress, vars)
|
||||
pasvIP, pasvPort, err := net.SplitHostPort(pasvAddress)
|
||||
if err != nil {
|
||||
log.Warn("FTP failed to split rule[id:%d] pasv_address(%s) :%s", _rule.ID, pasvAddress, err)
|
||||
@@ -365,7 +364,7 @@ func (s *Server) Run() {
|
||||
s.livingLock.Unlock()
|
||||
}()
|
||||
|
||||
if err := s.updateRules(); err != nil {
|
||||
if err := s.UpdateRules(); err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (r *Rule) CreateOrUpdate() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
return GetServer().updateRules()
|
||||
return GetServer().UpdateRules()
|
||||
}
|
||||
|
||||
// Delete deletes the ftp rule in database and ruleSet
|
||||
@@ -65,7 +65,7 @@ func (r *Rule) Delete() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
return GetServer().updateRules()
|
||||
return GetServer().UpdateRules()
|
||||
}
|
||||
|
||||
// ListRules lists all ftp rules those satisfy the filter
|
||||
|
||||
@@ -48,7 +48,7 @@ func (s *Server) getRules() []*Rule {
|
||||
return s.rules
|
||||
}
|
||||
|
||||
func (s *Server) updateRules() error {
|
||||
func (s *Server) UpdateRules() error {
|
||||
db := database.DB.Model(new(Rule))
|
||||
defer s.rulesLock.Unlock()
|
||||
s.rulesLock.Lock()
|
||||
@@ -105,11 +105,11 @@ func (s *Server) ConnectionClosed(c *vmysql.Conn) {
|
||||
var clientName, clientOS, flag, flagGroup string
|
||||
|
||||
user := c.User
|
||||
schema := c.SchemaName
|
||||
supportLoadLocalData := c.SupportLoadDataLocal
|
||||
|
||||
cr, ok := s.connRulePool.Load(c.ConnectionID)
|
||||
if !ok {
|
||||
log.Warn("MySQL Connection rule(%d) not match flag", c.ConnectionID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func (s *Server) ConnectionClosed(c *vmysql.Conn) {
|
||||
}
|
||||
}
|
||||
|
||||
r, err := newRecord(_rule, flag, user, clientName, clientOS, ip, qqwry.Area(ip), supportLoadLocalData, files)
|
||||
r, err := newRecord(_rule, flag, user, schema, clientName, clientOS, ip, qqwry.Area(ip), supportLoadLocalData, files)
|
||||
if err != nil {
|
||||
log.Warn("MySQL record[rule_id: %s] created failed: %s", _rule.Name, err)
|
||||
return
|
||||
@@ -288,7 +288,7 @@ func (s *Server) Run() {
|
||||
s.Enable = false
|
||||
s.livingLock.Unlock()
|
||||
}()
|
||||
if err := s.updateRules(); err != nil {
|
||||
if err := s.UpdateRules(); err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ type Record struct {
|
||||
record.BaseRecord
|
||||
Username string `gorm:"index" form:"username" json:"username" notice:"username"`
|
||||
ClientName string `gorm:"index" form:"client_name" json:"client_name" notice:"client_name"`
|
||||
Schema string `gorm:"index" form:"schema" json:"schema" notice:"schema"`
|
||||
ClientOS string `gorm:"index" form:"client_os" json:"client_os" notice:"client_os"`
|
||||
LoadLocalData bool `gorm:"index" form:"load_local_data" json:"load_local_data" notice:"load_local_data"`
|
||||
Files []file.MySQLFile `form:"-" json:"files" notice:"-"`
|
||||
@@ -31,7 +32,7 @@ func (r Record) Notice() {
|
||||
notice.Notice(r)
|
||||
}
|
||||
|
||||
func newRecord(rule *Rule, flag, username, clientName, clientOS, remoteIp, ipArea string, supportLoadLocalData bool, files []file.MySQLFile) (r *Record, err error) {
|
||||
func newRecord(rule *Rule, flag, username, schema, clientName, clientOS, remoteIp, ipArea string, supportLoadLocalData bool, files []file.MySQLFile) (r *Record, err error) {
|
||||
r = &Record{
|
||||
BaseRecord: record.BaseRecord{
|
||||
Flag: flag,
|
||||
@@ -40,6 +41,7 @@ func newRecord(rule *Rule, flag, username, clientName, clientOS, remoteIp, ipAre
|
||||
RequestTime: time.Now(),
|
||||
},
|
||||
Username: username,
|
||||
Schema: schema,
|
||||
ClientName: clientName,
|
||||
ClientOS: clientOS,
|
||||
LoadLocalData: supportLoadLocalData,
|
||||
@@ -90,6 +92,9 @@ func ListRecords(c *gin.Context) {
|
||||
if mysqlRecord.ClientName != "" {
|
||||
db.Where("client_name like ?", "%"+mysqlRecord.ClientName)
|
||||
}
|
||||
if mysqlRecord.Schema != "" {
|
||||
db.Where("schema like ?", "%"+mysqlRecord.Schema)
|
||||
}
|
||||
if c.Query("load_local_data") != "" {
|
||||
if c.Query("load_local_data") == "true" {
|
||||
db.Where("load_local_data = ?", true)
|
||||
|
||||
@@ -41,7 +41,7 @@ func (r *Rule) CreateOrUpdate() (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = GetServer().updateRules()
|
||||
err = GetServer().UpdateRules()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (r *Rule) Delete() (err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = GetServer().updateRules()
|
||||
err = GetServer().UpdateRules()
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ func (s *Server) getRules() []*Rule {
|
||||
return s.rules
|
||||
}
|
||||
|
||||
func (s *Server) updateRules() error {
|
||||
func (s *Server) UpdateRules() error {
|
||||
db := database.DB.Model(new(Rule))
|
||||
defer s.rulesLock.Unlock()
|
||||
s.rulesLock.Lock()
|
||||
@@ -116,7 +116,7 @@ func (s *Server) Restart() {
|
||||
}
|
||||
|
||||
func (s *Server) Run() {
|
||||
if err := s.updateRules(); err != nil {
|
||||
if err := s.UpdateRules(); err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
for {
|
||||
@@ -156,8 +156,12 @@ func compileTpl(c *gin.Context, tpl string, vars map[string]string) (compiled st
|
||||
|
||||
func (s *Server) Receive(c *gin.Context) {
|
||||
u := c.Request.URL.String()
|
||||
raw, err := getRawRequest(c.Request)
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
for _, _rule := range s.getRules() {
|
||||
flag, flagGroup, vars := _rule.Match(u)
|
||||
flag, flagGroup, vars := _rule.Match(string(raw))
|
||||
if flag == "" {
|
||||
continue
|
||||
}
|
||||
@@ -172,11 +176,6 @@ func (s *Server) Receive(c *gin.Context) {
|
||||
delete(c.Request.Header, s.IpHeader)
|
||||
}
|
||||
|
||||
raw, err := getRawRequest(c.Request)
|
||||
if err != nil {
|
||||
log.Warn(err.Error())
|
||||
}
|
||||
|
||||
// create new record
|
||||
r, err := NewRecord(_rule, flag, c.Request.Method, u, ip, area, string(raw))
|
||||
if err != nil {
|
||||
|
||||
@@ -14,7 +14,7 @@ var _ record.Record = (*Record)(nil)
|
||||
|
||||
type Record struct {
|
||||
Method string `gorm:"index" form:"method" json:"method"`
|
||||
Path string `form:"path" json:"path"`
|
||||
URI string `form:"uri" json:"uri"`
|
||||
record.BaseRecord
|
||||
RawRequest string `json:"raw_request" notice:"-"`
|
||||
Rule Rule `gorm:"foreignKey:RuleName;references:Name;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" form:"-" json:"-" notice:"-"`
|
||||
@@ -37,7 +37,7 @@ func NewRecord(rule *Rule, flag, method, url, ip, area, raw string) (r *Record,
|
||||
RequestTime: time.Now(),
|
||||
},
|
||||
Method: method,
|
||||
Path: url,
|
||||
URI: url,
|
||||
RawRequest: raw,
|
||||
Rule: *rule,
|
||||
}
|
||||
@@ -78,8 +78,8 @@ func ListRecords(c *gin.Context) {
|
||||
if httpRecord.Method != "" {
|
||||
db.Where("method = ?", httpRecord.Method)
|
||||
}
|
||||
if httpRecord.Path != "" {
|
||||
db.Where("path like ?", "%"+httpRecord.Path+"%")
|
||||
if httpRecord.URI != "" {
|
||||
db.Where("path like ?", "%"+httpRecord.URI+"%")
|
||||
}
|
||||
if httpRecord.RemoteIP != "" {
|
||||
db.Where("remote_ip = ?", httpRecord.RemoteIP)
|
||||
|
||||
@@ -58,7 +58,7 @@ func (r *Rule) CreateOrUpdate() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
err = GetServer().updateRules()
|
||||
err = GetServer().UpdateRules()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func (r *Rule) Delete() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
err = GetServer().updateRules()
|
||||
err = GetServer().UpdateRules()
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ func (s *Server) getRules() []*Rule {
|
||||
return s.rules
|
||||
}
|
||||
|
||||
func (s *Server) updateRules() error {
|
||||
func (s *Server) UpdateRules() error {
|
||||
db := database.DB.Model(new(Rule))
|
||||
defer s.rulesLock.Unlock()
|
||||
s.rulesLock.Lock()
|
||||
@@ -165,7 +165,7 @@ func (s *Server) Run() {
|
||||
s.livingLock.Unlock()
|
||||
}()
|
||||
|
||||
if err := s.updateRules(); err != nil {
|
||||
if err := s.UpdateRules(); err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func (r *Rule) CreateOrUpdate() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
err = GetServer().updateRules()
|
||||
err = GetServer().UpdateRules()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func (r *Rule) Delete() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
err = GetServer().updateRules()
|
||||
err = GetServer().UpdateRules()
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/li4n0/revsuit/internal/record"
|
||||
"github.com/li4n0/revsuit/internal/recycler"
|
||||
log "unknwon.dev/clog/v2"
|
||||
)
|
||||
@@ -26,18 +25,15 @@ func ping(c *gin.Context) {
|
||||
c.String(200, "pong")
|
||||
}
|
||||
|
||||
func events(c *gin.Context) {
|
||||
log.Info("Receive client connection from %v", c.Request.RemoteAddr)
|
||||
func (revsuit *Revsuit) events(c *gin.Context) {
|
||||
id := revsuit.addClient(c)
|
||||
log.Info("Receive client[id:%d] connection from %v", id, c.Request.RemoteAddr)
|
||||
c.Stream(func(w io.Writer) bool {
|
||||
select {
|
||||
case <-c.Writer.CloseNotify():
|
||||
return false
|
||||
case r := <-record.Channel():
|
||||
c.SSEvent("message", r.GetFlag())
|
||||
}
|
||||
return true
|
||||
<-c.Writer.CloseNotify()
|
||||
return false
|
||||
})
|
||||
log.Info("Client %s disconnect", c.Request.RemoteAddr)
|
||||
revsuit.removeClient(id)
|
||||
log.Info("Client[id:%d, remote_addr:%s] disconnect", id, c.Request.RemoteAddr)
|
||||
}
|
||||
|
||||
func recovery(c *gin.Context) {
|
||||
|
||||
@@ -40,7 +40,7 @@ func (revsuit *Revsuit) registerPlatformRouter() {
|
||||
|
||||
//platform routers
|
||||
api.GET("/auth", auth)
|
||||
api.GET("/events", events)
|
||||
api.GET("/events", revsuit.events)
|
||||
api.GET("/ping", ping)
|
||||
api.GET("/version", version)
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func (revsuit *Revsuit) registerHttpRouter() {
|
||||
// init settings router group
|
||||
settingsGroup := revsuit.http.ApiGroup.Group("setting")
|
||||
settingsGroup.GET("/exportRules", exportRules)
|
||||
settingsGroup.POST("/importRules", importRules)
|
||||
settingsGroup.POST("/importRules", revsuit.importRules)
|
||||
settingsGroup.GET("/getPlatformConfig", revsuit.getPlatformConfig)
|
||||
settingsGroup.POST("/updatePlatformConfig", revsuit.updatePlatformConfig)
|
||||
settingsGroup.GET("/getDnsConfig", revsuit.getDnsConfig)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/li4n0/revsuit/internal/database"
|
||||
"github.com/li4n0/revsuit/internal/file"
|
||||
"github.com/li4n0/revsuit/internal/notice"
|
||||
"github.com/li4n0/revsuit/internal/record"
|
||||
"github.com/li4n0/revsuit/pkg/dns"
|
||||
"github.com/li4n0/revsuit/pkg/ftp"
|
||||
"github.com/li4n0/revsuit/pkg/mysql"
|
||||
@@ -14,7 +17,7 @@ import (
|
||||
log "unknwon.dev/clog/v2"
|
||||
)
|
||||
|
||||
const VERSION = "0.1.2-beta"
|
||||
const VERSION = "0.1.3-beta"
|
||||
|
||||
type Revsuit struct {
|
||||
config *Config
|
||||
@@ -25,6 +28,29 @@ type Revsuit struct {
|
||||
mysql *mysql.Server
|
||||
rmi *rmi.Server
|
||||
ftp *ftp.Server
|
||||
|
||||
clients map[int]*gin.Context
|
||||
clientID int
|
||||
clientsLock sync.RWMutex
|
||||
clientsNum chan struct{}
|
||||
}
|
||||
|
||||
func (revsuit *Revsuit) addClient(c *gin.Context) int {
|
||||
revsuit.clientsLock.Lock()
|
||||
defer revsuit.clientsLock.Unlock()
|
||||
|
||||
revsuit.clientID++
|
||||
revsuit.clients[revsuit.clientID] = c
|
||||
revsuit.clientsNum <- struct{}{}
|
||||
return revsuit.clientID
|
||||
}
|
||||
|
||||
func (revsuit *Revsuit) removeClient(id int) {
|
||||
revsuit.clientsLock.Lock()
|
||||
defer revsuit.clientsLock.Unlock()
|
||||
|
||||
delete(revsuit.clients, id)
|
||||
<-revsuit.clientsNum
|
||||
}
|
||||
|
||||
func initDatabase(dsn string) {
|
||||
@@ -188,6 +214,8 @@ func New(c *Config) *Revsuit {
|
||||
s.dns.SetServerIP(c.ExternalIP)
|
||||
s.ftp.SetPasvIP(c.ExternalIP)
|
||||
}
|
||||
s.clients = make(map[int]*gin.Context)
|
||||
s.clientsNum = make(chan struct{}, 100)
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -207,6 +235,17 @@ func (revsuit *Revsuit) Run() {
|
||||
if revsuit.ftp != nil && revsuit.ftp.Enable {
|
||||
go revsuit.ftp.Run()
|
||||
}
|
||||
|
||||
go func() {
|
||||
for r := range record.Channel() {
|
||||
<-revsuit.clientsNum
|
||||
revsuit.clientsLock.RLock()
|
||||
for _, client := range revsuit.clients {
|
||||
client.SSEvent("message", r.GetFlag())
|
||||
client.Writer.Flush()
|
||||
}
|
||||
revsuit.clientsNum <- struct{}{}
|
||||
revsuit.clientsLock.RUnlock()
|
||||
}
|
||||
}()
|
||||
revsuit.http.Run()
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func exportRules(c *gin.Context) {
|
||||
c.String(200, string(out))
|
||||
}
|
||||
|
||||
func importRules(c *gin.Context) {
|
||||
func (revsuit *Revsuit) importRules(c *gin.Context) {
|
||||
var (
|
||||
db = database.DB
|
||||
rules Rules
|
||||
@@ -87,48 +87,58 @@ func importRules(c *gin.Context) {
|
||||
}
|
||||
|
||||
for _, rule := range rules.Http {
|
||||
err := db.Model(&rhttp.Rule{}).Create(&rule).Error
|
||||
if err != nil {
|
||||
if err := db.Model(&rhttp.Rule{}).Create(&rule).Error; err != nil {
|
||||
errs = append(errs, errors.Wrap(err, fmt.Sprintf("http rule[%s]", rule.Name)).Error())
|
||||
continue
|
||||
}
|
||||
count += 1
|
||||
count++
|
||||
}
|
||||
if err := revsuit.http.UpdateRules(); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
|
||||
for _, rule := range rules.Dns {
|
||||
err := db.Model(&dns.Rule{}).Create(&rule).Error
|
||||
if err != nil {
|
||||
if err := db.Model(&dns.Rule{}).Create(&rule).Error; err != nil {
|
||||
errs = append(errs, errors.Wrap(err, fmt.Sprintf("dns rule[%s]", rule.Name)).Error())
|
||||
continue
|
||||
}
|
||||
count += 1
|
||||
count++
|
||||
}
|
||||
if err := revsuit.dns.UpdateRules(); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
|
||||
for _, rule := range rules.Mysql {
|
||||
err := db.Model(&mysql.Rule{}).Create(&rule).Error
|
||||
if err != nil {
|
||||
if err := db.Model(&mysql.Rule{}).Create(&rule).Error; err != nil {
|
||||
errs = append(errs, errors.Wrap(err, fmt.Sprintf("mysql rule[%s]", rule.Name)).Error())
|
||||
continue
|
||||
}
|
||||
count += 1
|
||||
count++
|
||||
}
|
||||
if err := revsuit.mysql.UpdateRules(); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
|
||||
for _, rule := range rules.Rmi {
|
||||
err := db.Model(&rmi.Rule{}).Create(&rule).Error
|
||||
if err != nil {
|
||||
if err := db.Model(&rmi.Rule{}).Create(&rule).Error; err != nil {
|
||||
errs = append(errs, errors.Wrap(err, fmt.Sprintf("rmi rule[%s]", rule.Name)).Error())
|
||||
continue
|
||||
}
|
||||
count += 1
|
||||
count++
|
||||
}
|
||||
if err := revsuit.rmi.UpdateRules(); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
|
||||
for _, rule := range rules.Ftp {
|
||||
err := db.Model(&ftp.Rule{}).Create(&rule).Error
|
||||
if err != nil {
|
||||
if err := db.Model(&ftp.Rule{}).Create(&rule).Error; err != nil {
|
||||
errs = append(errs, errors.Wrap(err, fmt.Sprintf("ftp rule[%s]", rule.Name)).Error())
|
||||
continue
|
||||
}
|
||||
count += 1
|
||||
count++
|
||||
}
|
||||
if err := revsuit.ftp.UpdateRules(); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
|
||||