├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets └── swagger.png ├── cmd ├── clamd-api │ └── main.go └── clamd-ctl │ └── main.go ├── configs ├── clamav-api.yaml └── clamav-cli.yaml ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── examples └── simple │ └── main.go ├── go.mod ├── go.sum ├── internal ├── api │ ├── clamd.go │ ├── config.go │ └── run.go └── cmd │ ├── command.go │ ├── contscan.go │ ├── instream.go │ ├── multiscan.go │ ├── ping.go │ ├── reload.go │ ├── root.go │ ├── scan.go │ ├── shutdown.go │ ├── stats.go │ ├── version.go │ └── version_commands.go └── pkg ├── clamav └── clamd.go └── cli ├── clamd.go ├── network.go └── params.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | bin/ 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20 AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN GOOS=linux GOARCH=amd64 go build -o clamd-api ./cmd/clamd-api/main.go 8 | 9 | FROM alpine:latest 10 | 11 | WORKDIR /app 12 | 13 | COPY --from=builder /app/clamd-api . 14 | COPY ./configs/clamav-api.yaml ./configs/ 15 | 16 | # 暴露应用运行端口(根据实际情况设置) 17 | EXPOSE 8080 18 | 19 | CMD ["./clamd-api", "-f", "./configs/clamav-api.yaml"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: fmt 2 | fmt: ## Run go fmt against code. 3 | go fmt ./... 4 | 5 | .PHONY: run 6 | run: ## Run a controller from your host. 7 | go run ./cmd/clamd-api/main.go 8 | 9 | # 定义应用名称和默认版本号 10 | APP1 = clamd-ctl 11 | APP2 = clamd-api 12 | DEFAULT_VERSION = v0.3.0 13 | 14 | # 获取Git描述的版本号,如果未定义则使用默认值 15 | # VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo $(DEFAULT_VERSION)) 16 | VERSION ?= $(shell echo $(DEFAULT_VERSION)) 17 | # 定义目标平台和架构 18 | PLATFORMS = darwin-amd64 darwin-arm64 linux-amd64 linux-arm64 windows-amd64 windows-arm64 19 | 20 | # 输出目录 21 | OUTPUT_DIR = bin 22 | 23 | # 默认目标 24 | .PHONY: build-all build-ctl build-api clean 25 | 26 | # 构建所有目标 27 | build-all: build-ctl build-api 28 | 29 | # 构建cland-ctl 30 | build-ctl: $(foreach platform,$(PLATFORMS),$(OUTPUT_DIR)/$(APP1)-$(VERSION).$(platform)) 31 | 32 | $(OUTPUT_DIR)/$(APP1)-$(VERSION).%: GOOS = $(word 1, $(subst -, ,$*)) 33 | $(OUTPUT_DIR)/$(APP1)-$(VERSION).%: GOARCH = $(word 2, $(subst -, ,$*)) 34 | $(OUTPUT_DIR)/$(APP1)-$(VERSION).%: 35 | @mkdir -p $(OUTPUT_DIR) 36 | GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $@ cmd/$(APP1)/main.go 37 | @if [ "$(GOOS)" = "windows" ]; then mv $@ $@.exe; fi 38 | 39 | # 构建cland-api 40 | build-api: $(foreach platform,$(PLATFORMS),$(OUTPUT_DIR)/$(APP2)-$(VERSION).$(platform)) 41 | 42 | $(OUTPUT_DIR)/$(APP2)-$(VERSION).%: GOOS = $(word 1, $(subst -, ,$*)) 43 | $(OUTPUT_DIR)/$(APP2)-$(VERSION).%: GOARCH = $(word 2, $(subst -, ,$*)) 44 | $(OUTPUT_DIR)/$(APP2)-$(VERSION).%: 45 | @mkdir -p $(OUTPUT_DIR) 46 | GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $@ cmd/$(APP2)/main.go 47 | @if [ "$(GOOS)" = "windows" ]; then mv $@ $@.exe; fi 48 | 49 | # 清理目标文件 50 | clean: 51 | rm -rf $(OUTPUT_DIR) 52 | 53 | CONTAINER_TOOL ?= docker 54 | IMG ?= clamd-api:latest 55 | 56 | .PHONY: docker-build 57 | docker-build: ## Build docker image with the manager. 58 | $(CONTAINER_TOOL) build -t ${IMG} . 59 | 60 | .PHONY: docker-push 61 | docker-push: ## Push docker image with the manager. 62 | $(CONTAINER_TOOL) push ${IMG} 63 | 64 | .PHONY: swag-init 65 | swag-init: 66 | swag init -g cmd/clamd-api/main.go 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-clamav 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/hq0101/go-clamav.svg)](https://pkg.go.dev/github.com/hq0101/go-clamav) 4 | 5 | go-clamav是一个用Go语言编写的库和工具集,旨在与ClamAV防病毒软件进行无缝集成。它提供了一个简洁易用的REST API,用于调用ClamAV扫描、获取统计信息、版本信息等功能。同时,go-clamav 还包含一个命令行工具clamd-ctl,用于与 Clamd守护进程通信并执行各种操作。 6 | 7 | # 核心功能 8 | 9 | go-clamav 提供了丰富的核心功能来与 ClamAV 服务器进行交互。这些功能包括但不限于: 10 | 11 | - Ping: 检查 ClamAV 服务器是否可达。 12 | - Version: 获取 ClamAV 版本信息。 13 | - VersionCommands: 获取 ClamAV 版本和支持的命令列表。 14 | - Stats: 获取 ClamAV 的统计信息。 15 | - Scan: 扫描单个文件或目录。 16 | - ContScan: 连续扫描文件或目录。 17 | - MultiScan: 并行扫描多个文件或目录。 18 | - AllMatchScan: 扫描文件或目录,并在找到匹配时继续扫描。 19 | - Instream: 通过流方式扫描数据。 20 | - Reload: 重新加载病毒数据库。 21 | - Shutdown: 关闭 ClamAV 服务器。 22 | 23 | # 快速开始 24 | 25 | - 安装clamav 26 | 27 | 使用 APT(Debian/Ubuntu) 28 | 29 | ```bash 30 | sudo apt-get update 31 | sudo apt-get install -y clamav clamav-daemon 32 | ``` 33 | 34 | ## 安装go-clamav 35 | 首先,你需要安装Go编译器并设置好开发环境。然后你可以通过以下命令获取 go-clamav 36 | 37 | ```bash 38 | go get -u github.com/hq0101/go-clamav 39 | ``` 40 | 41 | ## 使用示例 42 | 43 | 下面是一些简单的示例,演示如何使用 go-clamav 库连接到 ClamAV 服务器并执行基本的操作。 44 | 45 | ### TCP 连接 Clamd 46 | 47 | ```go 48 | package main 49 | 50 | import ( 51 | "fmt" 52 | "github.com/hq0101/go-clamav/pkg/clamav" 53 | "time" 54 | ) 55 | 56 | func main() { 57 | client := clamav.NewClamClient("tcp", "192.168.127.131:3310", 10*time.Second, 30*time.Second) 58 | 59 | response, err := client.Ping() 60 | if err != nil { 61 | fmt.Println("Error:", err) 62 | return 63 | } 64 | 65 | fmt.Println("Response from ClamAV:", response) 66 | 67 | version, err := client.Version() 68 | if err != nil { 69 | fmt.Println("Error:", err) 70 | return 71 | } 72 | fmt.Println("ClamAV Version:", version) 73 | } 74 | 75 | ``` 76 | 编译并运行你的程序 77 | 78 | ```bash 79 | go run main.go 80 | ``` 81 | 82 | ### unix 套接字连接Clamd 83 | 84 | ```go 85 | package main 86 | 87 | import ( 88 | "fmt" 89 | "github.com/hq0101/go-clamav/pkg/clamav" 90 | "time" 91 | ) 92 | 93 | 94 | func main() { 95 | client := clamav.NewClamClient("unix", "/var/run/clamav/clamd.ctl", 10*time.Second, 30*time.Second) 96 | 97 | // Ping ClamAV server 98 | response, err := client.Ping() 99 | if err != nil { 100 | fmt.Println("Error:", err) 101 | return 102 | } 103 | fmt.Println("Ping Response from ClamAV:", response) 104 | 105 | // Get ClamAV version 106 | version, err := client.Version() 107 | if err != nil { 108 | fmt.Println("Error:", err) 109 | return 110 | } 111 | fmt.Println("ClamAV Version:", version) 112 | } 113 | 114 | ``` 115 | 116 | 编译并运行你的程序 117 | 118 | ```bash 119 | go run main.go 120 | ``` 121 | 122 | 123 | # API文档 124 | 125 | go-clamav 使用 Swagger 进行了文档化,方便开发者查阅和测试。 126 | 127 | - 启动clamd-api 服务 128 | 129 | ```shell 130 | make run 131 | ``` 132 | 133 | 浏览器打开swagger http://127.0.0.1:8080/swagger/index.html 134 | 135 | ![swagger](assets/swagger.png) 136 | 137 | # clamd-ctl 使用手册 138 | 139 | clamd-ctl是一个Go语言编写的命令行工具,用于与ClamAV服务器进行通信,并执行各种操作。 140 | 141 | ## 使用方法 142 | 143 | - 帮助手册 144 | 145 | ```bash 146 | clamd-ctl-v0.1.0.linux-amd64 -h 147 | ``` 148 | 149 | - 使用TCP连接clamd 150 | 151 | ```bash 152 | clamd-ctl-v0.1.0.linux-amd64 ping -a 192.168.127.131:3310 -n tcp 153 | ``` 154 | 155 | - 使用UNIX套接字连接clamd 156 | 157 | ```bash 158 | clamd-ctl-v0.1.0.linux-amd64 ping -a /var/run/clamav/clamd.ctl -n unix 159 | ``` 160 | 161 | - 使用配置文件 162 | 163 | 创建一个配置文件,例如 clamd-ctl.yaml 164 | 165 | ```yaml 166 | clamd_network_type: "tcp" # 连接类型,可以是 "tcp" 或 "unix" 167 | clamd_address: "127.0.0.1:3310" # clamd 服务器地址(对于 TCP)或套接字路径(对于 UNIX)/var/run/clamav/clamd.ctl 168 | clamd_conn_timeout: "10s" # 连接超时时间 169 | clamd_read_timeout: "30s" # 读取超时时间 170 | ``` 171 | 172 | 使用此配置文件运行命令 173 | ```bash 174 | clamd-ctl-v0.1.0.linux-amd64 ping -f ./configs/clamav-cli.yaml 175 | ``` 176 | 177 | - 获取 ClamAV 版本 178 | 179 | ```bash 180 | clamd-ctl-v0.1.0.linux-amd64 version -a 192.168.127.131:3310 -n tcp 181 | clamd-ctl-v0.1.0.linux-amd64 versioncommands -a 192.168.127.131:3310 -n tcp 182 | ``` 183 | 184 | - 获取 ClamAV 统计信息 185 | 186 | ```bash 187 | clamd-ctl-v0.1.0.linux-amd64 stats -a 192.168.127.131:3310 -n tcp 188 | ``` 189 | 190 | - 扫描单个文件或目录 191 | 192 | ```bash 193 | clamd-ctl-v0.1.0.linux-amd64 /path/to/file -a 192.168.127.131:3310 -n tcp 194 | ``` 195 | 196 | - 连续扫描文件或目录 197 | 198 | ```bash 199 | clamd-ctl-v0.1.0.linux-amd64 contscan /path/to/file -a 192.168.127.131:3310 -n tcp 200 | ``` 201 | 202 | - 并行扫描多个文件或目录 203 | 204 | ```bash 205 | clamd-ctl-v0.1.0.linux-amd64 multiscan /path/to/file -a 192.168.127.131:3310 -n tcp 206 | ``` 207 | 208 | - 扫描文件或目录并在找到匹配时继续扫描 209 | 210 | ```bash 211 | clamd-ctl-v0.1.0.linux-amd64 allmatchscan /path/to/file -a 192.168.127.131:3310 -n tcp 212 | ``` 213 | 214 | - 重新加载病毒数据库 215 | 216 | ```bash 217 | clamd-ctl-v0.1.0.linux-amd64 reload -a 192.168.127.131:3310 -n tcp 218 | ``` 219 | 220 | - 关闭 ClamAV 服务器 221 | 222 | ```bash 223 | clamd-ctl-v0.1.0.linux-amd64 shutdown -a 192.168.127.131:3310 -n tcp 224 | ``` 225 | 226 | - 流方式扫描数据 227 | 228 | ```bash 229 | clamd-ctl-v0.1.0.linux-amd64 instream "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" -a 192.168.127.131:3310 -n tcp 230 | ``` 231 | 232 | 通过以上命令,你可以方便地与 ClamAV 服务器进行互动,并利用其强大的防病毒功能来保护你的系统。 233 | 希望这些内容能帮助你快速上手 go-clamav 项目。如果有任何问题或建议,欢迎提交issue或 pull request! 234 | -------------------------------------------------------------------------------- /assets/swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hq0101/go-clamav/fc190c7258970248f738b83395eae3103bdae677/assets/swagger.png -------------------------------------------------------------------------------- /cmd/clamd-api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hq0101/go-clamav/internal/api" 5 | "github.com/spf13/cobra" 6 | "log" 7 | ) 8 | 9 | func main() { 10 | var cfgFilePath string 11 | var rootCmd = &cobra.Command{ 12 | Use: "clamd", 13 | Short: "clamd", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | if _err := api.Init(cfgFilePath); _err != nil { 16 | log.Fatalln(_err) 17 | } 18 | if err := api.Run(); err != nil { 19 | log.Fatalln(err) 20 | } 21 | }, 22 | } 23 | rootCmd.PersistentFlags().StringVarP(&cfgFilePath, "config", "c", "./configs/clamav-api.yaml", "config file path") 24 | if err := rootCmd.Execute(); err != nil { 25 | log.Fatalln(err) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cmd/clamd-ctl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hq0101/go-clamav/internal/cmd" 6 | "github.com/hq0101/go-clamav/pkg/cli" 7 | "github.com/spf13/cobra" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | func searchSubCommand(rootCmd *cobra.Command, args []string) *cobra.Command { 13 | for _, _cmd := range rootCmd.Commands() { 14 | if len(args) < 1 { 15 | return nil 16 | } 17 | if strings.HasPrefix(args[0], _cmd.Name()) { 18 | return _cmd 19 | } 20 | } 21 | return nil 22 | } 23 | 24 | func main() { 25 | p := &cli.ClamdParams{} 26 | rootCmd := cmd.Root(p) 27 | ag := os.Args[1:] 28 | subcommand := searchSubCommand(rootCmd, ag) 29 | if subcommand == nil && len(ag) > 0 { 30 | _ = rootCmd.Help() 31 | os.Exit(1) 32 | } 33 | if err := rootCmd.Execute(); err != nil { 34 | fmt.Println(err) 35 | os.Exit(1) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /configs/clamav-api.yaml: -------------------------------------------------------------------------------- 1 | clamd_network_type: "tcp" 2 | clamd_address: "192.168.127.131:3310" 3 | clamd_conn_timeout: "10s" 4 | clamd_read_timeout: "180s" 5 | listen: "0.0.0.0:8080" -------------------------------------------------------------------------------- /configs/clamav-cli.yaml: -------------------------------------------------------------------------------- 1 | clamd_network_type: "tcp" 2 | clamd_address: "192.168.127.131:3310" 3 | clamd_conn_timeout: "10s" 4 | clamd_read_timeout: "30s" 5 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs Code generated by swaggo/swag. DO NOT EDIT 2 | package docs 3 | 4 | import "github.com/swaggo/swag" 5 | 6 | const docTemplate = `{ 7 | "schemes": {{ marshal .Schemes }}, 8 | "swagger": "2.0", 9 | "info": { 10 | "description": "{{escape .Description}}", 11 | "title": "{{.Title}}", 12 | "contact": {}, 13 | "version": "{{.Version}}" 14 | }, 15 | "host": "{{.Host}}", 16 | "basePath": "{{.BasePath}}", 17 | "paths": { 18 | "/allmatchscan": { 19 | "get": { 20 | "tags": [ 21 | "ClamAV" 22 | ], 23 | "summary": "AllMatchScan scan a file or directory", 24 | "parameters": [ 25 | { 26 | "type": "string", 27 | "description": "File path to scan", 28 | "name": "file", 29 | "in": "query", 30 | "required": true 31 | } 32 | ], 33 | "responses": { 34 | "200": { 35 | "description": "OK", 36 | "schema": { 37 | "type": "array", 38 | "items": { 39 | "$ref": "#/definitions/cli.ScanResult" 40 | } 41 | } 42 | }, 43 | "400": { 44 | "description": "Bad Request", 45 | "schema": { 46 | "type": "string" 47 | } 48 | }, 49 | "500": { 50 | "description": "Internal Server Error", 51 | "schema": { 52 | "type": "string" 53 | } 54 | } 55 | } 56 | } 57 | }, 58 | "/contscan": { 59 | "get": { 60 | "tags": [ 61 | "ClamAV" 62 | ], 63 | "summary": "Continuously scan a file or directory", 64 | "parameters": [ 65 | { 66 | "type": "string", 67 | "description": "File path to scan", 68 | "name": "file", 69 | "in": "query", 70 | "required": true 71 | } 72 | ], 73 | "responses": { 74 | "200": { 75 | "description": "OK", 76 | "schema": { 77 | "type": "array", 78 | "items": { 79 | "$ref": "#/definitions/cli.ScanResult" 80 | } 81 | } 82 | }, 83 | "400": { 84 | "description": "Bad Request", 85 | "schema": { 86 | "type": "string" 87 | } 88 | }, 89 | "500": { 90 | "description": "Internal Server Error", 91 | "schema": { 92 | "type": "string" 93 | } 94 | } 95 | } 96 | } 97 | }, 98 | "/instream": { 99 | "post": { 100 | "consumes": [ 101 | "multipart/form-data" 102 | ], 103 | "produces": [ 104 | "application/json" 105 | ], 106 | "tags": [ 107 | "ClamAV" 108 | ], 109 | "summary": "Scan data stream", 110 | "parameters": [ 111 | { 112 | "type": "file", 113 | "description": "File to upload", 114 | "name": "file", 115 | "in": "formData", 116 | "required": true 117 | } 118 | ], 119 | "responses": { 120 | "200": { 121 | "description": "OK", 122 | "schema": { 123 | "type": "array", 124 | "items": { 125 | "$ref": "#/definitions/cli.ScanResult" 126 | } 127 | } 128 | }, 129 | "400": { 130 | "description": "Bad Request", 131 | "schema": { 132 | "type": "string" 133 | } 134 | }, 135 | "500": { 136 | "description": "Internal Server Error", 137 | "schema": { 138 | "type": "string" 139 | } 140 | } 141 | } 142 | } 143 | }, 144 | "/multiscan": { 145 | "get": { 146 | "tags": [ 147 | "ClamAV" 148 | ], 149 | "summary": "Multithreaded scan of a file or directory", 150 | "parameters": [ 151 | { 152 | "type": "string", 153 | "description": "File path to scan", 154 | "name": "file", 155 | "in": "query", 156 | "required": true 157 | } 158 | ], 159 | "responses": { 160 | "200": { 161 | "description": "OK", 162 | "schema": { 163 | "type": "array", 164 | "items": { 165 | "$ref": "#/definitions/cli.ScanResult" 166 | } 167 | } 168 | }, 169 | "400": { 170 | "description": "Bad Request", 171 | "schema": { 172 | "type": "string" 173 | } 174 | }, 175 | "500": { 176 | "description": "Internal Server Error", 177 | "schema": { 178 | "type": "string" 179 | } 180 | } 181 | } 182 | } 183 | }, 184 | "/ping": { 185 | "get": { 186 | "tags": [ 187 | "ClamAV" 188 | ], 189 | "summary": "Ping Check the server's state. It should reply with \"PONG\".", 190 | "responses": { 191 | "200": { 192 | "description": "OK", 193 | "schema": { 194 | "type": "string" 195 | } 196 | }, 197 | "500": { 198 | "description": "Internal Server Error", 199 | "schema": { 200 | "type": "string" 201 | } 202 | } 203 | } 204 | } 205 | }, 206 | "/reload": { 207 | "post": { 208 | "tags": [ 209 | "ClamAV" 210 | ], 211 | "summary": "Reload the virus database", 212 | "responses": { 213 | "200": { 214 | "description": "OK", 215 | "schema": { 216 | "type": "string" 217 | } 218 | }, 219 | "500": { 220 | "description": "Internal Server Error", 221 | "schema": { 222 | "type": "string" 223 | } 224 | } 225 | } 226 | } 227 | }, 228 | "/scan": { 229 | "get": { 230 | "tags": [ 231 | "ClamAV" 232 | ], 233 | "summary": "Scan a file or directory", 234 | "parameters": [ 235 | { 236 | "type": "string", 237 | "description": "File path to scan", 238 | "name": "file", 239 | "in": "query", 240 | "required": true 241 | } 242 | ], 243 | "responses": { 244 | "200": { 245 | "description": "OK", 246 | "schema": { 247 | "type": "array", 248 | "items": { 249 | "$ref": "#/definitions/cli.ScanResult" 250 | } 251 | } 252 | }, 253 | "400": { 254 | "description": "Bad Request", 255 | "schema": { 256 | "type": "string" 257 | } 258 | }, 259 | "500": { 260 | "description": "Internal Server Error", 261 | "schema": { 262 | "type": "string" 263 | } 264 | } 265 | } 266 | } 267 | }, 268 | "/shutdown": { 269 | "post": { 270 | "tags": [ 271 | "ClamAV" 272 | ], 273 | "summary": "Shut down the ClamAV server", 274 | "responses": { 275 | "200": { 276 | "description": "OK", 277 | "schema": { 278 | "type": "string" 279 | } 280 | }, 281 | "500": { 282 | "description": "Internal Server Error", 283 | "schema": { 284 | "type": "string" 285 | } 286 | } 287 | } 288 | } 289 | }, 290 | "/stats": { 291 | "get": { 292 | "tags": [ 293 | "ClamAV" 294 | ], 295 | "summary": "Get ClamAV stats", 296 | "responses": { 297 | "200": { 298 | "description": "OK", 299 | "schema": { 300 | "$ref": "#/definitions/cli.ClamdStats" 301 | } 302 | }, 303 | "500": { 304 | "description": "Internal Server Error", 305 | "schema": { 306 | "type": "string" 307 | } 308 | } 309 | } 310 | } 311 | }, 312 | "/version": { 313 | "get": { 314 | "tags": [ 315 | "ClamAV" 316 | ], 317 | "summary": "Get the ClamAV version", 318 | "responses": { 319 | "200": { 320 | "description": "ClamAV 0.103.11/27353/Wed", 321 | "schema": { 322 | "type": "string" 323 | } 324 | }, 325 | "500": { 326 | "description": "Internal Server Error", 327 | "schema": { 328 | "type": "string" 329 | } 330 | } 331 | } 332 | } 333 | }, 334 | "/versioncommands": { 335 | "get": { 336 | "tags": [ 337 | "ClamAV" 338 | ], 339 | "summary": "Get the ClamAV version", 340 | "responses": { 341 | "200": { 342 | "description": "ClamAV 0.103.11/27353/Wed", 343 | "schema": { 344 | "type": "string" 345 | } 346 | }, 347 | "500": { 348 | "description": "Internal Server Error", 349 | "schema": { 350 | "type": "string" 351 | } 352 | } 353 | } 354 | } 355 | } 356 | }, 357 | "definitions": { 358 | "cli.ClamdStats": { 359 | "type": "object", 360 | "properties": { 361 | "idleTimeout": { 362 | "type": "integer" 363 | }, 364 | "memFree": { 365 | "type": "number" 366 | }, 367 | "memHeap": { 368 | "type": "number" 369 | }, 370 | "memMmap": { 371 | "type": "number" 372 | }, 373 | "memPoolsTotal": { 374 | "type": "number" 375 | }, 376 | "memPoolsUsed": { 377 | "type": "number" 378 | }, 379 | "memReleasable": { 380 | "type": "number" 381 | }, 382 | "memUsed": { 383 | "type": "number" 384 | }, 385 | "pools": { 386 | "type": "integer" 387 | }, 388 | "queueItems": { 389 | "type": "integer" 390 | }, 391 | "threadsIdle": { 392 | "type": "integer" 393 | }, 394 | "threadsLive": { 395 | "type": "integer" 396 | }, 397 | "threadsMax": { 398 | "type": "integer" 399 | } 400 | } 401 | }, 402 | "cli.ScanResult": { 403 | "type": "object", 404 | "properties": { 405 | "path": { 406 | "type": "string" 407 | }, 408 | "status": { 409 | "type": "string" 410 | }, 411 | "virus": { 412 | "type": "string" 413 | } 414 | } 415 | } 416 | } 417 | }` 418 | 419 | // SwaggerInfo holds exported Swagger Info so clients can modify it 420 | var SwaggerInfo = &swag.Spec{ 421 | Version: "", 422 | Host: "", 423 | BasePath: "", 424 | Schemes: []string{}, 425 | Title: "", 426 | Description: "", 427 | InfoInstanceName: "swagger", 428 | SwaggerTemplate: docTemplate, 429 | LeftDelim: "{{", 430 | RightDelim: "}}", 431 | } 432 | 433 | func init() { 434 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 435 | } 436 | -------------------------------------------------------------------------------- /docs/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "contact": {} 5 | }, 6 | "paths": { 7 | "/allmatchscan": { 8 | "get": { 9 | "tags": [ 10 | "ClamAV" 11 | ], 12 | "summary": "AllMatchScan scan a file or directory", 13 | "parameters": [ 14 | { 15 | "type": "string", 16 | "description": "File path to scan", 17 | "name": "file", 18 | "in": "query", 19 | "required": true 20 | } 21 | ], 22 | "responses": { 23 | "200": { 24 | "description": "OK", 25 | "schema": { 26 | "type": "array", 27 | "items": { 28 | "$ref": "#/definitions/cli.ScanResult" 29 | } 30 | } 31 | }, 32 | "400": { 33 | "description": "Bad Request", 34 | "schema": { 35 | "type": "string" 36 | } 37 | }, 38 | "500": { 39 | "description": "Internal Server Error", 40 | "schema": { 41 | "type": "string" 42 | } 43 | } 44 | } 45 | } 46 | }, 47 | "/contscan": { 48 | "get": { 49 | "tags": [ 50 | "ClamAV" 51 | ], 52 | "summary": "Continuously scan a file or directory", 53 | "parameters": [ 54 | { 55 | "type": "string", 56 | "description": "File path to scan", 57 | "name": "file", 58 | "in": "query", 59 | "required": true 60 | } 61 | ], 62 | "responses": { 63 | "200": { 64 | "description": "OK", 65 | "schema": { 66 | "type": "array", 67 | "items": { 68 | "$ref": "#/definitions/cli.ScanResult" 69 | } 70 | } 71 | }, 72 | "400": { 73 | "description": "Bad Request", 74 | "schema": { 75 | "type": "string" 76 | } 77 | }, 78 | "500": { 79 | "description": "Internal Server Error", 80 | "schema": { 81 | "type": "string" 82 | } 83 | } 84 | } 85 | } 86 | }, 87 | "/instream": { 88 | "post": { 89 | "consumes": [ 90 | "multipart/form-data" 91 | ], 92 | "produces": [ 93 | "application/json" 94 | ], 95 | "tags": [ 96 | "ClamAV" 97 | ], 98 | "summary": "Scan data stream", 99 | "parameters": [ 100 | { 101 | "type": "file", 102 | "description": "File to upload", 103 | "name": "file", 104 | "in": "formData", 105 | "required": true 106 | } 107 | ], 108 | "responses": { 109 | "200": { 110 | "description": "OK", 111 | "schema": { 112 | "type": "array", 113 | "items": { 114 | "$ref": "#/definitions/cli.ScanResult" 115 | } 116 | } 117 | }, 118 | "400": { 119 | "description": "Bad Request", 120 | "schema": { 121 | "type": "string" 122 | } 123 | }, 124 | "500": { 125 | "description": "Internal Server Error", 126 | "schema": { 127 | "type": "string" 128 | } 129 | } 130 | } 131 | } 132 | }, 133 | "/multiscan": { 134 | "get": { 135 | "tags": [ 136 | "ClamAV" 137 | ], 138 | "summary": "Multithreaded scan of a file or directory", 139 | "parameters": [ 140 | { 141 | "type": "string", 142 | "description": "File path to scan", 143 | "name": "file", 144 | "in": "query", 145 | "required": true 146 | } 147 | ], 148 | "responses": { 149 | "200": { 150 | "description": "OK", 151 | "schema": { 152 | "type": "array", 153 | "items": { 154 | "$ref": "#/definitions/cli.ScanResult" 155 | } 156 | } 157 | }, 158 | "400": { 159 | "description": "Bad Request", 160 | "schema": { 161 | "type": "string" 162 | } 163 | }, 164 | "500": { 165 | "description": "Internal Server Error", 166 | "schema": { 167 | "type": "string" 168 | } 169 | } 170 | } 171 | } 172 | }, 173 | "/ping": { 174 | "get": { 175 | "tags": [ 176 | "ClamAV" 177 | ], 178 | "summary": "Ping Check the server's state. It should reply with \"PONG\".", 179 | "responses": { 180 | "200": { 181 | "description": "OK", 182 | "schema": { 183 | "type": "string" 184 | } 185 | }, 186 | "500": { 187 | "description": "Internal Server Error", 188 | "schema": { 189 | "type": "string" 190 | } 191 | } 192 | } 193 | } 194 | }, 195 | "/reload": { 196 | "post": { 197 | "tags": [ 198 | "ClamAV" 199 | ], 200 | "summary": "Reload the virus database", 201 | "responses": { 202 | "200": { 203 | "description": "OK", 204 | "schema": { 205 | "type": "string" 206 | } 207 | }, 208 | "500": { 209 | "description": "Internal Server Error", 210 | "schema": { 211 | "type": "string" 212 | } 213 | } 214 | } 215 | } 216 | }, 217 | "/scan": { 218 | "get": { 219 | "tags": [ 220 | "ClamAV" 221 | ], 222 | "summary": "Scan a file or directory", 223 | "parameters": [ 224 | { 225 | "type": "string", 226 | "description": "File path to scan", 227 | "name": "file", 228 | "in": "query", 229 | "required": true 230 | } 231 | ], 232 | "responses": { 233 | "200": { 234 | "description": "OK", 235 | "schema": { 236 | "type": "array", 237 | "items": { 238 | "$ref": "#/definitions/cli.ScanResult" 239 | } 240 | } 241 | }, 242 | "400": { 243 | "description": "Bad Request", 244 | "schema": { 245 | "type": "string" 246 | } 247 | }, 248 | "500": { 249 | "description": "Internal Server Error", 250 | "schema": { 251 | "type": "string" 252 | } 253 | } 254 | } 255 | } 256 | }, 257 | "/shutdown": { 258 | "post": { 259 | "tags": [ 260 | "ClamAV" 261 | ], 262 | "summary": "Shut down the ClamAV server", 263 | "responses": { 264 | "200": { 265 | "description": "OK", 266 | "schema": { 267 | "type": "string" 268 | } 269 | }, 270 | "500": { 271 | "description": "Internal Server Error", 272 | "schema": { 273 | "type": "string" 274 | } 275 | } 276 | } 277 | } 278 | }, 279 | "/stats": { 280 | "get": { 281 | "tags": [ 282 | "ClamAV" 283 | ], 284 | "summary": "Get ClamAV stats", 285 | "responses": { 286 | "200": { 287 | "description": "OK", 288 | "schema": { 289 | "$ref": "#/definitions/cli.ClamdStats" 290 | } 291 | }, 292 | "500": { 293 | "description": "Internal Server Error", 294 | "schema": { 295 | "type": "string" 296 | } 297 | } 298 | } 299 | } 300 | }, 301 | "/version": { 302 | "get": { 303 | "tags": [ 304 | "ClamAV" 305 | ], 306 | "summary": "Get the ClamAV version", 307 | "responses": { 308 | "200": { 309 | "description": "ClamAV 0.103.11/27353/Wed", 310 | "schema": { 311 | "type": "string" 312 | } 313 | }, 314 | "500": { 315 | "description": "Internal Server Error", 316 | "schema": { 317 | "type": "string" 318 | } 319 | } 320 | } 321 | } 322 | }, 323 | "/versioncommands": { 324 | "get": { 325 | "tags": [ 326 | "ClamAV" 327 | ], 328 | "summary": "Get the ClamAV version", 329 | "responses": { 330 | "200": { 331 | "description": "ClamAV 0.103.11/27353/Wed", 332 | "schema": { 333 | "type": "string" 334 | } 335 | }, 336 | "500": { 337 | "description": "Internal Server Error", 338 | "schema": { 339 | "type": "string" 340 | } 341 | } 342 | } 343 | } 344 | } 345 | }, 346 | "definitions": { 347 | "cli.ClamdStats": { 348 | "type": "object", 349 | "properties": { 350 | "idleTimeout": { 351 | "type": "integer" 352 | }, 353 | "memFree": { 354 | "type": "number" 355 | }, 356 | "memHeap": { 357 | "type": "number" 358 | }, 359 | "memMmap": { 360 | "type": "number" 361 | }, 362 | "memPoolsTotal": { 363 | "type": "number" 364 | }, 365 | "memPoolsUsed": { 366 | "type": "number" 367 | }, 368 | "memReleasable": { 369 | "type": "number" 370 | }, 371 | "memUsed": { 372 | "type": "number" 373 | }, 374 | "pools": { 375 | "type": "integer" 376 | }, 377 | "queueItems": { 378 | "type": "integer" 379 | }, 380 | "threadsIdle": { 381 | "type": "integer" 382 | }, 383 | "threadsLive": { 384 | "type": "integer" 385 | }, 386 | "threadsMax": { 387 | "type": "integer" 388 | } 389 | } 390 | }, 391 | "cli.ScanResult": { 392 | "type": "object", 393 | "properties": { 394 | "path": { 395 | "type": "string" 396 | }, 397 | "status": { 398 | "type": "string" 399 | }, 400 | "virus": { 401 | "type": "string" 402 | } 403 | } 404 | } 405 | } 406 | } -------------------------------------------------------------------------------- /docs/swagger.yaml: -------------------------------------------------------------------------------- 1 | definitions: 2 | cli.ClamdStats: 3 | properties: 4 | idleTimeout: 5 | type: integer 6 | memFree: 7 | type: number 8 | memHeap: 9 | type: number 10 | memMmap: 11 | type: number 12 | memPoolsTotal: 13 | type: number 14 | memPoolsUsed: 15 | type: number 16 | memReleasable: 17 | type: number 18 | memUsed: 19 | type: number 20 | pools: 21 | type: integer 22 | queueItems: 23 | type: integer 24 | threadsIdle: 25 | type: integer 26 | threadsLive: 27 | type: integer 28 | threadsMax: 29 | type: integer 30 | type: object 31 | cli.ScanResult: 32 | properties: 33 | path: 34 | type: string 35 | status: 36 | type: string 37 | virus: 38 | type: string 39 | type: object 40 | info: 41 | contact: {} 42 | paths: 43 | /allmatchscan: 44 | get: 45 | parameters: 46 | - description: File path to scan 47 | in: query 48 | name: file 49 | required: true 50 | type: string 51 | responses: 52 | "200": 53 | description: OK 54 | schema: 55 | items: 56 | $ref: '#/definitions/cli.ScanResult' 57 | type: array 58 | "400": 59 | description: Bad Request 60 | schema: 61 | type: string 62 | "500": 63 | description: Internal Server Error 64 | schema: 65 | type: string 66 | summary: AllMatchScan scan a file or directory 67 | tags: 68 | - ClamAV 69 | /contscan: 70 | get: 71 | parameters: 72 | - description: File path to scan 73 | in: query 74 | name: file 75 | required: true 76 | type: string 77 | responses: 78 | "200": 79 | description: OK 80 | schema: 81 | items: 82 | $ref: '#/definitions/cli.ScanResult' 83 | type: array 84 | "400": 85 | description: Bad Request 86 | schema: 87 | type: string 88 | "500": 89 | description: Internal Server Error 90 | schema: 91 | type: string 92 | summary: Continuously scan a file or directory 93 | tags: 94 | - ClamAV 95 | /instream: 96 | post: 97 | consumes: 98 | - multipart/form-data 99 | parameters: 100 | - description: File to upload 101 | in: formData 102 | name: file 103 | required: true 104 | type: file 105 | produces: 106 | - application/json 107 | responses: 108 | "200": 109 | description: OK 110 | schema: 111 | items: 112 | $ref: '#/definitions/cli.ScanResult' 113 | type: array 114 | "400": 115 | description: Bad Request 116 | schema: 117 | type: string 118 | "500": 119 | description: Internal Server Error 120 | schema: 121 | type: string 122 | summary: Scan data stream 123 | tags: 124 | - ClamAV 125 | /multiscan: 126 | get: 127 | parameters: 128 | - description: File path to scan 129 | in: query 130 | name: file 131 | required: true 132 | type: string 133 | responses: 134 | "200": 135 | description: OK 136 | schema: 137 | items: 138 | $ref: '#/definitions/cli.ScanResult' 139 | type: array 140 | "400": 141 | description: Bad Request 142 | schema: 143 | type: string 144 | "500": 145 | description: Internal Server Error 146 | schema: 147 | type: string 148 | summary: Multithreaded scan of a file or directory 149 | tags: 150 | - ClamAV 151 | /ping: 152 | get: 153 | responses: 154 | "200": 155 | description: OK 156 | schema: 157 | type: string 158 | "500": 159 | description: Internal Server Error 160 | schema: 161 | type: string 162 | summary: Ping Check the server's state. It should reply with "PONG". 163 | tags: 164 | - ClamAV 165 | /reload: 166 | post: 167 | responses: 168 | "200": 169 | description: OK 170 | schema: 171 | type: string 172 | "500": 173 | description: Internal Server Error 174 | schema: 175 | type: string 176 | summary: Reload the virus database 177 | tags: 178 | - ClamAV 179 | /scan: 180 | get: 181 | parameters: 182 | - description: File path to scan 183 | in: query 184 | name: file 185 | required: true 186 | type: string 187 | responses: 188 | "200": 189 | description: OK 190 | schema: 191 | items: 192 | $ref: '#/definitions/cli.ScanResult' 193 | type: array 194 | "400": 195 | description: Bad Request 196 | schema: 197 | type: string 198 | "500": 199 | description: Internal Server Error 200 | schema: 201 | type: string 202 | summary: Scan a file or directory 203 | tags: 204 | - ClamAV 205 | /shutdown: 206 | post: 207 | responses: 208 | "200": 209 | description: OK 210 | schema: 211 | type: string 212 | "500": 213 | description: Internal Server Error 214 | schema: 215 | type: string 216 | summary: Shut down the ClamAV server 217 | tags: 218 | - ClamAV 219 | /stats: 220 | get: 221 | responses: 222 | "200": 223 | description: OK 224 | schema: 225 | $ref: '#/definitions/cli.ClamdStats' 226 | "500": 227 | description: Internal Server Error 228 | schema: 229 | type: string 230 | summary: Get ClamAV stats 231 | tags: 232 | - ClamAV 233 | /version: 234 | get: 235 | responses: 236 | "200": 237 | description: ClamAV 0.103.11/27353/Wed 238 | schema: 239 | type: string 240 | "500": 241 | description: Internal Server Error 242 | schema: 243 | type: string 244 | summary: Get the ClamAV version 245 | tags: 246 | - ClamAV 247 | /versioncommands: 248 | get: 249 | responses: 250 | "200": 251 | description: ClamAV 0.103.11/27353/Wed 252 | schema: 253 | type: string 254 | "500": 255 | description: Internal Server Error 256 | schema: 257 | type: string 258 | summary: Get the ClamAV version 259 | tags: 260 | - ClamAV 261 | swagger: "2.0" 262 | -------------------------------------------------------------------------------- /examples/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/hq0101/go-clamav/pkg/clamav" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | client := clamav.NewClamClient("tcp", "192.168.127.131:3310", 10*time.Second, 30*time.Second) 12 | 13 | response, err := client.Ping() 14 | if err != nil { 15 | fmt.Println("Error:", err) 16 | return 17 | } 18 | 19 | fmt.Println("Response from ClamAV:", response) 20 | 21 | version, err := client.Version() 22 | if err != nil { 23 | fmt.Println("Error:", err) 24 | return 25 | } 26 | fmt.Println("ClamAV Version:", version) 27 | 28 | stats, err := client.Stats() 29 | if err != nil { 30 | fmt.Println("Error:", err) 31 | return 32 | } 33 | jsonResults, err := json.MarshalIndent(stats, "", " ") 34 | if err != nil { 35 | fmt.Printf("Failed to marshal results to JSON: %v\n", err) 36 | return 37 | } 38 | 39 | fmt.Println("ClamAV Stats:", string(jsonResults)) 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hq0101/go-clamav 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.7.0 7 | github.com/gin-gonic/gin v1.10.0 8 | github.com/prometheus/client_golang v1.19.1 9 | github.com/spf13/cobra v1.8.1 10 | github.com/spf13/viper v1.19.0 11 | github.com/swaggo/files v1.0.1 12 | github.com/swaggo/gin-swagger v1.6.0 13 | github.com/swaggo/swag v1.16.3 14 | ) 15 | 16 | require ( 17 | github.com/KyleBanks/depth v1.2.1 // indirect 18 | github.com/beorn7/perks v1.0.1 // indirect 19 | github.com/bytedance/sonic v1.11.6 // indirect 20 | github.com/bytedance/sonic/loader v0.1.1 // indirect 21 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 22 | github.com/cloudwego/base64x v0.1.4 // indirect 23 | github.com/cloudwego/iasm v0.2.0 // indirect 24 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 25 | github.com/gin-contrib/sse v0.1.0 // indirect 26 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 27 | github.com/go-openapi/jsonreference v0.21.0 // indirect 28 | github.com/go-openapi/spec v0.21.0 // indirect 29 | github.com/go-openapi/swag v0.23.0 // indirect 30 | github.com/go-playground/locales v0.14.1 // indirect 31 | github.com/go-playground/universal-translator v0.18.1 // indirect 32 | github.com/go-playground/validator/v10 v10.20.0 // indirect 33 | github.com/goccy/go-json v0.10.2 // indirect 34 | github.com/hashicorp/hcl v1.0.0 // indirect 35 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 36 | github.com/josharian/intern v1.0.0 // indirect 37 | github.com/json-iterator/go v1.1.12 // indirect 38 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 39 | github.com/leodido/go-urn v1.4.0 // indirect 40 | github.com/magiconair/properties v1.8.7 // indirect 41 | github.com/mailru/easyjson v0.7.7 // indirect 42 | github.com/mattn/go-isatty v0.0.20 // indirect 43 | github.com/mitchellh/mapstructure v1.5.0 // indirect 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 45 | github.com/modern-go/reflect2 v1.0.2 // indirect 46 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 47 | github.com/prometheus/client_model v0.5.0 // indirect 48 | github.com/prometheus/common v0.48.0 // indirect 49 | github.com/prometheus/procfs v0.12.0 // indirect 50 | github.com/sagikazarmark/locafero v0.4.0 // indirect 51 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 52 | github.com/sourcegraph/conc v0.3.0 // indirect 53 | github.com/spf13/afero v1.11.0 // indirect 54 | github.com/spf13/cast v1.6.0 // indirect 55 | github.com/spf13/pflag v1.0.5 // indirect 56 | github.com/subosito/gotenv v1.6.0 // indirect 57 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 58 | github.com/ugorji/go/codec v1.2.12 // indirect 59 | go.uber.org/atomic v1.9.0 // indirect 60 | go.uber.org/multierr v1.9.0 // indirect 61 | golang.org/x/arch v0.8.0 // indirect 62 | golang.org/x/crypto v0.25.0 // indirect 63 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 64 | golang.org/x/net v0.27.0 // indirect 65 | golang.org/x/sys v0.22.0 // indirect 66 | golang.org/x/text v0.16.0 // indirect 67 | golang.org/x/tools v0.23.0 // indirect 68 | google.golang.org/protobuf v1.34.1 // indirect 69 | gopkg.in/ini.v1 v1.67.0 // indirect 70 | gopkg.in/yaml.v3 v3.0.1 // indirect 71 | ) 72 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 2 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 6 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 7 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 8 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 9 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 10 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 12 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 13 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 14 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 15 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 16 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 19 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 21 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 22 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 23 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 24 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 25 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 26 | github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= 27 | github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= 28 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 29 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 30 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 31 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 32 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 33 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 34 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 35 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 36 | github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= 37 | github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= 38 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 39 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 40 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 41 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 42 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 43 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 44 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 45 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 46 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 47 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 48 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 49 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 50 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 51 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 52 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 53 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 54 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 55 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 56 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 57 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 58 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 59 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 60 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 61 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 62 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 63 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 64 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 65 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 66 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 67 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 68 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 69 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 70 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 71 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 72 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 73 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 74 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 75 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 76 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 77 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 78 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 79 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 80 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 81 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 82 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 83 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 84 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 85 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 86 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 87 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 88 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 89 | github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= 90 | github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 91 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 92 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 93 | github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= 94 | github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= 95 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 96 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 97 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 98 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 99 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 100 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 101 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 102 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 103 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 104 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 105 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 106 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 107 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 108 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 109 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 110 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 111 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 112 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 113 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 114 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 115 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 116 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 117 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 118 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 119 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 120 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 121 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 122 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 123 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 124 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 125 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 126 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 127 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 128 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 129 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 130 | github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= 131 | github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= 132 | github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= 133 | github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= 134 | github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= 135 | github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= 136 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 137 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 138 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 139 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 140 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 141 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 142 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 143 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 144 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 145 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 146 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 147 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 148 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 149 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 150 | golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= 151 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 152 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 153 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 154 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 155 | golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= 156 | golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 157 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 158 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 159 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 160 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 161 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 162 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 163 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 164 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 165 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 166 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 167 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 168 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 169 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 170 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 171 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 172 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 173 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 174 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 175 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 176 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 177 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 178 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 179 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 180 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 181 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 182 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 183 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 184 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 185 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 186 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 187 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 188 | golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= 189 | golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 190 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 191 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 192 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 193 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 194 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 195 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 196 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 197 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 198 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 199 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 200 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 201 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 202 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 203 | -------------------------------------------------------------------------------- /internal/api/clamd.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/hq0101/go-clamav/pkg/clamav" 6 | "net/http" 7 | ) 8 | 9 | type ClamdController struct { 10 | } 11 | 12 | func NewClamd() *ClamdController { 13 | return &ClamdController{} 14 | } 15 | 16 | // Ping godoc 17 | // @Summary Ping Check the server's state. It should reply with "PONG". 18 | // @Tags ClamAV 19 | // @Success 200 {string} string PONG 20 | // @Failure 500 {string} string 21 | // @Router /ping [get] 22 | func (c *ClamdController) Ping(ctx *gin.Context) { 23 | clamClient := clamav.NewClamClient(GetCfg().ClamdNetworkType, GetCfg().ClamdAddress, GetCfg().ClamdConnTimeout, GetCfg().ClamdReadTimeout) 24 | content, err := clamClient.Ping() 25 | if err != nil { 26 | ctx.JSON(http.StatusInternalServerError, err.Error()) 27 | return 28 | } 29 | ctx.JSON(http.StatusOK, content) 30 | } 31 | 32 | // Version godoc 33 | // @Summary Get the ClamAV version 34 | // @Tags ClamAV 35 | // @Success 200 {string} string "ClamAV 0.103.11/27353/Wed" 36 | // @Failure 500 {string} string 37 | // @Router /version [get] 38 | func (c *ClamdController) Version(ctx *gin.Context) { 39 | clamClient := clamav.NewClamClient(GetCfg().ClamdNetworkType, GetCfg().ClamdAddress, GetCfg().ClamdConnTimeout, GetCfg().ClamdReadTimeout) 40 | content, err := clamClient.Version() 41 | if err != nil { 42 | ctx.JSON(http.StatusInternalServerError, err.Error()) 43 | return 44 | } 45 | ctx.JSON(http.StatusOK, content) 46 | } 47 | 48 | // VersionCommands godoc 49 | // @Summary Get the ClamAV version 50 | // @Tags ClamAV 51 | // @Success 200 {string} string "ClamAV 0.103.11/27353/Wed" 52 | // @Failure 500 {string} string 53 | // @Router /versioncommands [get] 54 | func (c *ClamdController) VersionCommands(ctx *gin.Context) { 55 | clamClient := clamav.NewClamClient(GetCfg().ClamdNetworkType, GetCfg().ClamdAddress, GetCfg().ClamdConnTimeout, GetCfg().ClamdReadTimeout) 56 | content, err := clamClient.VersionCommands() 57 | if err != nil { 58 | ctx.JSON(http.StatusInternalServerError, err.Error()) 59 | return 60 | } 61 | ctx.JSON(http.StatusOK, content) 62 | } 63 | 64 | // Stats godoc 65 | // @Summary Get ClamAV stats 66 | // @Tags ClamAV 67 | // @Success 200 {object} cli.ClamdStats 68 | // @Failure 500 {string} string 69 | // @Router /stats [get] 70 | func (c *ClamdController) Stats(ctx *gin.Context) { 71 | clamClient := clamav.NewClamClient(GetCfg().ClamdNetworkType, GetCfg().ClamdAddress, GetCfg().ClamdConnTimeout, GetCfg().ClamdReadTimeout) 72 | content, err := clamClient.Stats() 73 | if err != nil { 74 | ctx.JSON(http.StatusInternalServerError, err.Error()) 75 | return 76 | } 77 | ctx.JSON(http.StatusOK, content) 78 | } 79 | 80 | // Reload godoc 81 | // @Summary Reload the virus database 82 | // @Tags ClamAV 83 | // @Success 200 {string} string RELOADING 84 | // @Failure 500 {string} string 85 | // @Router /reload [post] 86 | func (c *ClamdController) Reload(ctx *gin.Context) { 87 | clamClient := clamav.NewClamClient(GetCfg().ClamdNetworkType, GetCfg().ClamdAddress, GetCfg().ClamdConnTimeout, GetCfg().ClamdReadTimeout) 88 | content, err := clamClient.Reload() 89 | if err != nil { 90 | ctx.JSON(http.StatusInternalServerError, err.Error()) 91 | return 92 | } 93 | ctx.JSON(http.StatusOK, content) 94 | } 95 | 96 | // Shutdown godoc 97 | // @Summary Shut down the ClamAV server 98 | // @Tags ClamAV 99 | // @Success 200 {string} string "OK" 100 | // @Failure 500 {string} string 101 | // @Router /shutdown [post] 102 | func (c *ClamdController) Shutdown(ctx *gin.Context) { 103 | clamClient := clamav.NewClamClient(GetCfg().ClamdNetworkType, GetCfg().ClamdAddress, GetCfg().ClamdConnTimeout, GetCfg().ClamdReadTimeout) 104 | content, err := clamClient.Shutdown() 105 | if err != nil { 106 | ctx.JSON(http.StatusInternalServerError, err.Error()) 107 | return 108 | } 109 | ctx.JSON(http.StatusOK, content) 110 | } 111 | 112 | // Scan godoc 113 | // @Summary Scan a file or directory 114 | // @Tags ClamAV 115 | // @Param file query string true "File path to scan" 116 | // @Success 200 {array} cli.ScanResult 117 | // @Failure 400 {string} string 118 | // @Failure 500 {string} string 119 | // @Router /scan [get] 120 | func (c *ClamdController) Scan(ctx *gin.Context) { 121 | filePath := ctx.Query("file") 122 | if filePath == "" { 123 | ctx.JSON(http.StatusBadRequest, "File path is required") 124 | return 125 | } 126 | clamClient := clamav.NewClamClient(GetCfg().ClamdNetworkType, GetCfg().ClamdAddress, GetCfg().ClamdConnTimeout, GetCfg().ClamdReadTimeout) 127 | content, err := clamClient.Scan(filePath) 128 | if err != nil { 129 | ctx.JSON(http.StatusInternalServerError, err.Error()) 130 | return 131 | } 132 | 133 | ctx.JSON(http.StatusOK, content) 134 | } 135 | 136 | // Contscan godoc 137 | // @Summary Continuously scan a file or directory 138 | // @Tags ClamAV 139 | // @Param file query string true "File path to scan" 140 | // @Success 200 {array} cli.ScanResult 141 | // @Failure 400 {string} string 142 | // @Failure 500 {string} string 143 | // @Router /contscan [get] 144 | func (c *ClamdController) Contscan(ctx *gin.Context) { 145 | filePath := ctx.Query("file") 146 | if filePath == "" { 147 | ctx.JSON(http.StatusBadRequest, "File path is required") 148 | return 149 | } 150 | clamClient := clamav.NewClamClient(GetCfg().ClamdNetworkType, GetCfg().ClamdAddress, GetCfg().ClamdConnTimeout, GetCfg().ClamdReadTimeout) 151 | content, err := clamClient.ContScan(filePath) 152 | if err != nil { 153 | ctx.JSON(http.StatusInternalServerError, err.Error()) 154 | return 155 | } 156 | ctx.JSON(http.StatusOK, content) 157 | } 158 | 159 | // MultiScan godoc 160 | // @Summary Multithreaded scan of a file or directory 161 | // @Tags ClamAV 162 | // @Param file query string true "File path to scan" 163 | // @Success 200 {array} cli.ScanResult 164 | // @Failure 400 {string} string 165 | // @Failure 500 {string} string 166 | // @Router /multiscan [get] 167 | func (c *ClamdController) MultiScan(ctx *gin.Context) { 168 | filePath := ctx.Query("file") 169 | if filePath == "" { 170 | ctx.JSON(http.StatusBadRequest, "File path is required") 171 | return 172 | } 173 | clamClient := clamav.NewClamClient(GetCfg().ClamdNetworkType, GetCfg().ClamdAddress, GetCfg().ClamdConnTimeout, GetCfg().ClamdReadTimeout) 174 | content, err := clamClient.MultiScan(filePath) 175 | if err != nil { 176 | ctx.JSON(http.StatusInternalServerError, err.Error()) 177 | return 178 | } 179 | 180 | ctx.JSON(http.StatusOK, content) 181 | } 182 | 183 | // AllMatchScan godoc 184 | // @Summary AllMatchScan scan a file or directory 185 | // @Tags ClamAV 186 | // @Param file query string true "File path to scan" 187 | // @Success 200 {array} cli.ScanResult 188 | // @Failure 400 {string} string 189 | // @Failure 500 {string} string 190 | // @Router /allmatchscan [get] 191 | func (c *ClamdController) AllMatchScan(ctx *gin.Context) { 192 | filePath := ctx.Query("file") 193 | if filePath == "" { 194 | ctx.JSON(http.StatusBadRequest, "File path is required") 195 | return 196 | } 197 | clamClient := clamav.NewClamClient(GetCfg().ClamdNetworkType, GetCfg().ClamdAddress, GetCfg().ClamdConnTimeout, GetCfg().ClamdReadTimeout) 198 | content, err := clamClient.AllMatchScan(filePath) 199 | if err != nil { 200 | ctx.JSON(http.StatusInternalServerError, err.Error()) 201 | return 202 | } 203 | ctx.JSON(http.StatusOK, content) 204 | } 205 | 206 | // Instream godoc 207 | // @Summary Scan data stream 208 | // @Tags ClamAV 209 | // @Accept multipart/form-data 210 | // @Produce application/json 211 | // @Param file formData file true "File to upload" 212 | // @Success 200 {array} cli.ScanResult 213 | // @Failure 400 {string} string 214 | // @Failure 500 {string} string 215 | // @Router /instream [post] 216 | func (c *ClamdController) Instream(ctx *gin.Context) { 217 | data, err := ctx.GetRawData() 218 | if err != nil { 219 | ctx.JSON(http.StatusBadRequest, "Failed to read raw data") 220 | return 221 | } 222 | clamClient := clamav.NewClamClient(GetCfg().ClamdNetworkType, GetCfg().ClamdAddress, GetCfg().ClamdConnTimeout, GetCfg().ClamdReadTimeout) 223 | content, err := clamClient.Instream(data) 224 | if err != nil { 225 | ctx.JSON(http.StatusInternalServerError, err.Error()) 226 | return 227 | } 228 | ctx.JSON(http.StatusOK, content) 229 | } 230 | -------------------------------------------------------------------------------- /internal/api/config.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fsnotify/fsnotify" 6 | "github.com/spf13/viper" 7 | "time" 8 | ) 9 | 10 | var ( 11 | cfg *Config 12 | ) 13 | 14 | type Config struct { 15 | Listen string `mapstructure:"listen"` 16 | ClamdAddress string `mapstructure:"clamd_address"` 17 | ClamdNetworkType string `mapstructure:"clamd_network_type"` 18 | ClamdConnTimeout time.Duration `mapstructure:"clamd_conn_timeout"` 19 | ClamdReadTimeout time.Duration `mapstructure:"clamd_read_timeout"` 20 | } 21 | 22 | func Init(configPath string) error { 23 | viper.SetConfigFile(configPath) 24 | if _err := viper.ReadInConfig(); _err != nil { 25 | return fmt.Errorf("failed to read the configuration file: %w", _err) 26 | } 27 | if _err := viper.Unmarshal(&cfg); _err != nil { 28 | return fmt.Errorf("failed to parse the configuration file: %w", _err) 29 | } 30 | viper.OnConfigChange(func(e fsnotify.Event) { 31 | if _err := viper.Unmarshal(&cfg); _err != nil { 32 | fmt.Println("Failed to reload the configuration file:", _err) 33 | } else { 34 | fmt.Println("reload config file") 35 | } 36 | }) 37 | viper.WatchConfig() 38 | return nil 39 | } 40 | 41 | func GetCfg() *Config { 42 | return cfg 43 | } 44 | -------------------------------------------------------------------------------- /internal/api/run.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/hq0101/go-clamav/docs" 6 | "github.com/prometheus/client_golang/prometheus/promhttp" 7 | swaggerFiles "github.com/swaggo/files" 8 | ginSwagger "github.com/swaggo/gin-swagger" 9 | "net/http" 10 | ) 11 | 12 | func CORSMiddleware() gin.HandlerFunc { 13 | return func(c *gin.Context) { 14 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 15 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 16 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") 17 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") 18 | 19 | if c.Request.Method == "OPTIONS" { 20 | c.AbortWithStatus(http.StatusNoContent) 21 | return 22 | } 23 | 24 | c.Next() 25 | } 26 | } 27 | 28 | func Run() error { 29 | docs.SwaggerInfo.Title = "ClamAV API" 30 | docs.SwaggerInfo.Description = "This is a sample server for ClamAV" 31 | docs.SwaggerInfo.Version = "1.0" 32 | docs.SwaggerInfo.Host = "localhost:8080" 33 | docs.SwaggerInfo.BasePath = "/" 34 | docs.SwaggerInfo.Schemes = []string{"http", "https"} 35 | 36 | r := gin.Default() 37 | // CORS configuration 38 | r.Use(CORSMiddleware()) 39 | r.Use(gin.Recovery()) 40 | 41 | r.GET("/healthz", func(c *gin.Context) { c.Status(http.StatusOK) }) 42 | r.GET("/metrics", gin.WrapH(promhttp.Handler())) 43 | 44 | clamd := NewClamd() 45 | 46 | r.GET("/ping", clamd.Ping) 47 | r.GET("/version", clamd.Version) 48 | r.GET("/versioncommands", clamd.Version) 49 | r.GET("/stats", clamd.Stats) 50 | r.POST("/reload", clamd.Reload) 51 | r.POST("/shutdown", clamd.Shutdown) 52 | r.GET("/scan", clamd.Scan) 53 | r.GET("/contscan", clamd.Contscan) 54 | r.GET("/multiscan", clamd.MultiScan) 55 | r.GET("/allmatchscan", clamd.MultiScan) 56 | r.POST("/instream", clamd.Instream) 57 | 58 | // Use ginSwagger middleware to serve the API docs 59 | r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 60 | 61 | return r.Run(GetCfg().Listen) 62 | } 63 | -------------------------------------------------------------------------------- /internal/cmd/command.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/hq0101/go-clamav/pkg/clamav" 7 | "github.com/hq0101/go-clamav/pkg/cli" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func createClient(networkType, address string, connTimeout, readTimeout time.Duration) (*clamav.ClamClient, error) { 13 | switch networkType { 14 | case cli.TCP.String(), cli.Unix.String(): 15 | default: 16 | return nil, fmt.Errorf("invalid network type: %s", networkType) 17 | } 18 | return clamav.NewClamClient(networkType, address, connTimeout, readTimeout), nil 19 | } 20 | 21 | func handleResponse(results string, err error) { 22 | if err != nil { 23 | fmt.Printf("Command failed: %v\n", err) 24 | return 25 | } else { 26 | fmt.Println(results) 27 | } 28 | } 29 | 30 | func pretty(out string, results []cli.ScanResult, err error) { 31 | if err != nil { 32 | fmt.Printf("Command failed: %v\n", err) 33 | return 34 | } 35 | switch out { 36 | case cli.Json.String(): 37 | formatJson(results) 38 | case cli.Text.String(): 39 | formatText(results) 40 | } 41 | } 42 | 43 | func formatJson(results []cli.ScanResult) { 44 | jsonResults, err := json.MarshalIndent(results, "", " ") 45 | if err != nil { 46 | fmt.Printf("Failed to marshal results to JSON: %v\n", err) 47 | return 48 | } 49 | 50 | fmt.Println(string(jsonResults)) 51 | } 52 | 53 | func formatText(results []cli.ScanResult) { 54 | var sb strings.Builder 55 | for _, result := range results { 56 | sb.WriteString(fmt.Sprintf("%s %s %s\n", result.Path, result.Virus, result.Status)) 57 | } 58 | fmt.Println(sb.String()) 59 | } 60 | 61 | func formatPoolStats(ps *cli.ClamdStats) string { 62 | return fmt.Sprintf( 63 | ` 64 | === POOL STATISTICS === 65 | Pools: %d 66 | Primary Threads: 67 | - Live: %d 68 | - Idle: %d 69 | - Max: %d 70 | - Idle Timeout: %d 71 | Queue: %d items 72 | Memory Stats: 73 | - Heap: %.3fM 74 | - Mmap: %.3fM 75 | - Used: %.3fM 76 | - Free: %.3fM 77 | - Releasable: %.3fM 78 | - Pools Used: %.3fM 79 | - Pools Total: %.3fM 80 | `, ps.Pools, 81 | ps.ThreadsLive, ps.ThreadsIdle, ps.ThreadsMax, ps.IdleTimeout, 82 | ps.QueueItems, 83 | ps.MemHeap, ps.MemMmap, ps.MemUsed, ps.MemFree, ps.MemReleasable, ps.MemPoolsUsed, ps.MemPoolsTotal) 84 | } 85 | -------------------------------------------------------------------------------- /internal/cmd/contscan.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/hq0101/go-clamav/pkg/cli" 5 | "github.com/spf13/cobra" 6 | "log" 7 | ) 8 | 9 | func NewContScanCmd(p cli.Params) *cobra.Command { 10 | return &cobra.Command{ 11 | Use: "contscan", 12 | Short: "Continuously scan a file or directory", 13 | Args: cobra.ExactArgs(1), 14 | Run: func(cmd *cobra.Command, args []string) { 15 | client, _err := createClient(p.GetNetworkType(), p.GetAddress(), p.GetConnTimeout(), p.GetReadTimeout()) 16 | if _err != nil { 17 | log.Fatalln(_err) 18 | } 19 | response, err := client.ContScan(args[0]) 20 | pretty(p.GetOut().String(), response, err) 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/cmd/instream.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/hq0101/go-clamav/pkg/cli" 5 | "github.com/spf13/cobra" 6 | "log" 7 | ) 8 | 9 | func NewInstreamCmd(p cli.Params) *cobra.Command { 10 | return &cobra.Command{ 11 | Use: "instream", 12 | Short: "Scan data stream", 13 | Args: cobra.ExactArgs(1), 14 | Run: func(cmd *cobra.Command, args []string) { 15 | client, _err := createClient(p.GetNetworkType(), p.GetAddress(), p.GetConnTimeout(), p.GetReadTimeout()) 16 | if _err != nil { 17 | log.Fatalln(_err) 18 | } 19 | response, err := client.Instream([]byte(args[0])) 20 | pretty(p.GetOut().String(), response, err) 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/cmd/multiscan.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/hq0101/go-clamav/pkg/cli" 5 | "github.com/spf13/cobra" 6 | "log" 7 | ) 8 | 9 | func NewMultiScanCmd(p cli.Params) *cobra.Command { 10 | return &cobra.Command{ 11 | Use: "multiscan", 12 | Short: "Multithreaded scan of a file or directory", 13 | Args: cobra.ExactArgs(1), 14 | Run: func(cmd *cobra.Command, args []string) { 15 | client, _err := createClient(p.GetNetworkType(), p.GetAddress(), p.GetConnTimeout(), p.GetReadTimeout()) 16 | if _err != nil { 17 | log.Fatalln(_err) 18 | } 19 | response, err := client.MultiScan(args[0]) 20 | pretty(p.GetOut().String(), response, err) 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/cmd/ping.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/hq0101/go-clamav/pkg/cli" 5 | "github.com/spf13/cobra" 6 | "log" 7 | ) 8 | 9 | func NewPingCmd(p cli.Params) *cobra.Command { 10 | return &cobra.Command{ 11 | Use: "ping", 12 | Short: "Ping the ClamAV server", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | client, _err := createClient(p.GetNetworkType(), p.GetAddress(), p.GetConnTimeout(), p.GetReadTimeout()) 15 | if _err != nil { 16 | log.Fatalln(_err) 17 | } 18 | response, err := client.Ping() 19 | handleResponse(response, err) 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/cmd/reload.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/hq0101/go-clamav/pkg/cli" 5 | "github.com/spf13/cobra" 6 | "log" 7 | ) 8 | 9 | func NewReloadCmd(p cli.Params) *cobra.Command { 10 | return &cobra.Command{ 11 | Use: "reload", 12 | Short: "Reload the virus database", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | client, _err := createClient(p.GetNetworkType(), p.GetAddress(), p.GetConnTimeout(), p.GetReadTimeout()) 15 | if _err != nil { 16 | log.Fatalln(_err) 17 | } 18 | response, err := client.Reload() 19 | handleResponse(response, err) 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hq0101/go-clamav/pkg/cli" 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/viper" 8 | "net" 9 | "time" 10 | ) 11 | 12 | const ( 13 | address = "address" 14 | ConnTimeout = "conn_timeout" 15 | netType = "nettype" 16 | config = "config" 17 | out = "out" 18 | ReadTimeout = "read_timeout" 19 | ) 20 | 21 | func Root(p cli.Params) *cobra.Command { 22 | var rootCmd = &cobra.Command{ 23 | Use: "clamd-cli", 24 | Short: "A CLI for interacting with ClamAV", 25 | CompletionOptions: cobra.CompletionOptions{ 26 | DisableDefaultCmd: true, 27 | }, 28 | SilenceErrors: true, 29 | SilenceUsage: true, 30 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 31 | return initParams(p, cmd) 32 | }, 33 | } 34 | 35 | rootCmd.PersistentFlags().StringP(config, "f", "", "config file clamav-cli.yaml)") 36 | rootCmd.PersistentFlags().StringP(address, "a", "", "ClamAV server address /var/run/clamav/clamd.ctl or 127.0.0.1:3310") 37 | rootCmd.PersistentFlags().DurationP(ConnTimeout, "t", 10*time.Second, "Connection timeout") 38 | rootCmd.PersistentFlags().DurationP(ReadTimeout, "r", 30*time.Second, "Read timeout") 39 | rootCmd.PersistentFlags().StringP(netType, "n", "", "Network type (unix/tcp)") 40 | rootCmd.PersistentFlags().StringP(out, "o", "text", "json、text (default text)") 41 | 42 | rootCmd.AddCommand(NewPingCmd(p)) 43 | rootCmd.AddCommand(NewVersionCmd(p)) 44 | rootCmd.AddCommand(NewContScanCmd(p)) 45 | rootCmd.AddCommand(NewScanCmd(p)) 46 | rootCmd.AddCommand(NewInstreamCmd(p)) 47 | rootCmd.AddCommand(NewMultiScanCmd(p)) 48 | rootCmd.AddCommand(NewStatsCmd(p)) 49 | rootCmd.AddCommand(NewReloadCmd(p)) 50 | rootCmd.AddCommand(NewShutdownCmd(p)) 51 | rootCmd.AddCommand(NewVersionCommandsCmd(p)) 52 | return rootCmd 53 | } 54 | 55 | func initParams(p cli.Params, cmd *cobra.Command) error { 56 | cfg, err := cmd.Flags().GetString(config) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | if _err := initConfig(cfg, p); _err != nil { 62 | return _err 63 | } 64 | if _err := initializeFlags(cmd, p); _err != nil { 65 | return _err 66 | } 67 | if _err := validateConfig(p); _err != nil { 68 | return _err 69 | } 70 | return nil 71 | } 72 | 73 | func initConfig(cfg string, p cli.Params) error { 74 | if cfg == "" { 75 | return nil 76 | } 77 | viper.SetConfigFile(cfg) 78 | if err := viper.ReadInConfig(); err != nil { 79 | return fmt.Errorf("error reading config file: %v", err) 80 | } 81 | 82 | viper.AutomaticEnv() 83 | if viper.IsSet("clamd_address") { 84 | p.SetAddress(viper.GetString("clamd_address")) 85 | } 86 | if viper.IsSet("clamd_conn_timeout") { 87 | p.SetConnTimeout(viper.GetDuration("clamd_conn_timeout")) 88 | } 89 | if viper.IsSet("clamd_read_timeout") { 90 | p.SetReadTimeout(viper.GetDuration("clamd_read_timeout")) 91 | } 92 | if viper.IsSet("clamd_network_type") { 93 | p.SetNetworkType(viper.GetString("clamd_network_type")) 94 | } 95 | if viper.IsSet("clamd_out") { 96 | p.SetOut(cli.OutType(viper.GetString("clamd_out"))) 97 | } 98 | return nil 99 | } 100 | 101 | func initializeFlags(rootCmd *cobra.Command, p cli.Params) error { 102 | addr, err := rootCmd.Flags().GetString(address) 103 | if err != nil { 104 | return err 105 | } 106 | if addr != "" && p.GetAddress() == "" { 107 | p.SetAddress(addr) 108 | } 109 | connTimeout, err := rootCmd.Flags().GetDuration(ConnTimeout) 110 | if err != nil { 111 | return err 112 | } 113 | if connTimeout != 0 && p.GetConnTimeout() == 0 { 114 | p.SetConnTimeout(connTimeout) 115 | } 116 | readTimeout, err := rootCmd.Flags().GetDuration(ReadTimeout) 117 | if err != nil { 118 | return err 119 | } 120 | if readTimeout != 0 && p.GetReadTimeout() == 0 { 121 | p.SetReadTimeout(readTimeout) 122 | } 123 | 124 | netWorkType, err := rootCmd.Flags().GetString(netType) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | if netWorkType != "" && p.GetNetworkType() == "" { 130 | p.SetNetworkType(netWorkType) 131 | } 132 | 133 | ot, err := rootCmd.Flags().GetString(out) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | if ot != "" && p.GetOut().String() == "" { 139 | p.SetOut(cli.OutType(ot)) 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func validateConfig(p cli.Params) error { 146 | if p.GetAddress() == "" { 147 | return fmt.Errorf("ClamAV server address cannot be empty") 148 | } 149 | if p.GetNetworkType() == "" { 150 | return fmt.Errorf("Network type cannot be empty") 151 | } 152 | 153 | switch p.GetNetworkType() { 154 | case cli.Unix.String(), cli.TCP.String(): 155 | if p.GetNetworkType() == cli.TCP.String() { 156 | if err := validateTCPAddress(p.GetAddress()); err != nil { 157 | return fmt.Errorf("invalid TCP address format: %v", err) 158 | } 159 | } 160 | default: 161 | return fmt.Errorf("Invalid network type: %s. Must be 'unix' or 'tcp'", p.GetNetworkType()) 162 | } 163 | return nil 164 | } 165 | 166 | func validateTCPAddress(addr string) error { 167 | host, port, err := net.SplitHostPort(addr) 168 | if err != nil { 169 | return err 170 | } 171 | if ip := net.ParseIP(host); ip == nil { 172 | return fmt.Errorf("invalid IP address") 173 | } 174 | if portNum, err := net.LookupPort(cli.TCP.String(), port); err != nil || portNum <= 0 { 175 | return fmt.Errorf("invalid port number") 176 | } 177 | return nil 178 | } 179 | -------------------------------------------------------------------------------- /internal/cmd/scan.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/hq0101/go-clamav/pkg/cli" 5 | "github.com/spf13/cobra" 6 | "log" 7 | ) 8 | 9 | func NewScanCmd(p cli.Params) *cobra.Command { 10 | return &cobra.Command{ 11 | Use: "scan", 12 | Short: "Scan a file or directory", 13 | Args: cobra.ExactArgs(1), 14 | Run: func(cmd *cobra.Command, args []string) { 15 | client, _err := createClient(p.GetNetworkType(), p.GetAddress(), p.GetConnTimeout(), p.GetReadTimeout()) 16 | if _err != nil { 17 | log.Fatalln(_err) 18 | } 19 | response, err := client.Scan(args[0]) 20 | pretty(p.GetOut().String(), response, err) 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/cmd/shutdown.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/hq0101/go-clamav/pkg/cli" 5 | "github.com/spf13/cobra" 6 | "log" 7 | ) 8 | 9 | func NewShutdownCmd(p cli.Params) *cobra.Command { 10 | return &cobra.Command{ 11 | Use: "shutdown", 12 | Short: "Shutdown the ClamAV server", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | client, _err := createClient(p.GetNetworkType(), p.GetAddress(), p.GetConnTimeout(), p.GetReadTimeout()) 15 | if _err != nil { 16 | log.Fatalln(_err) 17 | } 18 | response, err := client.Shutdown() 19 | handleResponse(response, err) 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/cmd/stats.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hq0101/go-clamav/pkg/cli" 6 | "github.com/spf13/cobra" 7 | "log" 8 | ) 9 | 10 | func NewStatsCmd(p cli.Params) *cobra.Command { 11 | return &cobra.Command{ 12 | Use: "stats", 13 | Short: "Get ClamAV stats", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | client, _err := createClient(p.GetNetworkType(), p.GetAddress(), p.GetConnTimeout(), p.GetReadTimeout()) 16 | if _err != nil { 17 | log.Fatalln(_err) 18 | } 19 | response, err := client.Stats() 20 | if err != nil { 21 | log.Fatalln(err) 22 | } 23 | fmt.Println(formatPoolStats(response)) 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/hq0101/go-clamav/pkg/cli" 5 | "github.com/spf13/cobra" 6 | "log" 7 | ) 8 | 9 | func NewVersionCmd(p cli.Params) *cobra.Command { 10 | return &cobra.Command{ 11 | Use: "version", 12 | Short: "Get the ClamAV version", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | client, _err := createClient(p.GetNetworkType(), p.GetAddress(), p.GetConnTimeout(), p.GetReadTimeout()) 15 | if _err != nil { 16 | log.Fatalln(_err) 17 | } 18 | response, err := client.Version() 19 | handleResponse(response, err) 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/cmd/version_commands.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/hq0101/go-clamav/pkg/cli" 5 | "github.com/spf13/cobra" 6 | "log" 7 | ) 8 | 9 | func NewVersionCommandsCmd(p cli.Params) *cobra.Command { 10 | return &cobra.Command{ 11 | Use: "versioncommands", 12 | Short: "Get the ClamAV version", 13 | Run: func(cmd *cobra.Command, args []string) { 14 | client, _err := createClient(p.GetNetworkType(), p.GetAddress(), p.GetConnTimeout(), p.GetReadTimeout()) 15 | if _err != nil { 16 | log.Fatalln(_err) 17 | } 18 | response, err := client.VersionCommands() 19 | handleResponse(response, err) 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/clamav/clamd.go: -------------------------------------------------------------------------------- 1 | package clamav 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "github.com/hq0101/go-clamav/pkg/cli" 10 | "io" 11 | "net" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | type ClamClient struct { 17 | ConnectionType string `json:"connection_type"` 18 | Address string `json:"address"` 19 | ConnTimeout time.Duration 20 | ReadTimeout time.Duration 21 | } 22 | 23 | const EICAR = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*" 24 | 25 | func NewClamClient(connectionType string, address string, connTimeout, readTimeout time.Duration) *ClamClient { 26 | return &ClamClient{ 27 | ConnectionType: connectionType, 28 | Address: address, 29 | ConnTimeout: connTimeout, 30 | ReadTimeout: readTimeout, 31 | } 32 | } 33 | 34 | func (c *ClamClient) Dial() (net.Conn, error) { 35 | conn, err := net.DialTimeout(c.ConnectionType, c.Address, c.ConnTimeout) 36 | if err != nil { 37 | return nil, fmt.Errorf("failed to connect to clamd: %v", err) 38 | } 39 | 40 | return conn, nil 41 | } 42 | 43 | func (c *ClamClient) sendCommand(command string) (string, error) { 44 | conn, err := c.Dial() 45 | if err != nil { 46 | return "", err 47 | } 48 | defer conn.Close() 49 | 50 | if _, _err := fmt.Fprintf(conn, "%s", command); err != nil { 51 | return "", _err 52 | } 53 | if _err := conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)); _err != nil { 54 | return "", fmt.Errorf("error setting read deadline: %v", _err) 55 | } 56 | return readResponse(conn) 57 | } 58 | 59 | func readResponse(conn net.Conn) (string, error) { 60 | var response strings.Builder 61 | reader := bufio.NewReader(conn) 62 | for { 63 | line, err := reader.ReadBytes('\n') 64 | if len(line) > 0 { 65 | response.Write(line) 66 | } 67 | if err != nil { 68 | if errors.Is(err, io.EOF) { 69 | break 70 | } 71 | return "", fmt.Errorf("failed to read response: %v", err) 72 | } 73 | } 74 | 75 | return strings.TrimSpace(response.String()), nil 76 | } 77 | 78 | // Ping Check the server's state. It should reply with "PONG". 79 | func (c *ClamClient) Ping() (string, error) { 80 | return c.sendCommand("PING") 81 | } 82 | 83 | // Version Print program and database versions. 84 | func (c *ClamClient) Version() (string, error) { 85 | return c.sendCommand("VERSION") 86 | } 87 | 88 | // Stats It is mandatory to newline terminate this command, or prefix with n or z, it is recommended to only use the z prefix. 89 | // Replies with statistics about the scan queue, contents of scan queue, and memory usage. The exact reply format is subject to change in future releases. 90 | func (c *ClamClient) Stats() (*cli.ClamdStats, error) { 91 | response, err := c.sendCommand("nSTATS\n") 92 | if err != nil { 93 | return nil, err 94 | } 95 | return cli.ParseStatStr(response), nil 96 | } 97 | 98 | // Reload Reload the virus database. 99 | func (c *ClamClient) Reload() (string, error) { 100 | return c.sendCommand("RELOAD") 101 | } 102 | 103 | // Shutdown Perform a clean exit. 104 | func (c *ClamClient) Shutdown() (string, error) { 105 | return c.sendCommand("SHUTDOWN") 106 | } 107 | 108 | // Scan file/directory 109 | // Scan a file or a directory (recursively) with archive support enabled (if not disabled in clamd.conf). A full path is required. 110 | func (c *ClamClient) Scan(filePath string) ([]cli.ScanResult, error) { 111 | response, err := c.sendCommand(fmt.Sprintf("SCAN %s", filePath)) 112 | if err != nil { 113 | return nil, err 114 | } 115 | return cli.FormatScanResult(response) 116 | } 117 | 118 | // ContScan file/directory 119 | // Scan file or directory (recursively) with archive support enabled and don't stop the scanning when a virus is found. 120 | func (c *ClamClient) ContScan(filePath string) ([]cli.ScanResult, error) { 121 | response, err := c.sendCommand(fmt.Sprintf("CONTSCAN %s", filePath)) 122 | if err != nil { 123 | return nil, err 124 | } 125 | return cli.FormatScanResult(response) 126 | } 127 | 128 | // MultiScan file/directory 129 | // Scan file in a standard way or scan directory (recursively) using multiple threads (to make the scanning faster on SMP machines). 130 | func (c *ClamClient) MultiScan(filePath string) ([]cli.ScanResult, error) { 131 | response, err := c.sendCommand(fmt.Sprintf("MULTISCAN %s", filePath)) 132 | if err != nil { 133 | return nil, err 134 | } 135 | return cli.FormatScanResult(response) 136 | } 137 | 138 | // AllMatchScan file/directory 139 | // ALLMATCHSCAN works just like SCAN except that it sets a mode where scanning continues after finding a match within a file. 140 | func (c *ClamClient) AllMatchScan(filePath string) (string, error) { 141 | return c.sendCommand(fmt.Sprintf("ALLMATCHSCAN %s", filePath)) 142 | } 143 | 144 | // Instream It is mandatory to prefix this command with n or z. 145 | // Scan a stream of data. The stream is sent to clamd in chunks, after INSTREAM, on the same socket on which the command was sent. This avoids the overhead of establishing new TCP connections and problems with NAT. 146 | // The format of the chunk is: '' where is the size of the following data in bytes expressed as a 4 byte unsigned integer in network byte order and is the actual chunk. Streaming is 147 | // terminated by sending a zero-length chunk. Note: do not exceed StreamMaxLength as defined in clamd.conf, otherwise clamd will reply with INSTREAM size limit exceeded and close the connection. 148 | func (c *ClamClient) Instream(data []byte) ([]cli.ScanResult, error) { 149 | conn, err := c.Dial() 150 | if err != nil { 151 | return nil, err 152 | } 153 | defer conn.Close() 154 | 155 | if _err := conn.SetWriteDeadline(time.Now().Add(c.ReadTimeout)); _err != nil { 156 | return nil, fmt.Errorf("error setting write deadline: %v", _err) 157 | } 158 | 159 | // 发送 INSTREAM 命令 160 | if _, err := fmt.Fprintf(conn, "nINSTREAM\n"); err != nil { 161 | return nil, fmt.Errorf("error sending INSTREAM command: %v", err) 162 | } 163 | 164 | reader := bytes.NewReader(data) 165 | buf := make([]byte, 4096) 166 | 167 | for { 168 | n, err := reader.Read(buf) 169 | if n > 0 { 170 | size := make([]byte, 4) 171 | binary.BigEndian.PutUint32(size, uint32(n)) 172 | 173 | if _, err := conn.Write(size); err != nil { 174 | return nil, fmt.Errorf("error sending size: %v", err) 175 | } 176 | if _, err := conn.Write(buf[:n]); err != nil { 177 | return nil, fmt.Errorf("error sending data: %v", err) 178 | } 179 | } 180 | if err != nil { 181 | if err == io.EOF { 182 | break 183 | } 184 | return nil, fmt.Errorf("error reading data: %v", err) 185 | } 186 | } 187 | 188 | // 发送零长度块以终止流 189 | if _, err := conn.Write([]byte{0, 0, 0, 0}); err != nil { 190 | return nil, fmt.Errorf("error sending terminating chunk: %v", err) 191 | } 192 | 193 | if _err := conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)); _err != nil { 194 | return nil, fmt.Errorf("error setting read deadline: %v", _err) 195 | } 196 | 197 | response, err := readResponse(conn) 198 | if err != nil { 199 | return nil, err 200 | } 201 | return cli.FormatScanResult(response) 202 | } 203 | 204 | // VersionCommands t is mandatory to prefix this command with either n or z. It is recommended to use nVERSIONCOMMANDS. 205 | // Print program and database versions, followed by "| COMMANDS:" and a space-delimited list of supported commands. Clamd <0.95 will recognize this as the VERSION command, and reply only with their version, without he commands list. 206 | // This command can be used as an easy way to check for IDSESSION support for example. 207 | func (c *ClamClient) VersionCommands() (string, error) { 208 | return c.sendCommand("nVERSIONCOMMANDS\n") 209 | } 210 | -------------------------------------------------------------------------------- /pkg/cli/clamd.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type ClamdStats struct { 10 | Pools int 11 | ThreadsLive int 12 | ThreadsIdle int 13 | ThreadsMax int 14 | IdleTimeout int 15 | QueueItems int 16 | MemHeap float64 17 | MemMmap float64 18 | MemUsed float64 19 | MemFree float64 20 | MemReleasable float64 21 | MemPoolsUsed float64 22 | MemPoolsTotal float64 23 | } 24 | 25 | func ParseStatStr(statStr string) *ClamdStats { 26 | lines := strings.Split(statStr, "\n") 27 | stats := &ClamdStats{} 28 | 29 | for _, line := range lines { 30 | parts := strings.Fields(line) 31 | if len(parts) == 0 { 32 | continue 33 | } 34 | switch parts[0] { 35 | case "POOLS:": 36 | stats.Pools, _ = strconv.Atoi(parts[1]) 37 | case "THREADS:": 38 | stats.ThreadsLive, _ = strconv.Atoi(parts[2]) 39 | stats.ThreadsIdle, _ = strconv.Atoi(parts[4]) 40 | stats.ThreadsMax, _ = strconv.Atoi(parts[6]) 41 | stats.IdleTimeout, _ = strconv.Atoi(parts[8]) 42 | case "QUEUE:": 43 | stats.QueueItems, _ = strconv.Atoi(parts[1]) 44 | case "MEMSTATS:": 45 | stats.MemHeap, _ = strconv.ParseFloat(strings.TrimSuffix(parts[2], "M"), 64) 46 | stats.MemMmap, _ = strconv.ParseFloat(strings.TrimSuffix(parts[4], "M"), 64) 47 | stats.MemUsed, _ = strconv.ParseFloat(strings.TrimSuffix(parts[6], "M"), 64) 48 | stats.MemFree, _ = strconv.ParseFloat(strings.TrimSuffix(parts[8], "M"), 64) 49 | stats.MemReleasable, _ = strconv.ParseFloat(strings.TrimSuffix(parts[10], "M"), 64) 50 | stats.MemPoolsUsed, _ = strconv.ParseFloat(strings.TrimSuffix(parts[14], "M"), 64) 51 | stats.MemPoolsTotal, _ = strconv.ParseFloat(strings.TrimSuffix(parts[16], "M"), 64) 52 | } 53 | } 54 | 55 | return stats 56 | } 57 | 58 | type ScanResult struct { 59 | Path string `json:"path"` 60 | Virus string `json:"virus"` 61 | Status string `json:"status"` 62 | } 63 | 64 | func FormatScanResult(result string) ([]ScanResult, error) { 65 | var scanResults []ScanResult 66 | lines := strings.Split(result, "\n") 67 | 68 | for _, line := range lines { 69 | line = strings.TrimSpace(line) 70 | if line == "" { 71 | continue 72 | } 73 | 74 | parts := strings.SplitN(line, ": ", 3) 75 | if len(parts) < 2 { 76 | return nil, fmt.Errorf("invalid result format: %s", line) 77 | } 78 | 79 | path := parts[0] 80 | statusPart := parts[1] 81 | 82 | var virus string 83 | var status string 84 | 85 | if strings.Contains(statusPart, "FOUND") { 86 | // Extract virus name and status 87 | virusParts := strings.SplitN(statusPart, " ", 2) 88 | if len(virusParts) == 2 { 89 | virus = virusParts[0] 90 | status = virusParts[1] 91 | } else { 92 | status = statusPart 93 | } 94 | } else { 95 | status = statusPart 96 | } 97 | 98 | scanResults = append(scanResults, ScanResult{ 99 | Path: path, 100 | Virus: virus, 101 | Status: status, 102 | }) 103 | } 104 | 105 | return scanResults, nil 106 | } 107 | -------------------------------------------------------------------------------- /pkg/cli/network.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type NetworkType string 4 | 5 | const ( 6 | Unix NetworkType = "unix" 7 | TCP NetworkType = "tcp" 8 | ) 9 | 10 | func (t NetworkType) String() string { 11 | return string(t) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/cli/params.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "time" 4 | 5 | type OutType string 6 | 7 | func (o OutType) String() string { 8 | return string(o) 9 | } 10 | 11 | const ( 12 | Json OutType = "json" 13 | Text OutType = "text" 14 | ) 15 | 16 | type ClamdParams struct { 17 | Address string 18 | NetworkType string 19 | ConnTimeout time.Duration 20 | ReadTimeout time.Duration 21 | Out OutType 22 | } 23 | 24 | var _ Params = (*ClamdParams)(nil) 25 | 26 | func (c *ClamdParams) SetAddress(address string) { 27 | c.Address = address 28 | } 29 | 30 | func (c *ClamdParams) SetNetworkType(netWorkType string) { 31 | c.NetworkType = netWorkType 32 | } 33 | 34 | func (c *ClamdParams) SetConnTimeout(timeout time.Duration) { 35 | c.ConnTimeout = timeout 36 | } 37 | 38 | func (c *ClamdParams) SetReadTimeout(timeout time.Duration) { 39 | c.ReadTimeout = timeout 40 | } 41 | 42 | func (c *ClamdParams) SetOut(out OutType) { 43 | c.Out = out 44 | } 45 | 46 | func (c *ClamdParams) GetAddress() string { 47 | return c.Address 48 | } 49 | 50 | func (c *ClamdParams) GetNetworkType() string { 51 | return c.NetworkType 52 | } 53 | 54 | func (c *ClamdParams) GetConnTimeout() time.Duration { 55 | return c.ConnTimeout 56 | } 57 | 58 | func (c *ClamdParams) GetReadTimeout() time.Duration { 59 | return c.ReadTimeout 60 | } 61 | 62 | func (c *ClamdParams) GetOut() OutType { 63 | return c.Out 64 | } 65 | 66 | type Params interface { 67 | SetAddress(address string) 68 | SetNetworkType(netWorkType string) 69 | SetConnTimeout(timeout time.Duration) 70 | SetReadTimeout(timeout time.Duration) 71 | SetOut(out OutType) 72 | GetAddress() string 73 | GetNetworkType() string 74 | GetConnTimeout() time.Duration 75 | GetReadTimeout() time.Duration 76 | GetOut() OutType 77 | } 78 | --------------------------------------------------------------------------------