├── .gitignore ├── INSTALL.md ├── LICENSE.md ├── NOTICE.md ├── README.md ├── admin ├── actions.raw ├── auth.raw ├── bootstrap.min.css.map.raw ├── checkv2.raw ├── css.raw ├── errors.raw ├── fonts.raw ├── fuzzyadd.raw ├── getmap.raw ├── graph.raw ├── history.raw ├── historyreset.raw ├── img.raw ├── index.html ├── index.raw ├── js.raw ├── learnham.raw ├── learnspam.raw ├── maps.raw ├── neighbours.raw ├── plugins.raw ├── saveactions.raw ├── savemap.raw ├── savesymbols.raw ├── scan.raw ├── stat.raw └── symbols.raw ├── data ├── css │ ├── bootstrap-theme.css │ ├── bootstrap-theme.css.map │ ├── bootstrap-theme.min.css │ ├── bootstrap-theme.min.css.map │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.min.css │ └── bootstrap.min.css.map ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 └── js │ ├── bootstrap.js │ ├── bootstrap.min.js │ └── npm.js ├── exec ├── admin.inc.php ├── class.inc.php ├── functions.inc.php ├── raw.inc.php └── settings.inc.php ├── hooks ├── admin_img.html └── admin_txt.html ├── images ├── admin_icon.svg ├── js │ └── plugin.js └── logo.png ├── php.ini ├── plugin.conf ├── scripts ├── install.sh ├── uninstall.sh └── update.sh ├── version.txt └── www ├── NOTICE.md ├── README.md ├── apple-touch-icon.png ├── browserconfig.xml ├── css ├── FooTable.Glyphicons.css ├── bootstrap.min.css ├── codejar-linenumbers.css ├── d3evolution.css ├── d3pie.css ├── font-glyphicons.css ├── footable.standalone.min.css ├── nprogress.css ├── prism.css ├── rspamd.css └── svg-with-js.min.css ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonts ├── glyphicons-halflings-regular.ttf ├── glyphicons-halflings-regular.woff └── glyphicons-halflings-regular.woff2 ├── img ├── asc.png ├── desc.png └── rspamd_logo_navbar.png ├── index.html ├── js ├── app │ ├── common.js │ ├── config.js │ ├── graph.js │ ├── history.js │ ├── libft.js │ ├── rspamd.js │ ├── selectors.js │ ├── stats.js │ ├── symbols.js │ └── upload.js ├── lib │ ├── bootstrap.bundle.min.js │ ├── codejar-linenumbers.min.js │ ├── codejar.min.js │ ├── d3.min.js │ ├── d3evolution.min.js │ ├── d3pie.min.js │ ├── fontawesome.min.js │ ├── footable.min.js │ ├── jquery-3.7.1.min.js │ ├── jquery.stickytabs.min.js │ ├── nprogress.min.js │ ├── prism.js │ ├── require.min.js │ ├── solid.min.js │ └── visibility.min.js └── main.js ├── mstile-150x150.png └── safari-pinned-tab.svg /.gitignore: -------------------------------------------------------------------------------- 1 | available_version.txt 2 | update_www.sh 3 | plugin.tar.gz 4 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | ## INSTALL FROM GIT 2 | 3 | Run the following commands as `root` to install the plugin from a `git` repository: 4 | 5 | ``` 6 | cd /usr/local/directadmin/plugins/ 7 | git clone https://github.com/poralix/rspamd.git 8 | cd rspamd/scripts/ 9 | ./install.sh 10 | ``` 11 | 12 | ## INSTALL VIA DIRECTADMIN INTERFACE 13 | 14 | - Connect to DirectAdmin as admin 15 | - Go to Plugin Manager page 16 | - Install a plugin from URL: https://files.poralix.com/get/freesoftware/rspamd.tar.gz 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | ## RSPAMD 2 | 3 | * Author: Rspamd with the web interface is written by **Vsevolod Stakhov** - [vstakhov](https://github.com/vstakhov) 4 | * License: This project is licensed under the Apache 2.0 License, see the [LICENSE.md](LICENSE.md) file for details 5 | * Rspamd Home site: 6 | * Site repository: 7 | 8 | ## RSPAMD WEB INTERFACE 9 | 10 | * Original files copied from official Rspamd Web-UI to monitor changes (as a read-only repository) 11 | * Original location of the files is /usr/share/rspamd/www/ on CentOS 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Directadmin    +   Rspamd

2 | 3 | ## NAME 4 | 5 | Rspamd web interface plugin for DirectAdmin Panel 6 | 7 | ## DESCRIPTION 8 | 9 | The plugin provides administrators an access from DirectAdmin (the hosting panel) to a simple control interface for Rspamd (spam filtering system). 10 | 11 | The Rspamd Web-UI provides basic functions for setting metric actions, scores, viewing statistic and learning. 12 | 13 | The plugin does not use or modify original files of Rspamd Web-UI. It proxies requests from users to Rspamd Web-UI and replies from the Web-UI to users. 14 | As a reverse proxy the plugin modifies links to keep navigation working within DirectAdmin. 15 | 16 | ## PLUGIN VERSION 17 | 18 | - Version: 0.2.6 19 | - Last modified: Wed May 8 14:09:59 +07 2024 20 | - Update URL: https://files.poralix.com/get/freesoftware/rspamd.tar.gz 21 | - Version URL: https://files.poralix.com/version/freesoftware/rspamd 22 | - Tested with version of Rspamd: rspamd-3.9.0 23 | 24 | ## CHANGELOG 25 | 26 | - May 05, 2024: Updated for rspamd-3.9.0-57 27 | - Feb 22, 2023: Updated for rspamd-3.5-12 28 | - Sep 20, 2020: Updated for Rspamd 2.6-25.git37f19ff44.x86_64 29 | - May 21, 2020: Updated to support Rspamd web-interface via UNIX-socket 30 | - Oct 08, 2019: Updated for Rspamd 2.0-43.git6ea2346e8.x86_64 31 | - May 14, 2019: Updated for Rspamd 1.9.3 (stable) and 1.9.4 (experimental) 32 | 33 | ## INSTALLATION 34 | 35 | See the [INSTALL.md](INSTALL.md) file for installation instructions 36 | 37 | ## USAGE 38 | 39 | Connect to DirectAdmin as administrator and go to `Rspamd Web Interface` under `Extra Features` 40 | 41 | ## AUTHORS 42 | 43 | - Plugin for Directadmin is written by **Alex Grebenschikov** 44 | - Rspamd with the web interface is written by **Vsevolod Stakhov** 45 | - Directadmin is owned and written by **JBMC Software** 46 | 47 | ## LICENSE 48 | 49 | This project is licensed under the Apache 2.0 License - see the [LICENSE.md](LICENSE.md) file for details 50 | 51 | ## NOTICES 52 | 53 | See the [NOTICE.md](NOTICE.md) file for details 54 | 55 | ## REFERENCES 56 | 57 | - Rspamd Home site: 58 | - Plugin Development: 59 | - Custom Plugin Development and Server Support: 60 | - DirectAdmin Home site: 61 | -------------------------------------------------------------------------------- /admin/actions.raw: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/php -c/usr/local/directadmin/plugins/rspamd/php.ini 2 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | 16 | setUrl($url); 41 | 42 | if (defined('RSPAMD_SOCKET') && RSPAMD_SOCKET) 43 | { 44 | $plugin->setUseSocket(RSPAMD_SOCKET); 45 | } 46 | else 47 | { 48 | $plugin->setRemoteAddr('127.0.0.1'); 49 | } 50 | $plugin->setRequestMethod($_SERVER['REQUEST_METHOD']); 51 | $plugin->setRequestReferer($requestReferer); 52 | $plugin->setUserAgent($_SERVER['SERVER_SOFTWARE'].' / '. $_SERVER['DA_VERSION']); 53 | 54 | if ($_SERVER['REQUEST_METHOD'] == 'POST') 55 | { 56 | if (defined('SAVE_CONTENT_TYPE') && (SAVE_CONTENT_TYPE == 'JSON')) 57 | { 58 | $postData = json_encode(json_decode($_SERVER['POST'])); 59 | $plugin->setPostData($postData); 60 | } 61 | elseif (defined('SAVE_CONTENT_TYPE') && (SAVE_CONTENT_TYPE == 'RAW')) 62 | { 63 | $plugin->setPostData($_SERVER['POST']); 64 | } 65 | else 66 | { 67 | $plugin->setPostData($_POST); 68 | } 69 | } 70 | 71 | if ($plugin->makeRequest() && $plugin->getResponseHeaders()) 72 | { 73 | $bodyOutput = filterContent($plugin->getResponseBody()); 74 | 75 | 76 | if (defined('ADMIN_RAW_CONTENT') && ADMIN_RAW_CONTENT) 77 | { 78 | if ($responseHeaders=parseHeaders($plugin->getResponseHeaders())) 79 | { 80 | print(filterHeaders($responseHeaders, strlen($bodyOutput))); 81 | } 82 | else 83 | { 84 | printHeaders(false, strlen($bodyOutput)); 85 | } 86 | } 87 | 88 | print($bodyOutput); 89 | } 90 | else 91 | { 92 | if (defined('ADMIN_RAW_CONTENT') && ADMIN_RAW_CONTENT) 93 | { 94 | printHeaders(); 95 | print json_encode(array("error" => "An error occurred!")); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /exec/class.inc.php: -------------------------------------------------------------------------------- 1 | Url=$str; 31 | } 32 | 33 | public function setUserAgent($str) 34 | { 35 | $this->userAgent=$str; 36 | } 37 | 38 | public function setRemoteAddr($str) 39 | { 40 | $this->remoteAddr=$str; 41 | $this->socketFile=false; 42 | } 43 | 44 | public function setUseSocket($str) 45 | { 46 | $this->remoteAddr=false; 47 | $this->socketFile=$str; 48 | } 49 | 50 | public function setRequestMethod($str) 51 | { 52 | $this->requestMethod=(strtoupper($str) == "POST") ? "POST" : "GET"; 53 | } 54 | 55 | public function setRequestHeaders($arr) 56 | { 57 | $this->requestHeaders=$arr; 58 | } 59 | 60 | public function setRequestReferer($str) 61 | { 62 | $this->requestReferer=$str; 63 | } 64 | 65 | public function setPostData($arr) 66 | { 67 | $this->postData=$arr; 68 | } 69 | 70 | public function setResponseHeaders($str) 71 | { 72 | $this->responseHeaders=$str; 73 | } 74 | 75 | public function setResponseBody($str) 76 | { 77 | $this->responseBody=$str; 78 | } 79 | 80 | public function setResponseInfo($str) 81 | { 82 | $this->responseInfo=$str; 83 | } 84 | 85 | 86 | public function getUrl() 87 | { 88 | return $this->Url; 89 | } 90 | 91 | public function getUserAgent() 92 | { 93 | return $this->userAgent; 94 | } 95 | 96 | public function getRemoteAddr() 97 | { 98 | return $this->remoteAddr; 99 | } 100 | 101 | public function getUseSocket() 102 | { 103 | return $this->socketFile; 104 | } 105 | 106 | public function getRequestMethod() 107 | { 108 | return $this->requestMethod; 109 | } 110 | 111 | public function getRequestHeaders() 112 | { 113 | return $this->requestHeaders; 114 | } 115 | 116 | public function getRequestReferer() 117 | { 118 | return $this->requestReferer; 119 | } 120 | 121 | public function getPostData() 122 | { 123 | return $this->postData; 124 | } 125 | 126 | public function getResponseHeaders() 127 | { 128 | return $this->responseHeaders; 129 | } 130 | 131 | public function getResponseBody() 132 | { 133 | return $this->responseBody; 134 | } 135 | 136 | public function getResponseInfo() 137 | { 138 | return $this->responseInfo; 139 | } 140 | 141 | public function makeRequest() 142 | { 143 | if ($this->getUseSocket()) 144 | { 145 | return $this->makeSocketRequest(); 146 | } 147 | else 148 | { 149 | return $this->makeHTTPRequest(); 150 | } 151 | } 152 | 153 | public function makeSocketRequest() 154 | { 155 | $responseInfo=''; 156 | $headerSize=''; 157 | 158 | $this->setResponseHeaders(false); 159 | $this->setResponseBody(false); 160 | $this->setResponseInfo(false); 161 | 162 | $userAgent = $this->getUserAgent(); 163 | $requestMethod = $this->getRequestMethod(); 164 | $remoteAddr = $this->getRemoteAddr(); 165 | $postData = $this->getPostData(); 166 | $url = $this->getUrl(); 167 | $requestHeaders = $this->getRequestHeaders(); 168 | $requestReferer = $this->getRequestReferer(); 169 | 170 | $useSocket = $this->getUseSocket(); 171 | 172 | $socket = socket_create(AF_UNIX, SOCK_STREAM, 0); 173 | 174 | $result = socket_connect($socket, $useSocket); 175 | if ($result === false) { 176 | // echo "Failed to socket_connect().\nReason: ($result) " . socket_strerror(socket_last_error($socket)) . "\n"; 177 | return false; 178 | } 179 | 180 | switch ($requestMethod) 181 | { 182 | case 'HEAD': 183 | $request = "HEAD ".$url." HTTP/1.0\r\n"; 184 | break; 185 | case 'POST': 186 | $request = "POST ".$url." HTTP/1.0\r\n"; 187 | break; 188 | default: 189 | $request = "GET ".$url." HTTP/1.0\r\n"; 190 | break; 191 | } 192 | 193 | if ($requestHeaders && is_array($requestHeaders)) 194 | { 195 | foreach ($requestHeaders as $name => $value) 196 | { 197 | $request .= $name . ": " . $value ."\n"; 198 | } 199 | } 200 | if ($requestMethod === "POST") 201 | { 202 | if (defined('SAVE_CONTENT_TYPE') && (SAVE_CONTENT_TYPE == 'JSON')) 203 | { 204 | $request .= "Content-Type: application/json\r\n"; 205 | $request .= "Content-Length: " . strlen($postData)."\r\n\r\n"; 206 | $request .= $postData ."\r\n"; 207 | } 208 | elseif (defined('SAVE_CONTENT_TYPE') && (SAVE_CONTENT_TYPE == 'RAW')) 209 | { 210 | $request .= "Content-Type: application/x-www-form-urlencoded\r\n"; 211 | $request .= "Content-Length: " . strlen($postData)."\r\n\r\n"; 212 | $request .= $postData ."\r\n"; 213 | } 214 | } 215 | $request .= "\r\n"; 216 | 217 | socket_write($socket, $request, strlen($request)); 218 | $response = ''; 219 | 220 | while ($out = socket_read($socket, 2048)) { 221 | $response .= $out; 222 | } 223 | 224 | if ($response) 225 | { 226 | $responseHeaders = substr($response, 0, strpos($response, "\r\n\r\n")); 227 | $responseBody = substr($response, strpos($response, "\r\n\r\n")+4); 228 | $this->setResponseHeaders($responseHeaders); 229 | $this->setResponseBody($responseBody); 230 | $this->setResponseInfo(''); 231 | socket_close($socket); 232 | return true; 233 | } 234 | 235 | socket_close($socket); 236 | return false; 237 | } 238 | 239 | public function makeHTTPRequest() 240 | { 241 | $responseInfo=''; 242 | $headerSize=''; 243 | 244 | $this->setResponseHeaders(false); 245 | $this->setResponseBody(false); 246 | $this->setResponseInfo(false); 247 | 248 | $userAgent = $this->getUserAgent(); 249 | $requestMethod = $this->getRequestMethod(); 250 | $remoteAddr = $this->getRemoteAddr(); 251 | $postData = $this->getPostData(); 252 | $url = $this->getUrl(); 253 | $requestHeaders = $this->getRequestHeaders(); 254 | $requestReferer = $this->getRequestReferer(); 255 | 256 | if (($ch=curl_init()) && $url) 257 | { 258 | $curlRequestHeaders = array(); 259 | if ($requestHeaders && is_array($requestHeaders)) 260 | { 261 | foreach ($requestHeaders as $name => $value) { 262 | $curlRequestHeaders[] = $name . ': ' . $value; 263 | } 264 | } 265 | if ($remoteAddr) $curlRequestHeaders[] = 'X-Forwarded-For: ' . $remoteAddr; 266 | if ($requestMethod == 'POST') 267 | { 268 | curl_setopt($ch, CURLOPT_POST, true); 269 | if (defined('SAVE_CONTENT_TYPE') && (SAVE_CONTENT_TYPE == 'JSON' || SAVE_CONTENT_TYPE == 'RAW')) 270 | { 271 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); 272 | curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); 273 | curl_setopt($ch, CURLOPT_HTTPHEADER, array( 274 | 'Content-Type: application/json', 275 | 'Content-Length: ' . strlen($postData)) 276 | ); 277 | } 278 | else 279 | { 280 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData)); 281 | } 282 | } 283 | if ($requestReferer) curl_setopt($ch, CURLOPT_REFERER, $requestReferer); 284 | if ($curlRequestHeaders) curl_setopt($ch, CURLOPT_HTTPHEADER, $curlRequestHeaders); 285 | if ($userAgent) curl_setopt($ch, CURLOPT_USERAGENT, $userAgent); 286 | curl_setopt($ch, CURLOPT_ENCODING, ''); 287 | curl_setopt($ch, CURLOPT_HEADER, true); 288 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 289 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 290 | curl_setopt($ch, CURLOPT_URL, $url); 291 | if ($response = curl_exec($ch)) 292 | { 293 | $responseInfo = curl_getinfo($ch); 294 | $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); 295 | curl_close($ch); 296 | $responseHeaders = substr($response, 0, $headerSize); 297 | $responseBody = substr($response, $headerSize); 298 | $this->setResponseHeaders($responseHeaders); 299 | $this->setResponseBody($responseBody); 300 | $this->setResponseInfo($responseInfo); 301 | return true; 302 | } 303 | curl_close($ch); 304 | } 305 | return false; 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /exec/raw.inc.php: -------------------------------------------------------------------------------- 1 | Rspamd
2 | -------------------------------------------------------------------------------- /hooks/admin_txt.html: -------------------------------------------------------------------------------- 1 | Rspamd Web Interface 2 | -------------------------------------------------------------------------------- /images/admin_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/js/plugin.js: -------------------------------------------------------------------------------- 1 | // plugins.js 2 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poralix/rspamd/fb3a969e51c69ddf52143348e3b3978b52cc2aa4/images/logo.png -------------------------------------------------------------------------------- /plugin.conf: -------------------------------------------------------------------------------- 1 | active=yes 2 | author=Alex Grebenschikov, Poralix, www.poralix.com 3 | id=rspamd 4 | installed=yes 5 | name=Rspamd web interface 6 | admin_run_as=_rspamd 7 | version=0.2.6 8 | update_url=https://files.poralix.com/get/freesoftware/rspamd.tar.gz 9 | version_url=https://files.poralix.com/version/freesoftware/rspamd 10 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ###################################################################################### 3 | # 4 | # Rspamd web interface plugin for Directadmin $ 0.2 5 | # ============================================================================== 6 | # Last modified: Fri May 22 13:02:38 +07 2020 7 | # ============================================================================== 8 | # Written by Alex S Grebenschikov (support@poralix.com) 9 | # Copyright 2019 by Alex S Grebenschikov (support@poralix.com) 10 | # ============================================================================== 11 | # 12 | ###################################################################################### 13 | 14 | DIR="/usr/local/directadmin/plugins/rspamd"; 15 | 16 | chown -R diradmin:diradmin "${DIR}"; 17 | id _rspamd >/dev/null 2>&1; RVAL=$?; 18 | 19 | if [ "${RVAL}" == "0" ]; then 20 | { 21 | chown -R diradmin:_rspamd "${DIR}"; 22 | chmod 750 "${DIR}/admin/"*.raw; 23 | chmod 750 "${DIR}/admin/"*.html; 24 | chmod 644 "${DIR}/exec/"*.php; 25 | chmod 750 "${DIR}/data/"; 26 | chmod 750 "${DIR}/exec/"; 27 | chmod 755 "${DIR}/"; 28 | 29 | perl -pi -e "s/^active=no/active=yes/" "${DIR}/plugin.conf"; 30 | perl -pi -e "s/^installed=no/installed=yes/" "${DIR}/plugin.conf"; 31 | perl -pi -e "s/.*'RSPAMD_SOCKET'.*\n//" "${DIR}/exec/settings.inc.php"; 32 | 33 | if [ -S "/var/run/rspamd/rspamd_controller.sock" ]; then 34 | echo "if (!defined('RSPAMD_SOCKET')) define('RSPAMD_SOCKET','/var/run/rspamd/rspamd_controller.sock');" >> "${DIR}/exec/settings.inc.php"; 35 | fi; 36 | 37 | echo "Plugin installed"; 38 | } 39 | else 40 | { 41 | perl -pi -e "s/^active=.*/active=no/" "${DIR}/plugin.conf"; 42 | perl -pi -e "s/^installed=.*/installed=no/" "${DIR}/plugin.conf"; 43 | echo "No Rspamd is found to be installed!
First install Rspamd and then re-install the plugin!
"; 44 | } 45 | fi; 46 | 47 | exit 0; 48 | -------------------------------------------------------------------------------- /scripts/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ###################################################################################### 3 | # 4 | # Rspamd web interface plugin for Directadmin $ 0.2 5 | # ============================================================================== 6 | # Last modified: Thu May 21 20:33:07 +07 2020 7 | # ============================================================================== 8 | # Written by Alex S Grebenschikov (support@poralix.com) 9 | # Copyright 2019 by Alex S Grebenschikov (support@poralix.com) 10 | # ============================================================================== 11 | # 12 | ###################################################################################### 13 | 14 | DIR="/usr/local/directadmin/plugins/rspamd"; 15 | 16 | perl -pi -e "s/^active=yes/active=no/" "${DIR}/plugin.conf"; 17 | perl -pi -e "s/^installed=yes/installed=no/" "${DIR}/plugin.conf"; 18 | 19 | echo "Plugin uninstalled"; 20 | 21 | exit 0; 22 | -------------------------------------------------------------------------------- /scripts/update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ###################################################################################### 3 | # 4 | # Rspamd web interface plugin for Directadmin $ 0.2 5 | # ============================================================================== 6 | # Last modified: Thu May 21 20:33:07 +07 2020 7 | # ============================================================================== 8 | # Written by Alex S Grebenschikov (support@poralix.com) 9 | # Copyright 2019 by Alex S Grebenschikov (support@poralix.com) 10 | # ============================================================================== 11 | # 12 | ###################################################################################### 13 | 14 | DIR="/usr/local/directadmin/plugins/rspamd"; 15 | 16 | "${DIR}/scripts/uninstall.sh" > /dev/null 2>&1; 17 | "${DIR}/scripts/install.sh" > /dev/null 2>&1; 18 | 19 | echo "Plugin updated"; 20 | 21 | exit 0; 22 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 0.2.5 -------------------------------------------------------------------------------- /www/NOTICE.md: -------------------------------------------------------------------------------- 1 | ## RSPAMD 2 | 3 | * Author: Rspamd with the web interface is written by **Vsevolod Stakhov** - [vstakhov](https://github.com/vstakhov) 4 | * License: This project is licensed under the Apache 2.0 License, see the [LICENSE.md](LICENSE.md) file for details 5 | * Rspamd Home site: 6 | * Site repository: 7 | 8 | ## RSPAMD WEB INTERFACE 9 | 10 | * Original files copied from official Rspamd Web-UI to monitor changes (as a read-only repository) 11 | * Original location of the files is /usr/share/rspamd/www/ on CentOS 12 | -------------------------------------------------------------------------------- /www/README.md: -------------------------------------------------------------------------------- 1 | # Rspamd web interface 2 | 3 | ## Overview 4 | 5 | This is a simple control interface for rspamd spam filtering system. 6 | It provides basic functions for setting metric actions, scores, 7 | viewing statistic and learning. 8 | 9 | Webui screenshot 10 | Webui screenshot 11 | 12 | ## Rspamd setup 13 | 14 | It is required to configure dynamic settings to store configured values. 15 | Basically this can be done by providing the following line in options settings: 16 | 17 | ~~~ucl 18 | options { 19 | dynamic_conf = "/var/lib/rspamd/rspamd_dynamic"; 20 | } 21 | ~~~ 22 | 23 | Please note that this path must have write access for rspamd user. 24 | 25 | Then controller worker should be configured: 26 | 27 | ~~~ucl 28 | worker { 29 | type = "controller"; 30 | bind_socket = "localhost:11334"; 31 | count = 1; 32 | # Password for normal commands (use rspamadm pw) 33 | password = "$2$anydoddx67ggcs74owybhcwqsq3z67q4$udympbo8pfcfqkeiiuj7gegabk5jpt8edmhseujhar9ooyuzig5b"; 34 | # Password for privileged commands (use rspamadm pw) 35 | enable_password = "$2$nx6sqkxtewx9c5s3hxjmabaxdcr46pk9$45qajkbyqx77abapiqugpjpsojj38zcqn7xnp3ekqyu674koux4b"; 36 | # Path to webiu static files 37 | static_dir = "${WWWDIR}"; 38 | } 39 | ~~~ 40 | 41 | Password option should be changed for sure for your specific configuration. Encrypted password using is encouraged (`rspamadm pw --encrypt`). 42 | 43 | ## Interface setup 44 | 45 | Interface itself is written in pure HTML5/js and, hence, it requires zero setup. 46 | Just enter a password for webui access and you are ready. 47 | 48 | ## Contact information 49 | 50 | Rspamd interface is distributed under the terms of [MIT license](http://opensource.org/licenses/MIT). For all questions related to this 51 | product please see the [support page](https://rspamd.com/support.html) 52 | -------------------------------------------------------------------------------- /www/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poralix/rspamd/fb3a969e51c69ddf52143348e3b3978b52cc2aa4/www/apple-touch-icon.png -------------------------------------------------------------------------------- /www/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2b5797 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /www/css/FooTable.Glyphicons.css: -------------------------------------------------------------------------------- 1 | /* Glyphicons Icons - We're not actually using Glyphicons classes but instead provide a simple mapping 2 | from Glyphicons to FooTable class names. */ 3 | .fooicon { 4 | position: relative; 5 | top: 1px; 6 | display: inline-block; 7 | /* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */ 8 | font-family: "Glyphicons Halflings" !important; 9 | font-style: normal; 10 | font-weight: 400; 11 | line-height: 1; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | .fooicon::before, 16 | .fooicon::after { 17 | -webkit-box-sizing: border-box; 18 | -moz-box-sizing: border-box; 19 | box-sizing: border-box; 20 | } 21 | .fooicon-loader::before { 22 | content: "\e030"; 23 | } 24 | .fooicon-plus::before { 25 | content: "\2b"; 26 | } 27 | .fooicon-minus::before { 28 | content: "\2212"; 29 | } 30 | .fooicon-search::before { 31 | content: "\e003"; 32 | } 33 | .fooicon-remove::before { 34 | content: "\e014"; 35 | } 36 | .fooicon-sort::before { 37 | content: "\e150"; 38 | } 39 | .fooicon-sort-asc::before { 40 | content: "\e155"; 41 | } 42 | .fooicon-sort-desc::before { 43 | content: "\e156"; 44 | } 45 | .fooicon-pencil::before { 46 | content: "\270f"; 47 | } 48 | .fooicon-trash::before { 49 | content: "\e020"; 50 | } 51 | .fooicon-eye-close::before { 52 | content: "\e106"; 53 | } 54 | .fooicon-flash::before { 55 | content: "\e162"; 56 | } 57 | .fooicon-cog::before { 58 | content: "\e019"; 59 | } 60 | .fooicon-stats::before { 61 | content: "\e185"; 62 | } 63 | -------------------------------------------------------------------------------- /www/css/codejar-linenumbers.css: -------------------------------------------------------------------------------- 1 | .codejar-linenumbers-inner-wrap { 2 | position: absolute; 3 | top: 0px; 4 | left: 0px; 5 | bottom: 0px; 6 | overflow: hidden; 7 | } 8 | 9 | .codejar-linenumbers { 10 | mix-blend-mode: initial; 11 | height: 100%; 12 | } 13 | 14 | .codejar-linenumber { 15 | position: relative; 16 | top: 0px; 17 | } 18 | -------------------------------------------------------------------------------- /www/css/d3evolution.css: -------------------------------------------------------------------------------- 1 | .d3evolution svg { 2 | background-color: white; 3 | } 4 | .d3evolution .chart-title { 5 | font-size: 17px; 6 | } 7 | .d3evolution .y.label { 8 | font-size: 11px; 9 | font-weight: normal; 10 | } 11 | .d3evolution .grid line { 12 | stroke: lightgrey; 13 | stroke-opacity: .7; 14 | shape-rendering: crispEdges; 15 | } 16 | .d3evolution .grid path { 17 | stroke-width: 0; 18 | } 19 | .d3evolution .axis, 20 | .d3evolution .legend { 21 | font-size: 12px; 22 | } 23 | .d3evolution .legend .value { 24 | font-size: 10px; 25 | } 26 | .d3evolution .cursor-time { 27 | font-size: 10px; 28 | } 29 | .d3evolution .cursor { 30 | shape-rendering: crispEdges; 31 | } 32 | .d3evolution .cursor .background { 33 | stroke: white; 34 | } 35 | .d3evolution .cursor .foreground { 36 | stroke-dasharray: 4, 2; 37 | } 38 | .d3evolution .cursor .x.foreground { 39 | stroke: blue; 40 | } 41 | .d3evolution .cursor circle { 42 | shape-rendering: geometricPrecision; 43 | } 44 | .d3evolution .cursor circle.foreground { 45 | stroke-dasharray: none; 46 | stroke: black; 47 | opacity: .5; 48 | } 49 | /* 50 | path.path { 51 | shape-rendering: crispEdges; 52 | } 53 | */ 54 | .d3evolution .axis path, 55 | .d3evolution .axis line { 56 | fill: none; 57 | stroke: grey; 58 | shape-rendering: crispEdges; 59 | } 60 | .d3evolution .legend circle { 61 | stroke-width: 2px; 62 | } 63 | .d3evolution .path-null { 64 | fill: steelblue; 65 | fill-opacity: .1; 66 | } 67 | -------------------------------------------------------------------------------- /www/css/d3pie.css: -------------------------------------------------------------------------------- 1 | .d3pie .chart-title { 2 | font-family: Arial, sans-serif; 3 | font-size: 24px; 4 | text-anchor: middle; 5 | } 6 | .d3pie-tooltip { 7 | pointer-events: none; 8 | position: absolute; 9 | padding: 5px; 10 | color: white; 11 | background-color: rgb(0 0 0 /50%); 12 | border-radius: 4px; 13 | opacity: 0; 14 | line-height: normal; 15 | } 16 | 17 | .d3pie .total-text { 18 | text-anchor: middle; 19 | dominant-baseline: central; 20 | } 21 | .d3pie .total-value { 22 | font-family: Arial, sans-serif; 23 | } 24 | 25 | .d3pie .inner-label { 26 | fill: #eeeeee; 27 | pointer-events: none; 28 | text-anchor: middle; 29 | } 30 | 31 | /* pie placeholder */ 32 | .d3pie .slice-g:first-of-type .inner-label { 33 | fill: #cccccc; 34 | } 35 | .d3pie defs radialGradient:first-of-type stop { 36 | stop-opacity: 0.1; 37 | } 38 | 39 | /* gradient color */ 40 | .d3pie .grad-stop-1 { 41 | stop-color: black; 42 | } 43 | 44 | .outer-label-g { 45 | opacity: 0; 46 | } 47 | .link { 48 | fill: none; 49 | } 50 | -------------------------------------------------------------------------------- /www/css/nprogress.css: -------------------------------------------------------------------------------- 1 | /* Make clicks pass-through */ 2 | #nprogress { 3 | pointer-events: none; 4 | } 5 | 6 | #nprogress .bar { 7 | background: #29d; 8 | 9 | position: fixed; 10 | z-index: 1031; 11 | top: 0; 12 | left: 0; 13 | 14 | width: 100%; 15 | height: 2px; 16 | } 17 | 18 | /* Fancy blur effect */ 19 | #nprogress .peg { 20 | display: block; 21 | position: absolute; 22 | right: 0px; 23 | width: 100px; 24 | height: 100%; 25 | box-shadow: 0 0 10px #29d, 0 0 5px #29d; 26 | opacity: 1.0; 27 | 28 | -webkit-transform: rotate(3deg) translate(0px, -4px); 29 | -ms-transform: rotate(3deg) translate(0px, -4px); 30 | transform: rotate(3deg) translate(0px, -4px); 31 | } 32 | 33 | /* Remove these to get rid of the spinner */ 34 | #nprogress .spinner { 35 | display: block; 36 | position: fixed; 37 | z-index: 1031; 38 | top: 15px; 39 | right: 15px; 40 | } 41 | 42 | #nprogress .spinner-icon { 43 | width: 18px; 44 | height: 18px; 45 | box-sizing: border-box; 46 | 47 | border: solid 2px transparent; 48 | border-top-color: #29d; 49 | border-left-color: #29d; 50 | border-radius: 50%; 51 | 52 | -webkit-animation: nprogress-spinner 400ms linear infinite; 53 | animation: nprogress-spinner 400ms linear infinite; 54 | } 55 | 56 | .nprogress-custom-parent { 57 | overflow: hidden; 58 | position: relative; 59 | } 60 | 61 | .nprogress-custom-parent #nprogress .spinner, 62 | .nprogress-custom-parent #nprogress .bar { 63 | position: absolute; 64 | } 65 | 66 | @-webkit-keyframes nprogress-spinner { 67 | 0% { -webkit-transform: rotate(0deg); } 68 | 100% { -webkit-transform: rotate(360deg); } 69 | } 70 | @keyframes nprogress-spinner { 71 | 0% { transform: rotate(0deg); } 72 | 100% { transform: rotate(360deg); } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /www/css/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism-okaidia&languages=clike&plugins=show-invisibles */ 3 | code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} 4 | .token.cr,.token.lf,.token.space,.token.tab:not(:empty){position:relative}.token.cr:before,.token.lf:before,.token.space:before,.token.tab:not(:empty):before{color:grey;opacity:.6;position:absolute}.token.tab:not(:empty):before{content:'\21E5'}.token.cr:before{content:'\240D'}.token.crlf:before{content:'\240D\240A'}.token.lf:before{content:'\240A'}.token.space:before{content:'\00B7'} 5 | -------------------------------------------------------------------------------- /www/css/rspamd.css: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2012-2013 Anton Simonov 5 | Copyright (C) 2014-2015 Vsevolod Stakhov 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | */ 25 | 26 | /* stylelint-disable selector-id-pattern */ 27 | 28 | :root { 29 | font-size: 14px; 30 | 31 | /* Tweak bootstrap 5 colors for better accessibility */ 32 | --bs-danger-rgb: 221, 0, 0; 33 | --bs-success-rgb: 40, 139, 69; 34 | } 35 | 36 | /* bootstrap 4 overrides */ 37 | body { 38 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 39 | } 40 | code { 41 | font-size: 90%; 42 | } 43 | small, 44 | .small { 45 | font-size: 85%; 46 | } 47 | .text-secondary { 48 | color: #666 !important; 49 | } 50 | .navbar { 51 | padding-top: 0; 52 | padding-bottom: 0; 53 | margin-bottom: 20px; 54 | border-bottom: 1px solid rgb(231 231 231); 55 | } 56 | .nav-pills .nav-link.active { 57 | background-color: #e7e7e7; 58 | } 59 | .danger > td { 60 | background-color: #fbe9e5; 61 | } 62 | .success > td { 63 | background-color: #eef9e7; 64 | } 65 | td.warning { 66 | background-color: #fff8e6; 67 | } 68 | @media (max-width: 1199px) { 69 | .navbar-collapse.order-3 { 70 | border-top: 1px solid #dee2e6 !important; 71 | } 72 | /* Avoid navbar toggler hiding on navbar collapse */ 73 | .navbar-toggler { 74 | display: block !important; 75 | } 76 | } 77 | 78 | /* Tweak FooTable for Bootstrap 5 */ 79 | .footable .btn, 80 | .footable .form-control { 81 | border-radius: 0.375rem; 82 | } 83 | 84 | /* bootstrap 4 additionals */ 85 | .btn-group-xs > .btn, 86 | .btn-xs { 87 | padding: .06rem .3rem; 88 | font-size: .875rem; 89 | line-height: 1.5; 90 | border-radius: .2rem; 91 | } 92 | .btn.disabled, 93 | .btn[disabled], 94 | fieldset[disabled] .btn { 95 | pointer-events: auto; 96 | cursor: not-allowed; 97 | } 98 | .w-1 { 99 | width: 1%; 100 | } 101 | 102 | a { 103 | outline: none; 104 | } 105 | textarea { 106 | font-family: "Courier New", Courier, monospace; 107 | resize: vertical; 108 | } 109 | 110 | /* Tweak FooTable for Bootstrap 4 */ 111 | .footable .dropdown-toggle::after { 112 | content: none; 113 | } 114 | .footable .btn-outline-secondary { 115 | border-color: rgb(108 117 125); 116 | } 117 | .footable .btn-group > .btn:not(:first-child), 118 | .footable .btn-group > .btn-group:not(:first-child) > .btn { 119 | border-top-left-radius: 0; 120 | border-bottom-left-radius: 0; 121 | } 122 | .footable .input-sm { 123 | height: 30px; 124 | } 125 | 126 | .footable-header .fooicon { 127 | font-size: 12px; 128 | } 129 | .footable .pagination > li:last-child > a { 130 | border-top-right-radius:4px; 131 | border-bottom-right-radius:4px 132 | } 133 | 134 | /* local overrides */ 135 | .navbar-brand > img { 136 | height: 50px; 137 | } 138 | .btn-group > .btn.radius-right { 139 | border-top-right-radius: .25rem !important; 140 | border-bottom-right-radius: .25rem !important; 141 | } 142 | 143 | input.form-control[type="number"] { 144 | width: 4em; 145 | padding-left: 0; 146 | padding-right: 0; 147 | text-align: center; 148 | } 149 | input.action-scores { 150 | margin: 5px -7em 5px 0; 151 | } 152 | table#symbolsTable input[type="number"] { 153 | width: 6em; 154 | font-size: 11px; 155 | } 156 | 157 | .notification-area { 158 | position: fixed; 159 | z-index: 1050; 160 | top: 44px; 161 | left: 0; 162 | right: 0; 163 | padding: 8px; 164 | } 165 | .alert { 166 | margin-bottom: 4px; 167 | } 168 | .alert.alert-modal { 169 | top: 0; 170 | } 171 | .alert strong { 172 | display: inline-block; 173 | padding-left: 35px; 174 | } 175 | .alert, 176 | .alert h4 { 177 | color: #c09853; 178 | } 179 | .alert h4 { 180 | margin: 0; 181 | } 182 | .alert-success { 183 | color: #468847; 184 | background: #dff0d8; 185 | border-color: #d6e9c6; 186 | } 187 | .alert-success h4 { 188 | color: #468847; 189 | } 190 | .alert-danger, 191 | .alert-error { 192 | color: #b94a48; 193 | background: #f2dede; 194 | border-color: #eed3d7; 195 | } 196 | .alert-danger h4, 197 | .alert-error h4 { 198 | color: #b94a48; 199 | } 200 | .alert-info { 201 | color: #3a87ad; 202 | background: #d9edf7; 203 | border-color: #bce8f1; 204 | } 205 | .alert-info h4 { 206 | color: #3a87ad; 207 | } 208 | 209 | #authInvalidCharFeedback, 210 | #authUnauthorizedFeedback { 211 | position: unset; 212 | padding-top: 0.1rem; 213 | padding-bottom: 0.1rem; 214 | } 215 | 216 | .card-header, 217 | .modal-header { 218 | background-color: #f3f3f3; 219 | background-image: linear-gradient(to bottom, #fdfdfd, #eaeaea); 220 | } 221 | .card-header .h6 { 222 | font-size: 0.857rem; 223 | } 224 | 225 | .stat-box { 226 | background-color: #f3f3f3; 227 | background-image: linear-gradient(to bottom, #f9f9f9, #ededed); 228 | line-height: 1; 229 | } 230 | .stat-box:not(.float-end) { 231 | min-width: 90px; 232 | } 233 | .stat-box .widget { 234 | font-size: 10px; 235 | } 236 | .stat-box .widget strong { 237 | font-size: 26px; 238 | } 239 | 240 | /* Symbols coloring */ 241 | .symbol-default { 242 | border-radius: 2px; 243 | padding-left: 2px; 244 | padding-right: 2px; 245 | } 246 | .symbol-default:hover { 247 | background-color: #e6e6e6; 248 | } 249 | .symbol-negative.symbol-negative { 250 | background-color: #eef9e7; 251 | } 252 | .symbol-positive.symbol-positive { 253 | background-color: #fbe9e5; 254 | } 255 | .symbol-special { 256 | background-color: #e2e9fe; 257 | } 258 | .symbol-negative:hover { 259 | background-color: #dcf9d3; 260 | } 261 | .symbol-positive:hover { 262 | background-color: #fbd6d1; 263 | } 264 | .symbol-special:hover { 265 | background-color: #cddbff; 266 | } 267 | 268 | .map-link { 269 | display: block; 270 | color: #0088cc; 271 | cursor: pointer; 272 | } 273 | .map-link:hover, 274 | .map-link:focus { 275 | color: #005580; 276 | text-decoration: underline; 277 | } 278 | 279 | /* Font Awesome icons size */ 280 | .svg-inline--fa { /* stylelint-disable-line selector-class-pattern */ 281 | font-size: 16px; 282 | } 283 | /* Increase refresh button spinner speed */ 284 | #refresh .fa-spin { 285 | -webkit-animation: fa-spin 1s linear infinite; 286 | animation: fa-spin 1s linear infinite; 287 | } 288 | 289 | /* Some spacing tweaks */ 290 | .notification-area div > button:not(.close) { 291 | margin-right: 9px; 292 | } 293 | 294 | .status-table thead th:first-child, 295 | .status-table td:first-child { 296 | border-left: none; 297 | } 298 | .status-table thead th:last-child, 299 | .status-table td:last-child { 300 | border-right: none; 301 | } 302 | .status-table thead tr { 303 | border-top: none; 304 | } 305 | .status-table tr:last-child, 306 | .status-table tr:last-child td { 307 | border-bottom: none; 308 | } 309 | 310 | .footable-header, 311 | .footable tr:not(.footable-detail-row) > td { 312 | font-size: 11px; 313 | } 314 | 315 | .status-table tr:last-child td:last-child { 316 | border-radius: 0 0 calc(var(--bs-border-radius) + 1px) 0; 317 | } 318 | .status-table :not(:has([rowspan])) tr:last-child td:first-child, 319 | .status-table :nth-last-child(1 of tr:has([rowspan])) td:first-child { 320 | border-radius: 0 0 0 calc(var(--bs-border-radius) + 1px); 321 | } 322 | 323 | /* RRD summary */ 324 | #summary-row { 325 | padding-left: 80px; 326 | padding-right: 80px; 327 | } 328 | .col-fixed, 329 | .col-fluid { 330 | position: relative; 331 | float: left; 332 | } 333 | .col-fixed { 334 | width: 200px; 335 | min-height: 1px; /* make an empty div take space */ 336 | } 337 | .col-fluid { 338 | width: calc(100% - 200px); 339 | } 340 | #rrd-table_toggle { 341 | position: absolute; 342 | top: 0; 343 | height: 100%; 344 | width: 100%; 345 | } 346 | #rrd-table { 347 | margin-bottom: 2px; 348 | width: 100% !important; 349 | text-align: left; 350 | font-size: 12px; 351 | z-index: 100; 352 | } 353 | #rrd-table td { 354 | color: inherit; 355 | padding-top: 2px; 356 | padding-bottom: 2px; 357 | } 358 | #rrd-total { 359 | padding-left: 8px; 360 | margin-bottom: 10px; 361 | text-align: left; 362 | font-size: 12px; 363 | } 364 | 365 | /* Throughput graph controls */ 366 | #graph_controls select { 367 | margin: 10px 20px 0; 368 | display: inline-block; 369 | width: auto; 370 | border: 1px solid grey; 371 | } 372 | 373 | /* history table */ 374 | .footable-details.table { 375 | margin-bottom: 0; 376 | } 377 | #historyTable_scan > tbody > tr > td, 378 | #historyTable_scan > thead > tr > th, 379 | #historyTable_history > tbody > tr > td, 380 | #historyTable_history > thead > tr > th { 381 | padding: 4px; 382 | } 383 | #historyTable_scan > thead > tr > th, 384 | #historyTable_history > thead > tr > th { 385 | padding-right: 20px; 386 | } 387 | @media (min-width: 576px) and (max-width: 1199px) { 388 | .history-col-time { 389 | /* Avoid taking multiple lines in every row when one of rows has long ID */ 390 | white-space: nowrap; 391 | } 392 | } 393 | .footable-filtering-search .dropdown-menu .sym-order-toggle { 394 | display: none; 395 | } 396 | 397 | #history_page_size { 398 | width: 6em !important; 399 | text-align: center; 400 | } 401 | 402 | .outline-dashed-primary { outline: 2px dashed var(--bs-primary); } 403 | 404 | .scorebar-spam { 405 | background-color: rgba(240 0 0 / 0.1) !important; 406 | } 407 | .scorebar-ham { 408 | background: rgba(100 230 80 / 0.1) !important; 409 | } 410 | 411 | .danger .icon { 412 | color: #b94a48; 413 | } 414 | .success .icon { 415 | color: #468847; 416 | } 417 | 418 | #learnServers { 419 | display: flex; 420 | } 421 | 422 | #nprogress .bar { 423 | height: 1px; 424 | } 425 | 426 | @media (min-width: 992px) { 427 | #selectors > .card { 428 | height: calc(100vh - 96px); 429 | } 430 | #row-main { 431 | /* necessary to hide collapsed sidebar */ 432 | overflow-x: hidden; 433 | } 434 | #content > div { 435 | display: flex; 436 | } 437 | } 438 | #content { 439 | transition: all 0.3s ease; 440 | transition-property: flex-basis, max-width, width; 441 | } 442 | 443 | .sidebar { 444 | padding: 8px; 445 | background-color: #ffe; 446 | transition: margin 0.3s ease; 447 | } 448 | .collapsed { 449 | /* hide it for small displays */ 450 | display: none; 451 | } 452 | @media (min-width: 992px) { 453 | .collapsed { 454 | display: block; 455 | } 456 | #sidebar-left.collapsed { 457 | /* same width as sidebar */ 458 | margin-left: -25%; 459 | } 460 | #sidebar-right.collapsed { 461 | /* same width as sidebar */ 462 | margin-right: -25%; 463 | } 464 | } 465 | 466 | #selectors > .card > .card-body { 467 | min-height: 0; 468 | } 469 | 470 | .sidebar-nav { 471 | width: 20px; 472 | } 473 | .sidebar-nav .nav-link, 474 | .sidebar-nav .nav-link:hover { 475 | border: 1px solid #ddd; 476 | } 477 | #sidebar-tab-left > a, 478 | #sidebar-tab-right > a { 479 | background-color: #ffe; 480 | margin-left: 12px; 481 | margin-right: 12px; 482 | } 483 | #sidebar-tab-left { 484 | transform: rotate(180deg); 485 | } 486 | #sidebar-tab-text-left { 487 | transform: rotate(180deg); 488 | } 489 | @media (min-width: 992px) { 490 | #sidebar-left { 491 | border-bottom-left-radius: 3.5px; 492 | } 493 | #sidebar-right { 494 | border-bottom-right-radius: 3.5px; 495 | } 496 | .sidebar-nav { 497 | padding-right: 0; 498 | display: block; 499 | } 500 | #content { 501 | border-left: 1px solid #ddd; 502 | border-right: 1px solid #ddd; 503 | } 504 | #sidebar-tab-left { 505 | display: flex; 506 | transform: translateX(-50%) rotate(90deg) translate(50%, -50%); 507 | } 508 | #sidebar-tab-right { 509 | float: right; 510 | transform: translateX(50%) rotate(-90deg) translate(-50%, -50%); 511 | } 512 | } 513 | @media (max-width: 991.98px) { 514 | #sidebar-right { 515 | border-bottom-left-radius: 3.5px; 516 | border-bottom-right-radius: 3.5px; 517 | } 518 | #content { 519 | border-top: 1px solid #ddd; 520 | border-bottom: 1px solid #ddd; 521 | } 522 | #sidebar-tab-right { 523 | bottom: 0; 524 | right: 0; 525 | } 526 | } 527 | 528 | #navBar .navbar-nav .nav-link { 529 | padding: 15px; 530 | } 531 | 532 | #modalDialog > .modal-dialog { 533 | /* Center the modal vertically */ 534 | top: 50%; 535 | transform: translate(0, -50%); 536 | -webkit-transform: translate(0, -50%); 537 | } 538 | 539 | .codejar-wrap, 540 | #editor.map-textarea { 541 | border-radius: 6px; 542 | max-height: calc(100vh - 178px); 543 | max-width: 100%; 544 | min-width: 100%; 545 | overflow: auto; 546 | resize: both; 547 | } 548 | #editor.map-textarea { 549 | height: calc(100vh - 178px); 550 | width: calc(100vw - 36px - 3rem); 551 | } 552 | .codejar-wrap, 553 | #editor.map-textarea, 554 | #editor.map-textarea:focus { 555 | background: rgb(0 47 79); 556 | color: silver; 557 | } 558 | .codejar-wrap { 559 | /* Fix line wrapping */ 560 | scrollbar-width: thin; 561 | } 562 | .codejar-linenumbers-inner-wrap { 563 | bottom: unset; 564 | } 565 | .codejar-linenumbers { 566 | background: rgba(255 255 255 / 0.07) !important; 567 | } 568 | .codejar-linenumber { 569 | color: rgba(120 120 120 / 1) !important; 570 | text-align: right; 571 | } 572 | .editor { 573 | font-family: monospace; 574 | font-size: 14px; 575 | font-weight: 400; 576 | letter-spacing: normal; 577 | margin-left: 10px; 578 | margin-right: 10px; 579 | resize: unset !important; 580 | tab-size: 4; 581 | -moz-tab-size: 4; 582 | overflow: unset !important; 583 | } 584 | 585 | /* Prism show-invisibles plugin overrides */ 586 | .token.tab:not(:empty)::before { 587 | content: "\23af\27F6"; 588 | } 589 | /* Temporarily remove CR and LF tokens as they overflow line width */ 590 | .token.cr::before, 591 | .token.crlf::before, 592 | .token.lf::before { 593 | content: ""; 594 | } 595 | 596 | /* Preloader */ 597 | .blinking { 598 | animation: blinker 1.2s ease-in-out infinite; 599 | } 600 | @keyframes blinker { 601 | 50% { 602 | -webkit-filter: invert(1); 603 | filter: invert(1); 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /www/css/svg-with-js.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.13.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | .svg-inline--fa,svg:not(:root).svg-inline--fa{overflow:visible}.svg-inline--fa{display:inline-block;font-size:inherit;height:1em;vertical-align:-.125em}.svg-inline--fa.fa-lg{vertical-align:-.225em}.svg-inline--fa.fa-w-1{width:.0625em}.svg-inline--fa.fa-w-2{width:.125em}.svg-inline--fa.fa-w-3{width:.1875em}.svg-inline--fa.fa-w-4{width:.25em}.svg-inline--fa.fa-w-5{width:.3125em}.svg-inline--fa.fa-w-6{width:.375em}.svg-inline--fa.fa-w-7{width:.4375em}.svg-inline--fa.fa-w-8{width:.5em}.svg-inline--fa.fa-w-9{width:.5625em}.svg-inline--fa.fa-w-10{width:.625em}.svg-inline--fa.fa-w-11{width:.6875em}.svg-inline--fa.fa-w-12{width:.75em}.svg-inline--fa.fa-w-13{width:.8125em}.svg-inline--fa.fa-w-14{width:.875em}.svg-inline--fa.fa-w-15{width:.9375em}.svg-inline--fa.fa-w-16{width:1em}.svg-inline--fa.fa-w-17{width:1.0625em}.svg-inline--fa.fa-w-18{width:1.125em}.svg-inline--fa.fa-w-19{width:1.1875em}.svg-inline--fa.fa-w-20{width:1.25em}.svg-inline--fa.fa-pull-left{margin-right:.3em;width:auto}.svg-inline--fa.fa-pull-right{margin-left:.3em;width:auto}.svg-inline--fa.fa-border{height:1.5em}.svg-inline--fa.fa-li{width:2em}.svg-inline--fa.fa-fw{width:1.25em}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers-text{left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-counter{background-color:#ff253a;border-radius:1em;-webkit-box-sizing:border-box;box-sizing:border-box;color:#fff;height:1.5em;line-height:1;max-width:5em;min-width:1.5em;overflow:hidden;padding:.25em;right:0;text-overflow:ellipsis;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-bottom-right{bottom:0;right:0;top:auto;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:bottom right;transform-origin:bottom right}.fa-layers-bottom-left{bottom:0;left:0;right:auto;top:auto;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:bottom left;transform-origin:bottom left}.fa-layers-top-right{right:0;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-top-left{left:0;right:auto;top:0;-webkit-transform:scale(.25);transform:scale(.25);-webkit-transform-origin:top left;transform-origin:top left}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;position:relative;width:2.5em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.svg-inline--fa.fa-stack-1x{height:1em;width:1.25em}.svg-inline--fa.fa-stack-2x{height:2em;width:2.5em}.fa-inverse{color:#fff}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.svg-inline--fa .fa-primary{fill:var(--fa-primary-color,currentColor);opacity:1;opacity:var(--fa-primary-opacity,1)}.svg-inline--fa .fa-secondary{fill:var(--fa-secondary-color,currentColor)}.svg-inline--fa .fa-secondary,.svg-inline--fa.fa-swap-opacity .fa-primary{opacity:.4;opacity:var(--fa-secondary-opacity,.4)}.svg-inline--fa.fa-swap-opacity .fa-secondary{opacity:1;opacity:var(--fa-primary-opacity,1)}.svg-inline--fa mask .fa-primary,.svg-inline--fa mask .fa-secondary{fill:#000}.fad.fa-inverse{color:#fff} -------------------------------------------------------------------------------- /www/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poralix/rspamd/fb3a969e51c69ddf52143348e3b3978b52cc2aa4/www/favicon-16x16.png -------------------------------------------------------------------------------- /www/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poralix/rspamd/fb3a969e51c69ddf52143348e3b3978b52cc2aa4/www/favicon-32x32.png -------------------------------------------------------------------------------- /www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poralix/rspamd/fb3a969e51c69ddf52143348e3b3978b52cc2aa4/www/favicon.ico -------------------------------------------------------------------------------- /www/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poralix/rspamd/fb3a969e51c69ddf52143348e3b3978b52cc2aa4/www/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /www/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poralix/rspamd/fb3a969e51c69ddf52143348e3b3978b52cc2aa4/www/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /www/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poralix/rspamd/fb3a969e51c69ddf52143348e3b3978b52cc2aa4/www/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /www/img/asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poralix/rspamd/fb3a969e51c69ddf52143348e3b3978b52cc2aa4/www/img/asc.png -------------------------------------------------------------------------------- /www/img/desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poralix/rspamd/fb3a969e51c69ddf52143348e3b3978b52cc2aa4/www/img/desc.png -------------------------------------------------------------------------------- /www/img/rspamd_logo_navbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poralix/rspamd/fb3a969e51c69ddf52143348e3b3978b52cc2aa4/www/img/rspamd_logo_navbar.png -------------------------------------------------------------------------------- /www/js/app/common.js: -------------------------------------------------------------------------------- 1 | /* global jQuery */ 2 | 3 | define(["jquery", "nprogress"], 4 | ($, NProgress) => { 5 | "use strict"; 6 | const ui = { 7 | chartLegend: [ 8 | {label: "reject", color: "#FF0000"}, 9 | {label: "soft reject", color: "#BF8040"}, 10 | {label: "rewrite subject", color: "#FF6600"}, 11 | {label: "add header", color: "#FFAD00"}, 12 | {label: "greylist", color: "#436EEE"}, 13 | {label: "no action", color: "#66CC00"} 14 | ], 15 | locale: (localStorage.getItem("selected_locale") === "custom") ? localStorage.getItem("custom_locale") : null, 16 | neighbours: [], 17 | page_size: { 18 | scan: 25, 19 | errors: 25, 20 | history: 25 21 | }, 22 | symbols: { 23 | scan: [], 24 | history: [] 25 | }, 26 | tables: {} 27 | }; 28 | 29 | 30 | NProgress.configure({ 31 | minimum: 0.01, 32 | showSpinner: false, 33 | }); 34 | 35 | function getPassword() { 36 | return sessionStorage.getItem("Password"); 37 | } 38 | 39 | function alertMessage(alertClass, alertText) { 40 | const a = $("
" + 41 | "" + 42 | "" + alertText + ""); 43 | $(".notification-area").append(a); 44 | 45 | setTimeout(() => { 46 | $(a).fadeTo(500, 0).slideUp(500, function () { 47 | $(this).alert("close"); 48 | }); 49 | }, 5000); 50 | } 51 | 52 | function queryServer(neighbours_status, ind, req_url, o) { 53 | neighbours_status[ind].checked = false; 54 | neighbours_status[ind].data = {}; 55 | neighbours_status[ind].status = false; 56 | const req_params = { 57 | jsonp: false, 58 | data: o.data, 59 | headers: $.extend({Password: getPassword()}, o.headers), 60 | url: neighbours_status[ind].url + req_url, 61 | xhr: function () { 62 | const xhr = $.ajaxSettings.xhr(); 63 | // Download progress 64 | if (req_url !== "neighbours") { 65 | xhr.addEventListener("progress", (e) => { 66 | if (e.lengthComputable) { 67 | neighbours_status[ind].percentComplete = e.loaded / e.total; 68 | const percentComplete = neighbours_status 69 | .reduce((prev, curr) => (curr.percentComplete ? curr.percentComplete + prev : prev), 0); 70 | NProgress.set(percentComplete / neighbours_status.length); 71 | } 72 | }, false); 73 | } 74 | return xhr; 75 | }, 76 | success: function (json) { 77 | neighbours_status[ind].checked = true; 78 | neighbours_status[ind].status = true; 79 | neighbours_status[ind].data = json; 80 | }, 81 | error: function (jqXHR, textStatus, errorThrown) { 82 | neighbours_status[ind].checked = true; 83 | function errorMessage() { 84 | alertMessage("alert-error", neighbours_status[ind].name + " > " + 85 | (o.errorMessage ? o.errorMessage : "Request failed") + 86 | (errorThrown ? ": " + errorThrown : "")); 87 | } 88 | if (o.error) { 89 | o.error(neighbours_status[ind], 90 | jqXHR, textStatus, errorThrown); 91 | } else if (o.errorOnceId) { 92 | const alert_status = o.errorOnceId + neighbours_status[ind].name; 93 | if (!(alert_status in sessionStorage)) { 94 | sessionStorage.setItem(alert_status, true); 95 | errorMessage(); 96 | } 97 | } else { 98 | errorMessage(); 99 | } 100 | }, 101 | complete: function (jqXHR) { 102 | if (neighbours_status.every((elt) => elt.checked)) { 103 | if (neighbours_status.some((elt) => elt.status)) { 104 | if (o.success) { 105 | o.success(neighbours_status, jqXHR); 106 | } else { 107 | alertMessage("alert-success", "Request completed"); 108 | } 109 | } else { 110 | alertMessage("alert-error", "Request failed"); 111 | } 112 | if (o.complete) o.complete(); 113 | NProgress.done(); 114 | } 115 | }, 116 | statusCode: o.statusCode 117 | }; 118 | if (o.method) { 119 | req_params.method = o.method; 120 | } 121 | if (o.params) { 122 | $.each(o.params, (k, v) => { 123 | req_params[k] = v; 124 | }); 125 | } 126 | $.ajax(req_params); 127 | } 128 | 129 | 130 | // Public functions 131 | 132 | ui.alertMessage = alertMessage; 133 | ui.getPassword = getPassword; 134 | 135 | // Get selectors' current state 136 | ui.getSelector = function (id) { 137 | const e = document.getElementById(id); 138 | return e.options[e.selectedIndex].value; 139 | }; 140 | 141 | ui.getServer = function () { 142 | const checked_server = ui.getSelector("selSrv"); 143 | return (checked_server === "All SERVERS") ? "local" : checked_server; 144 | }; 145 | 146 | /** 147 | * @param {string} url - A string containing the URL to which the request is sent 148 | * @param {Object} [options] - A set of key/value pairs that configure the Ajax request. All settings are optional. 149 | * 150 | * @param {Function} [options.complete] - A function to be called when the requests to all neighbours complete. 151 | * @param {Object|string|Array} [options.data] - Data to be sent to the server. 152 | * @param {Function} [options.error] - A function to be called if the request fails. 153 | * @param {string} [options.errorMessage] - Text to display in the alert message if the request fails. 154 | * @param {string} [options.errorOnceId] - A prefix of the alert ID to be added to the session storage. If the 155 | * parameter is set, the error for each server will be displayed only once per session. 156 | * @param {Object} [options.headers] - An object of additional header key/value pairs to send along with requests 157 | * using the XMLHttpRequest transport. 158 | * @param {string} [options.method] - The HTTP method to use for the request. 159 | * @param {Object} [options.params] - An object of additional jQuery.ajax() settings key/value pairs. 160 | * @param {string} [options.server] - A server to which send the request. 161 | * @param {Function} [options.success] - A function to be called if the request succeeds. 162 | * 163 | * @returns {undefined} 164 | */ 165 | ui.query = function (url, options) { 166 | // Force options to be an object 167 | const o = options || {}; 168 | Object.keys(o).forEach((option) => { 169 | if (["complete", "data", "error", "errorMessage", "errorOnceId", "headers", "method", "params", "server", 170 | "statusCode", "success"] 171 | .indexOf(option) < 0) { 172 | throw new Error("Unknown option: " + option); 173 | } 174 | }); 175 | 176 | let neighbours_status = [{ 177 | name: "local", 178 | host: "local", 179 | url: "", 180 | }]; 181 | o.server = o.server || ui.getSelector("selSrv"); 182 | if (o.server === "All SERVERS") { 183 | queryServer(neighbours_status, 0, "neighbours", { 184 | success: function (json) { 185 | const [{data}] = json; 186 | if (jQuery.isEmptyObject(data)) { 187 | ui.neighbours = { 188 | local: { 189 | host: window.location.host, 190 | url: window.location.origin + window.location.pathname 191 | } 192 | }; 193 | } else { 194 | ui.neighbours = data; 195 | } 196 | neighbours_status = []; 197 | $.each(ui.neighbours, (ind) => { 198 | neighbours_status.push({ 199 | name: ind, 200 | host: ui.neighbours[ind].host, 201 | url: ui.neighbours[ind].url, 202 | }); 203 | }); 204 | $.each(neighbours_status, (ind) => { 205 | queryServer(neighbours_status, ind, url, o); 206 | }); 207 | }, 208 | errorMessage: "Cannot receive neighbours data" 209 | }); 210 | } else { 211 | if (o.server !== "local") { 212 | neighbours_status = [{ 213 | name: o.server, 214 | host: ui.neighbours[o.server].host, 215 | url: ui.neighbours[o.server].url, 216 | }]; 217 | } 218 | queryServer(neighbours_status, 0, url, o); 219 | } 220 | }; 221 | 222 | ui.escapeHTML = function (string) { 223 | const htmlEscaper = /[&<>"'/`=]/g; 224 | const htmlEscapes = { 225 | "&": "&", 226 | "<": "<", 227 | ">": ">", 228 | "\"": """, 229 | "'": "'", 230 | "/": "/", 231 | "`": "`", 232 | "=": "=" 233 | }; 234 | return String(string).replace(htmlEscaper, (match) => htmlEscapes[match]); 235 | }; 236 | 237 | return ui; 238 | }); 239 | -------------------------------------------------------------------------------- /www/js/app/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Vsevolod Stakhov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | /* global require */ 26 | 27 | define(["jquery", "app/common"], 28 | ($, common) => { 29 | "use strict"; 30 | const ui = {}; 31 | 32 | ui.getActions = function getActions() { 33 | common.query("actions", { 34 | success: function (data) { 35 | $("#actionsFormField").empty(); 36 | const items = []; 37 | $.each(data[0].data, (i, item) => { 38 | const actionsOrder = ["greylist", "add header", "rewrite subject", "reject"]; 39 | const idx = actionsOrder.indexOf(item.action); 40 | if (idx >= 0) { 41 | items.push({ 42 | idx: idx, 43 | html: 44 | '
' + 45 | '" + 46 | '
' + 47 | '' + 49 | "
" + 50 | "
" 51 | }); 52 | } 53 | }); 54 | 55 | items.sort((a, b) => a.idx - b.idx); 56 | 57 | $("#actionsFormField").html( 58 | items.map((e) => e.html).join("")); 59 | }, 60 | server: common.getServer() 61 | }); 62 | }; 63 | 64 | ui.saveActions = function (server) { 65 | function descending(arr) { 66 | let desc = true; 67 | const filtered = arr.filter((el) => el !== null); 68 | for (let i = 0; i < filtered.length - 1; i++) { 69 | if (filtered[i + 1] >= filtered[i]) { 70 | desc = false; 71 | break; 72 | } 73 | } 74 | return desc; 75 | } 76 | 77 | const elts = (function () { 78 | const values = []; 79 | const inputs = $("#actionsForm :input[data-id=\"action\"]"); 80 | // Rspamd order: [spam, rewrite_subject, probable_spam, greylist] 81 | values[0] = parseFloat(inputs[3].value); 82 | values[1] = parseFloat(inputs[2].value); 83 | values[2] = parseFloat(inputs[1].value); 84 | values[3] = parseFloat(inputs[0].value); 85 | 86 | return JSON.stringify(values); 87 | }()); 88 | // String to array for comparison 89 | const eltsArray = JSON.parse(elts); 90 | if (eltsArray[0] < 0) { 91 | common.alertMessage("alert-modal alert-error", "Spam can not be negative"); 92 | } else if (eltsArray[1] < 0) { 93 | common.alertMessage("alert-modal alert-error", "Rewrite subject can not be negative"); 94 | } else if (eltsArray[2] < 0) { 95 | common.alertMessage("alert-modal alert-error", "Probable spam can not be negative"); 96 | } else if (eltsArray[3] < 0) { 97 | common.alertMessage("alert-modal alert-error", "Greylist can not be negative"); 98 | } else if (descending(eltsArray)) { 99 | common.query("saveactions", { 100 | method: "POST", 101 | params: { 102 | data: elts, 103 | dataType: "json" 104 | }, 105 | server: server 106 | }); 107 | } else { 108 | common.alertMessage("alert-modal alert-error", "Incorrect order of actions thresholds"); 109 | } 110 | }; 111 | 112 | ui.getMaps = function () { 113 | const $listmaps = $("#listMaps"); 114 | $listmaps.closest(".card").hide(); 115 | common.query("maps", { 116 | success: function (json) { 117 | const [{data}] = json; 118 | $listmaps.empty(); 119 | $("#modalBody").empty(); 120 | const $tbody = $(""); 121 | 122 | $.each(data, (i, item) => { 123 | let $td = 'Read'; 124 | if (!(item.editable === false || common.read_only)) { 125 | $td = $($td).append(' Write'); 126 | } 127 | const $tr = $("").append($td); 128 | 129 | const $span = $('' + 130 | item.uri + "").data("item", item); 131 | $span.wrap("").parent().appendTo($tr); 132 | $("" + item.description + "").appendTo($tr); 133 | $tr.appendTo($tbody); 134 | }); 135 | $tbody.appendTo($listmaps); 136 | $listmaps.closest(".card").show(); 137 | }, 138 | server: common.getServer() 139 | }); 140 | }; 141 | 142 | 143 | let jar = {}; 144 | const editor = { 145 | advanced: { 146 | codejar: true, 147 | elt: "div", 148 | class: "editor language-clike", 149 | readonly_attr: {contenteditable: false}, 150 | }, 151 | basic: { 152 | elt: "textarea", 153 | class: "form-control map-textarea", 154 | readonly_attr: {readonly: true}, 155 | } 156 | }; 157 | let mode = "advanced"; 158 | 159 | // Modal form for maps 160 | $(document).on("click", "[data-bs-toggle=\"modal\"]", function () { 161 | const item = $(this).data("item"); 162 | common.query("getmap", { 163 | headers: { 164 | Map: item.map 165 | }, 166 | success: function (data) { 167 | // Highlighting a large amount of text is unresponsive 168 | mode = (new Blob([data[0].data]).size > 5120) ? "basic" : $("input[name=editorMode]:checked").val(); 169 | 170 | $("<" + editor[mode].elt + ' id="editor" class="' + editor[mode].class + '" data-id="' + item.map + 171 | '">").appendTo("#modalBody"); 172 | 173 | if (editor[mode].codejar) { 174 | require(["codejar", "linenumbers", "prism"], (CodeJar, withLineNumbers, Prism) => { 175 | jar = new CodeJar( 176 | document.querySelector("#editor"), 177 | withLineNumbers((el) => Prism.highlightElement(el)) 178 | ); 179 | jar.updateCode(data[0].data); 180 | }); 181 | } else { 182 | document.querySelector("#editor").innerHTML = common.escapeHTML(data[0].data); 183 | } 184 | 185 | let icon = "fa-edit"; 186 | if (item.editable === false || common.read_only) { 187 | $("#editor").attr(editor[mode].readonly_attr); 188 | icon = "fa-eye"; 189 | $("#modalSaveGroup").hide(); 190 | } else { 191 | $("#modalSaveGroup").show(); 192 | } 193 | $("#modalDialog .modal-header").find("[data-fa-i2svg]").addClass(icon); 194 | $("#modalTitle").html(item.uri); 195 | 196 | $("#modalDialog").modal("show"); 197 | }, 198 | errorMessage: "Cannot receive maps data", 199 | server: common.getServer() 200 | }); 201 | return false; 202 | }); 203 | $("#modalDialog").on("hidden.bs.modal", () => { 204 | if (editor[mode].codejar) { 205 | jar.destroy(); 206 | $(".codejar-wrap").remove(); 207 | } else { 208 | $("#editor").remove(); 209 | } 210 | }); 211 | 212 | $("#saveActionsBtn").on("click", () => { 213 | ui.saveActions(); 214 | }); 215 | $("#saveActionsClusterBtn").on("click", () => { 216 | ui.saveActions("All SERVERS"); 217 | }); 218 | 219 | function saveMap(server) { 220 | common.query("savemap", { 221 | success: function () { 222 | common.alertMessage("alert-success", "Map data successfully saved"); 223 | $("#modalDialog").modal("hide"); 224 | }, 225 | errorMessage: "Save map error", 226 | method: "POST", 227 | headers: { 228 | Map: $("#editor").data("id"), 229 | }, 230 | params: { 231 | data: editor[mode].codejar ? jar.toString() : $("#editor").val(), 232 | dataType: "text", 233 | }, 234 | server: server 235 | }); 236 | } 237 | $("#modalSave").on("click", () => { 238 | saveMap(); 239 | }); 240 | $("#modalSaveAll").on("click", () => { 241 | saveMap("All SERVERS"); 242 | }); 243 | 244 | return ui; 245 | }); 246 | -------------------------------------------------------------------------------- /www/js/app/graph.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Vsevolod Stakhov 5 | Copyright (C) 2017 Alexander Moisseev 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | */ 25 | 26 | /* global FooTable */ 27 | 28 | define(["jquery", "app/common", "d3evolution", "d3pie", "d3", "footable"], 29 | ($, common, D3Evolution, D3Pie, d3) => { 30 | "use strict"; 31 | 32 | const rrd_pie_config = { 33 | cornerRadius: 2, 34 | size: { 35 | canvasWidth: 400, 36 | canvasHeight: 180, 37 | pieInnerRadius: "50%", 38 | pieOuterRadius: "80%" 39 | }, 40 | labels: { 41 | outer: { 42 | format: "none" 43 | }, 44 | inner: { 45 | hideWhenLessThanPercentage: 8, 46 | offset: 0 47 | }, 48 | }, 49 | padAngle: 0.02, 50 | pieCenterOffset: { 51 | x: -120, 52 | y: 10, 53 | }, 54 | total: { 55 | enabled: true 56 | }, 57 | }; 58 | 59 | const ui = {}; 60 | let prevUnit = "msg/s"; 61 | 62 | ui.draw = function (graphs, neighbours, checked_server, type) { 63 | const graph_options = { 64 | title: "Rspamd throughput", 65 | width: 1060, 66 | height: 370, 67 | yAxisLabel: "Message rate, msg/s", 68 | 69 | legend: { 70 | space: 140, 71 | entries: common.chartLegend 72 | } 73 | }; 74 | 75 | function initGraph() { 76 | const graph = new D3Evolution("graph", $.extend({}, graph_options, { 77 | yScale: common.getSelector("selYScale"), 78 | type: common.getSelector("selType"), 79 | interpolate: common.getSelector("selInterpolate"), 80 | convert: common.getSelector("selConvert"), 81 | })); 82 | $("#selYScale").change(function () { 83 | graph.yScale(this.value); 84 | }); 85 | $("#selConvert").change(function () { 86 | graph.convert(this.value); 87 | }); 88 | $("#selType").change(function () { 89 | graph.type(this.value); 90 | }); 91 | $("#selInterpolate").change(function () { 92 | graph.interpolate(this.value); 93 | }); 94 | 95 | return graph; 96 | } 97 | 98 | function getRrdSummary(json, scaleFactor) { 99 | const xExtents = d3.extent(d3.merge(json), (d) => d.x); 100 | const timeInterval = xExtents[1] - xExtents[0]; 101 | 102 | let total = 0; 103 | const rows = json.map((curr, i) => { 104 | // Time intervals that don't have data are excluded from average calculation as d3.mean()ignores nulls 105 | const avg = d3.mean(curr, (d) => d.y); 106 | // To find an integral on the whole time interval we need to convert nulls to zeroes 107 | // eslint-disable-next-line no-bitwise 108 | const value = d3.mean(curr, (d) => Number(d.y)) * timeInterval / scaleFactor ^ 0; 109 | const yExtents = d3.extent(curr, (d) => d.y); 110 | 111 | total += value; 112 | return { 113 | label: graph_options.legend.entries[i].label, 114 | value: value, 115 | min: Number(yExtents[0].toFixed(6)), 116 | avg: Number(avg.toFixed(6)), 117 | max: Number(yExtents[1].toFixed(6)), 118 | last: Number(curr[curr.length - 1].y.toFixed(6)), 119 | color: graph_options.legend.entries[i].color, 120 | }; 121 | }, []); 122 | 123 | return { 124 | rows: rows, 125 | total: total 126 | }; 127 | } 128 | 129 | function initSummaryTable(rows, unit) { 130 | common.tables.rrd_summary = FooTable.init("#rrd-table", { 131 | sorting: { 132 | enabled: true 133 | }, 134 | columns: [ 135 | {name: "label", title: "Action"}, 136 | {name: "value", title: "Messages", defaultContent: ""}, 137 | {name: "min", title: "Minimum, " + unit + "", defaultContent: ""}, 138 | {name: "avg", title: "Average, " + unit + "", defaultContent: ""}, 139 | {name: "max", title: "Maximum, " + unit + "", defaultContent: ""}, 140 | {name: "last", title: "Last, " + unit}, 141 | ], 142 | rows: rows.map((curr, i) => ({ 143 | options: { 144 | style: { 145 | color: graph_options.legend.entries[i].color 146 | } 147 | }, 148 | value: curr 149 | }), []) 150 | }); 151 | } 152 | 153 | function drawRrdTable(rows, unit) { 154 | if (Object.prototype.hasOwnProperty.call(common.tables, "rrd_summary")) { 155 | $.each(common.tables.rrd_summary.rows.all, (i, row) => { 156 | row.val(rows[i], false, true); 157 | }); 158 | } else { 159 | initSummaryTable(rows, unit); 160 | } 161 | } 162 | 163 | function updateWidgets(data) { 164 | let rrd_summary = {rows: []}; 165 | let unit = "msg/s"; 166 | 167 | if (data) { 168 | // Autoranging 169 | let scaleFactor = 1; 170 | const yMax = d3.max(d3.merge(data), (d) => d.y); 171 | if (yMax < 1) { 172 | scaleFactor = 60; 173 | unit = "msg/min"; 174 | data.forEach((s) => { 175 | s.forEach((d) => { 176 | if (d.y !== null) { d.y *= scaleFactor; } 177 | }); 178 | }); 179 | } 180 | 181 | rrd_summary = getRrdSummary(data, scaleFactor); 182 | } 183 | 184 | if (!graphs.rrd_pie) graphs.rrd_pie = new D3Pie("rrd-pie", rrd_pie_config); 185 | graphs.rrd_pie.data(rrd_summary.rows); 186 | 187 | graphs.graph.data(data); 188 | if (unit !== prevUnit) { 189 | graphs.graph.yAxisLabel("Message rate, " + unit); 190 | $(".unit").text(unit); 191 | prevUnit = unit; 192 | } 193 | drawRrdTable(rrd_summary.rows, unit); 194 | document.getElementById("rrd-total-value").innerHTML = rrd_summary.total; 195 | } 196 | 197 | if (!graphs.graph) { 198 | graphs.graph = initGraph(); 199 | } 200 | 201 | 202 | common.query("graph", { 203 | success: function (req_data) { 204 | let data = null; 205 | const neighbours_data = req_data 206 | .filter((d) => d.status) // filter out unavailable neighbours 207 | .map((d) => d.data); 208 | 209 | if (neighbours_data.length === 1) { 210 | [data] = neighbours_data; 211 | } else { 212 | let time_match = true; 213 | neighbours_data.reduce((res, curr, _, arr) => { 214 | if ((curr[0][0].x !== res[0][0].x) || 215 | (curr[0][curr[0].length - 1].x !== res[0][res[0].length - 1].x)) { 216 | time_match = false; 217 | common.alertMessage("alert-error", 218 | "Neighbours time extents do not match. Check if time is synchronized on all servers."); 219 | arr.splice(1); // Break out of .reduce() by mutating the source array 220 | } 221 | return curr; 222 | }); 223 | 224 | if (time_match) { 225 | data = neighbours_data.reduce((res, curr) => curr.map((action, j) => action.map((d, i) => ({ 226 | x: d.x, 227 | y: (res[j][i].y === null) ? d.y : res[j][i].y + d.y 228 | })))); 229 | } 230 | } 231 | updateWidgets(data); 232 | }, 233 | complete: function () { $("#refresh").removeAttr("disabled").removeClass("disabled"); }, 234 | errorMessage: "Cannot receive throughput data", 235 | errorOnceId: "alerted_graph_", 236 | data: {type: type} 237 | }); 238 | }; 239 | 240 | 241 | // Handling mouse events on overlapping elements 242 | $("#rrd-pie").mouseover(() => { 243 | $("#rrd-pie,#rrd-pie-tooltip").css("z-index", "200"); 244 | $("#rrd-table_toggle").css("z-index", "300"); 245 | }); 246 | $("#rrd-table_toggle").mouseover(() => { 247 | $("#rrd-pie,#rrd-pie-tooltip").css("z-index", "0"); 248 | $("#rrd-table_toggle").css("z-index", "0"); 249 | }); 250 | 251 | return ui; 252 | }); 253 | -------------------------------------------------------------------------------- /www/js/app/history.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Vsevolod Stakhov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | /* global FooTable */ 26 | 27 | define(["jquery", "app/common", "app/libft", "footable"], 28 | ($, common, libft) => { 29 | "use strict"; 30 | const ui = {}; 31 | let prevVersion = null; 32 | 33 | function process_history_legacy(data) { 34 | const items = []; 35 | 36 | function compare(e1, e2) { return e1.name.localeCompare(e2.name); } 37 | 38 | $("#selSymOrder_history, label[for='selSymOrder_history']").hide(); 39 | 40 | $.each(data, (i, item) => { 41 | item.time = libft.unix_time_format(item.unix_time); 42 | libft.preprocess_item(item); 43 | item.symbols = Object.keys(item.symbols) 44 | .map((key) => item.symbols[key]) 45 | .sort(compare) 46 | .map((e) => e.name) 47 | .join(", "); 48 | item.time = { 49 | value: libft.unix_time_format(item.unix_time), 50 | options: { 51 | sortValue: item.unix_time 52 | } 53 | }; 54 | 55 | items.push(item); 56 | }); 57 | 58 | return {items: items}; 59 | } 60 | 61 | function columns_legacy() { 62 | return [{ 63 | name: "id", 64 | title: "ID", 65 | style: { 66 | width: 300, 67 | maxWidth: 300, 68 | overflow: "hidden", 69 | textOverflow: "ellipsis", 70 | wordBreak: "keep-all", 71 | whiteSpace: "nowrap" 72 | } 73 | }, { 74 | name: "ip", 75 | title: "IP address", 76 | breakpoints: "xs sm", 77 | style: {width: 150, maxWidth: 150} 78 | }, { 79 | name: "action", 80 | title: "Action", 81 | style: {width: 110, maxWidth: 110} 82 | }, { 83 | name: "score", 84 | title: "Score", 85 | style: {maxWidth: 110}, 86 | sortValue: function (val) { return Number(val.options.sortValue); } 87 | }, { 88 | name: "symbols", 89 | title: "Symbols", 90 | breakpoints: "all", 91 | style: {width: 550, maxWidth: 550} 92 | }, { 93 | name: "size", 94 | title: "Message size", 95 | breakpoints: "xs sm", 96 | style: {width: 120, maxWidth: 120}, 97 | formatter: libft.formatBytesIEC 98 | }, { 99 | name: "scan_time", 100 | title: "Scan time", 101 | breakpoints: "xs sm", 102 | style: {maxWidth: 80}, 103 | sortValue: function (val) { return Number(val); } 104 | }, { 105 | sorted: true, 106 | direction: "DESC", 107 | name: "time", 108 | title: "Time", 109 | sortValue: function (val) { return Number(val.options.sortValue); } 110 | }, { 111 | name: "user", 112 | title: "Authenticated user", 113 | breakpoints: "xs sm", 114 | style: {width: 200, maxWidth: 200} 115 | }]; 116 | } 117 | 118 | const columns = { 119 | 2: libft.columns_v2("history"), 120 | legacy: columns_legacy() 121 | }; 122 | 123 | function process_history_data(data) { 124 | const process_functions = { 125 | 2: libft.process_history_v2, 126 | legacy: process_history_legacy 127 | }; 128 | let pf = process_functions.legacy; 129 | 130 | if (data.version) { 131 | const strkey = data.version.toString(); 132 | if (process_functions[strkey]) { 133 | pf = process_functions[strkey]; 134 | } 135 | } 136 | 137 | return pf(data, "history"); 138 | } 139 | 140 | function get_history_columns(data) { 141 | let func = columns.legacy; 142 | 143 | if (data.version) { 144 | const strkey = data.version.toString(); 145 | if (columns[strkey]) { 146 | func = columns[strkey]; 147 | } 148 | } 149 | 150 | return func; 151 | } 152 | 153 | ui.getHistory = function () { 154 | $("#refresh, #updateHistory").attr("disabled", true); 155 | common.query("history", { 156 | success: function (req_data) { 157 | function differentVersions(neighbours_data) { 158 | const dv = neighbours_data.some((e) => e.version !== neighbours_data[0].version); 159 | if (dv) { 160 | common.alertMessage("alert-error", 161 | "Neighbours history backend versions do not match. Cannot display history."); 162 | return true; 163 | } 164 | return false; 165 | } 166 | 167 | const neighbours_data = req_data 168 | .filter((d) => d.status) // filter out unavailable neighbours 169 | .map((d) => d.data); 170 | if (neighbours_data.length && !differentVersions(neighbours_data)) { 171 | let data = {}; 172 | const [{version}] = neighbours_data; 173 | if (version) { 174 | data.rows = [].concat.apply([], neighbours_data 175 | .map((e) => e.rows)); 176 | data.version = version; 177 | $("#legacy-history-badge").hide(); 178 | } else { 179 | // Legacy version 180 | data = [].concat.apply([], neighbours_data); 181 | $("#legacy-history-badge").show(); 182 | } 183 | const o = process_history_data(data); 184 | const {items} = o; 185 | common.symbols.history = o.symbols; 186 | 187 | if (Object.prototype.hasOwnProperty.call(common.tables, "history") && 188 | version === prevVersion) { 189 | common.tables.history.rows.load(items); 190 | } else { 191 | libft.destroyTable("history"); 192 | // Is there a way to get an event when the table is destroyed? 193 | setTimeout(() => { 194 | libft.initHistoryTable(data, items, "history", get_history_columns(data), false, 195 | () => $("#refresh, #updateHistory").removeAttr("disabled")); 196 | }, 200); 197 | } 198 | prevVersion = version; 199 | } else { 200 | libft.destroyTable("history"); 201 | } 202 | }, 203 | error: () => $("#refresh, #updateHistory").removeAttr("disabled"), 204 | errorMessage: "Cannot receive history", 205 | }); 206 | }; 207 | 208 | function initErrorsTable(rows) { 209 | common.tables.errors = FooTable.init("#errorsLog", { 210 | columns: [ 211 | {sorted: true, 212 | direction: "DESC", 213 | name: "ts", 214 | title: "Time", 215 | style: {width: 300, maxWidth: 300}, 216 | sortValue: function (val) { return Number(val.options.sortValue); }}, 217 | {name: "type", 218 | title: "Worker type", 219 | breakpoints: "xs sm", 220 | style: {width: 150, maxWidth: 150}}, 221 | {name: "pid", 222 | title: "PID", 223 | breakpoints: "xs sm", 224 | style: {width: 110, maxWidth: 110}}, 225 | {name: "module", title: "Module"}, 226 | {name: "id", title: "Internal ID"}, 227 | {name: "message", title: "Message", breakpoints: "xs sm"}, 228 | ], 229 | rows: rows, 230 | paging: { 231 | enabled: true, 232 | limit: 5, 233 | size: common.page_size.errors 234 | }, 235 | filtering: { 236 | enabled: true, 237 | position: "left", 238 | connectors: false 239 | }, 240 | sorting: { 241 | enabled: true 242 | } 243 | }); 244 | } 245 | 246 | ui.getErrors = function () { 247 | if (common.read_only) return; 248 | 249 | common.query("errors", { 250 | success: function (data) { 251 | const neighbours_data = data 252 | .filter((d) => d.status) // filter out unavailable neighbours 253 | .map((d) => d.data); 254 | const rows = [].concat.apply([], neighbours_data); 255 | $.each(rows, (i, item) => { 256 | item.ts = { 257 | value: libft.unix_time_format(item.ts), 258 | options: { 259 | sortValue: item.ts 260 | } 261 | }; 262 | }); 263 | if (Object.prototype.hasOwnProperty.call(common.tables, "errors")) { 264 | common.tables.errors.rows.load(rows); 265 | } else { 266 | initErrorsTable(rows); 267 | } 268 | } 269 | }); 270 | 271 | $("#updateErrors").off("click"); 272 | $("#updateErrors").on("click", (e) => { 273 | e.preventDefault(); 274 | ui.getErrors(); 275 | }); 276 | }; 277 | 278 | 279 | libft.set_page_size("history", $("#history_page_size").val()); 280 | libft.bindHistoryTableEventHandlers("history", 8); 281 | 282 | $("#updateHistory").off("click"); 283 | $("#updateHistory").on("click", (e) => { 284 | e.preventDefault(); 285 | ui.getHistory(); 286 | }); 287 | 288 | // @reset history log 289 | $("#resetHistory").off("click"); 290 | $("#resetHistory").on("click", (e) => { 291 | e.preventDefault(); 292 | if (!confirm("Are you sure you want to reset history log?")) { // eslint-disable-line no-alert 293 | return; 294 | } 295 | libft.destroyTable("history"); 296 | libft.destroyTable("errors"); 297 | 298 | common.query("historyreset", { 299 | success: function () { 300 | ui.getHistory(); 301 | ui.getErrors(); 302 | }, 303 | errorMessage: "Cannot reset history log" 304 | }); 305 | }); 306 | 307 | return ui; 308 | }); 309 | -------------------------------------------------------------------------------- /www/js/app/selectors.js: -------------------------------------------------------------------------------- 1 | define(["jquery", "app/common"], 2 | ($, common) => { 3 | "use strict"; 4 | const ui = {}; 5 | 6 | function enable_disable_check_btn() { 7 | $("#selectorsChkMsgBtn").prop("disabled", ( 8 | $.trim($("#selectorsMsgArea").val()).length === 0 || 9 | !$("#selectorsSelArea").hasClass("is-valid") 10 | )); 11 | } 12 | 13 | function checkMsg(data) { 14 | const selector = $("#selectorsSelArea").val(); 15 | common.query("plugins/selectors/check_message?selector=" + encodeURIComponent(selector), { 16 | data: data, 17 | method: "POST", 18 | success: function (neighbours_status) { 19 | const json = neighbours_status[0].data; 20 | if (json.success) { 21 | common.alertMessage("alert-success", "Message successfully processed"); 22 | $("#selectorsResArea") 23 | .val(Object.prototype.hasOwnProperty.call(json, "data") ? json.data.toString() : ""); 24 | } else { 25 | common.alertMessage("alert-error", "Unexpected error processing message"); 26 | } 27 | }, 28 | server: common.getServer() 29 | }); 30 | } 31 | 32 | function checkSelectors() { 33 | function toggle_form_group_class(remove, add) { 34 | $("#selectorsSelArea").removeClass("is-" + remove).addClass("is-" + add); 35 | enable_disable_check_btn(); 36 | } 37 | const selector = $("#selectorsSelArea").val(); 38 | if (selector.length && !common.read_only) { 39 | common.query("plugins/selectors/check_selector?selector=" + encodeURIComponent(selector), { 40 | method: "GET", 41 | success: function (json) { 42 | if (json[0].data.success) { 43 | toggle_form_group_class("invalid", "valid"); 44 | } else { 45 | toggle_form_group_class("valid", "invalid"); 46 | } 47 | }, 48 | server: common.getServer() 49 | }); 50 | } else { 51 | $("#selectorsSelArea").removeClass("is-valid is-invalid"); 52 | enable_disable_check_btn(); 53 | } 54 | } 55 | 56 | function buildLists() { 57 | function build_table_from_json(json, table_id) { 58 | Object.keys(json).forEach((key) => { 59 | const td = $(""); 60 | const tr = $("") 61 | .append(td.clone().html("" + key + "")) 62 | .append(td.clone().html(json[key].description)); 63 | $(table_id + " tbody").append(tr); 64 | }); 65 | } 66 | 67 | function getList(list) { 68 | common.query("plugins/selectors/list_" + list, { 69 | method: "GET", 70 | success: function (neighbours_status) { 71 | const json = neighbours_status[0].data; 72 | build_table_from_json(json, "#selectorsTable-" + list); 73 | }, 74 | server: common.getServer() 75 | }); 76 | } 77 | 78 | getList("extractors"); 79 | getList("transforms"); 80 | } 81 | 82 | ui.displayUI = function () { 83 | if (!common.read_only && 84 | !$("#selectorsTable-extractors>tbody>tr").length && 85 | !$("#selectorsTable-transforms>tbody>tr").length) buildLists(); 86 | if (!$("#selectorsSelArea").is(".is-valid, .is-invalid")) checkSelectors(); 87 | }; 88 | 89 | 90 | function toggleSidebar(side) { 91 | $("#sidebar-" + side).toggleClass("collapsed"); 92 | let contentClass = "col-lg-6"; 93 | const openSidebarsCount = $("#sidebar-left").hasClass("collapsed") + 94 | $("#sidebar-right").hasClass("collapsed"); 95 | switch (openSidebarsCount) { 96 | case 1: 97 | contentClass = "col-lg-9"; 98 | break; 99 | case 2: 100 | contentClass = "col-lg-12"; 101 | break; 102 | default: 103 | } 104 | $("#content").removeClass("col-lg-12 col-lg-9 col-lg-6") 105 | .addClass(contentClass); 106 | } 107 | $("#sidebar-tab-left>a").click(() => { 108 | toggleSidebar("left"); 109 | return false; 110 | }); 111 | $("#sidebar-tab-right>a").click(() => { 112 | toggleSidebar("right"); 113 | return false; 114 | }); 115 | 116 | $("#selectorsMsgClean").on("click", () => { 117 | $("#selectorsChkMsgBtn").attr("disabled", true); 118 | $("#selectorsMsgArea").val(""); 119 | return false; 120 | }); 121 | $("#selectorsClean").on("click", () => { 122 | $("#selectorsSelArea").val(""); 123 | checkSelectors(); 124 | return false; 125 | }); 126 | $("#selectorsChkMsgBtn").on("click", () => { 127 | $("#selectorsResArea").val(""); 128 | checkMsg($("#selectorsMsgArea").val()); 129 | return false; 130 | }); 131 | 132 | $("#selectorsMsgArea").on("input", () => { 133 | enable_disable_check_btn(); 134 | }); 135 | $("#selectorsSelArea").on("input", () => { 136 | checkSelectors(); 137 | }); 138 | 139 | return ui; 140 | }); 141 | -------------------------------------------------------------------------------- /www/js/app/symbols.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (C) 2017 Vsevolod Stakhov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | /* global FooTable */ 26 | 27 | define(["jquery", "app/common", "footable"], 28 | ($, common) => { 29 | "use strict"; 30 | const ui = {}; 31 | let altered = {}; 32 | 33 | function clear_altered() { 34 | $("#save-alert").addClass("d-none"); 35 | altered = {}; 36 | } 37 | 38 | function saveSymbols(server) { 39 | $("#save-alert button").attr("disabled", true); 40 | 41 | const values = []; 42 | Object.entries(altered).forEach(([key, value]) => values.push({name: key, value: value})); 43 | 44 | common.query("./savesymbols", { 45 | success: function () { 46 | clear_altered(); 47 | common.alertMessage("alert-modal alert-success", "Symbols successfully saved"); 48 | }, 49 | complete: () => $("#save-alert button").removeAttr("disabled"), 50 | errorMessage: "Save symbols error", 51 | method: "POST", 52 | params: { 53 | data: JSON.stringify(values), 54 | dataType: "json", 55 | }, 56 | server: server 57 | }); 58 | } 59 | 60 | function process_symbols_data(data) { 61 | const items = []; 62 | const lookup = {}; 63 | const freqs = []; 64 | const distinct_groups = []; 65 | 66 | data.forEach((group) => { 67 | group.rules.forEach((item) => { 68 | const formatter = new Intl.NumberFormat("en", { 69 | minimumFractionDigits: 2, 70 | maximumFractionDigits: 6, 71 | useGrouping: false 72 | }); 73 | item.group = group.group; 74 | let label_class = ""; 75 | if (item.weight < 0) { 76 | label_class = "scorebar-ham"; 77 | } else if (item.weight > 0) { 78 | label_class = "scorebar-spam"; 79 | } 80 | item.weight = ''; 83 | if (!item.time) { 84 | item.time = 0; 85 | } 86 | item.time = Number(item.time).toFixed(2) + "s"; 87 | if (!item.frequency) { 88 | item.frequency = 0; 89 | } 90 | freqs.push(item.frequency); 91 | item.frequency = Number(item.frequency).toFixed(2); 92 | if (!(item.group in lookup)) { 93 | lookup[item.group] = 1; 94 | distinct_groups.push(item.group); 95 | } 96 | items.push(item); 97 | }); 98 | }); 99 | 100 | // For better mean calculations 101 | const avg_freq = freqs 102 | .sort((a, b) => Number(a) < Number(b)) 103 | .reduce((f1, acc) => f1 + acc) / (freqs.length !== 0 ? freqs.length : 1.0); 104 | let mult = 1.0; 105 | let exp = 0.0; 106 | 107 | if (avg_freq > 0.0) { 108 | while (mult * avg_freq < 1.0) { 109 | mult *= 10; 110 | exp++; 111 | } 112 | } 113 | $.each(items, (i, item) => { 114 | item.frequency = Number(item.frequency) * mult; 115 | 116 | if (exp > 0) { 117 | item.frequency = item.frequency.toFixed(2) + "e-" + exp; 118 | } else { 119 | item.frequency = item.frequency.toFixed(2); 120 | } 121 | }); 122 | return [items, distinct_groups]; 123 | } 124 | // @get symbols into modal form 125 | ui.getSymbols = function () { 126 | $("#refresh, #updateSymbols").attr("disabled", true); 127 | clear_altered(); 128 | common.query("symbols", { 129 | success: function (json) { 130 | const [{data}] = json; 131 | const items = process_symbols_data(data); 132 | 133 | /* eslint-disable consistent-this, no-underscore-dangle, one-var-declaration-per-line */ 134 | FooTable.groupFilter = FooTable.Filtering.extend({ 135 | construct: function (instance) { 136 | this._super(instance); 137 | [,this.groups] = items; 138 | this.def = "Any group"; 139 | this.$group = null; 140 | }, 141 | $create: function () { 142 | this._super(); 143 | const self = this; 144 | const $form_grp = $("
", { 145 | class: "form-group" 146 | }).append($("