├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ └── build-images.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Readme.md ├── buses ├── mqtt │ └── mqtt.go └── webhooks │ └── webhooks.go ├── config └── config.go ├── docs ├── config.yaml └── hisilicon.jpg ├── go.mod ├── go.sum ├── main.go └── servers ├── dahua └── server.go ├── ftp ├── dumbAuth.go ├── ftpDriver.go └── server.go ├── hikvision ├── httpEventReader.go ├── server.go └── tcpEventReader.go └── hisilicon └── server.go /.dockerignore: -------------------------------------------------------------------------------- 1 | config.yaml 2 | .github/ 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [toxuin] 2 | -------------------------------------------------------------------------------- /.github/workflows/build-images.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'build images' 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | docker: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Prepare 17 | id: prep 18 | run: | 19 | DOCKER_IMAGE=${{ secrets.DOCKER_USERNAME }}/${GITHUB_REPOSITORY#*/} 20 | VERSION=latest 21 | SHORTREF=${GITHUB_SHA::8} 22 | 23 | # If this is git tag, use the tag name as a docker tag 24 | if [[ $GITHUB_REF == refs/tags/* ]]; then 25 | VERSION=${GITHUB_REF#refs/tags/v} 26 | fi 27 | TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:${SHORTREF}" 28 | 29 | # If the VERSION looks like a version number, assume that 30 | # this is the most recent version of the image and also 31 | # tag it 'latest'. 32 | if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 33 | TAGS="$TAGS,${DOCKER_IMAGE}:latest" 34 | fi 35 | 36 | # Set output parameters. 37 | echo ::set-output name=tags::${TAGS} 38 | echo ::set-output name=docker_image::${DOCKER_IMAGE} 39 | 40 | - name: Set up QEMU 41 | uses: docker/setup-qemu-action@master 42 | with: 43 | platforms: all 44 | 45 | - name: Set up Docker Buildx 46 | id: buildx 47 | uses: docker/setup-buildx-action@master 48 | 49 | - name: Login to DockerHub 50 | if: github.event_name != 'pull_request' 51 | uses: docker/login-action@v1 52 | with: 53 | username: ${{ secrets.DOCKER_USERNAME }} 54 | password: ${{ secrets.DOCKER_PASSWORD }} 55 | 56 | - name: Build 57 | uses: docker/build-push-action@v2 58 | with: 59 | builder: ${{ steps.buildx.outputs.name }} 60 | context: . 61 | file: ./Dockerfile 62 | platforms: linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/amd64,linux/ppc64le 63 | push: true 64 | tags: ${{ steps.prep.outputs.tags }} 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /config.yaml 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21.5-alpine3.19 AS build_base 2 | RUN apk add ca-certificates 3 | 4 | WORKDIR /tmp/app 5 | 6 | COPY . . 7 | RUN go get -d ./... && CGO_ENABLED=0 go build -ldflags="-w -s" -o ./out/alarmserver 8 | 9 | FROM scratch 10 | COPY --from=build_base /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 11 | COPY --from=build_base /tmp/app/out/alarmserver /alarmserver 12 | 13 | EXPOSE 15002 14 | 15 | ENTRYPOINT ["/alarmserver"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Toxuin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # IP Camera Alarm Server 2 | 3 | ![docker pulls](https://img.shields.io/docker/pulls/toxuin/alarmserver) 4 | 5 | Universal Alarm Server for all your IP cameras in one place! 6 | 7 | Integrates well with Home Assistant, Node-Red, etc. Runs great on Raspberry-Pi! 8 | 9 | Supported Cameras 📸: 10 | - Hikvision (Annke/Alarm.com/etc.) 11 | - Dahua (Lorex/Amcrest/etc.) 12 | - Hisilicon (any camera that uses XmEye) 13 | - Any camera that can upload alarms to FTP 14 | 15 | Supported Delivery 📬: 16 | - MQTT 17 | - Webhooks 18 | 19 | ## Configuration 20 | 21 | Create file `config.yaml` in the folder where the application binary lives. 22 | 23 | Also see [sample config](docs/config.yaml). 24 | 25 | When alarm server is coming online, it will also send a status message to `/camera-alerts` topic with its status. 26 | 27 | #### HiSilicon 28 | 29 | This includes most of no-brand Chinese cameras that use XmEye app and have "Alarm Server" feature. 30 | 31 | If your camera needs Internet Explorer to access its Web Interface ([here's a picture!](docs/hisilicon.jpg)) and would not work in any other browser - that's the server you need. These cameras are all made by HiSilicon and sold under hundreds of different names. 32 | 33 | Logs from the camera can also work as alarm - set that in your camera under "send logs" setting. 34 | 35 | ```yaml 36 | hisilicon: 37 | enabled: true # if false, will not listen for alarms from hisilicon cams 38 | port: 15002 # has to be the same in cameras' settings 39 | ``` 40 | 41 | #### Dahua 42 | 43 | Dahua is an OEM company that makes cameras for various popular brands. See the list of [OEM Dahua camera manufacturers](https://ipvm.com/reports/dahua-oem) to find if your cameras should be configured as Dahua in Alarm Server. 44 | 45 | ```yaml 46 | dahua: 47 | enabled: true # if not enabled, it won't connect to any dahua cams 48 | cams: 49 | myCam: # name of your camera 50 | address: 192.168.1.69 # ip address or domain name 51 | username: admin # username that you use to log in to camera's web panel 52 | password: admin1234 # password that you use to log in to camera's web panel 53 | ``` 54 | 55 | #### Hikvision 56 | 57 | Alarm Server uses HTTP streaming to connect to each camera individually and subscribe to all the events. 58 | 59 | Some lower-end cameras, especially doorbells and intercoms, have broken HTTP streaming implementation that can't open more that 1 connection and "close" the http response, but leave TCP connection open (without sending keep-alive header!). For those, Alarm Server has an alternative streaming implementation. To use it, set `rawTcp: true` in camera's config file. 60 | 61 | Hikvision cameras can also be used with FTP server no problem. 62 | 63 | ```yaml 64 | hikvision: 65 | enabled: true # if not enabled, it won't connect to any hikvision cams 66 | cams: 67 | myCam: # name of your camera 68 | address: 192.168.1.69 # ip address or domain name 69 | https: false # if your camera supports ONLY https - set to true 70 | username: admin # username that you use to log in to camera's web panel 71 | password: admin1234 # password that you use to log in to camera's web panel 72 | rawTcp: false # some cams have broken streaming. Set to true if normal HTTP streaming doesn't work 73 | ``` 74 | 75 | #### FTP 76 | 77 | Alarm Server will accept any username as FTP login username and use it as camera's name. As long as the password matches, it will allow the connection. 78 | 79 | ```yaml 80 | ftp: 81 | enabled: true # if not enabled, it won't accept connections 82 | port: 21 # has to match settings in the cameras 83 | password: "root" # FTP password that will be accepted 84 | allowFiles: true # if false, no files will be stored (but transfers will still happen) 85 | rootPath: "./ftp" # folder where to save cameras' uploads 86 | ``` 87 | 88 | _Q_: Isn't FTP, like, slow?? 89 | 90 | _A_: No. Alarm processing part happens before the actual file upload even begins, and on your typical wireless home network that is less than 0.2 seconds. It's plenty fast. 91 | 92 | _Q_: Why this if there is ONVIF? 93 | 94 | _A_: Some IP cameras have ONVIF, and sometimes that even includes motion alarms, but not always does that mean that the Human Detection alarm is exposed through ONVIF, as well as all the other alarms like "SD card dead" or "failed admin login". This and, well, not all the cameras support ONVIF. 95 | 96 | ## Tested cameras: 97 | 98 | - 3xLogic VX-2M-2D-RIA (Hikvision server) 99 | 100 | - Uniden U-Bell DB1 (also known as Hikvision DS-KB6003, LaView LV-PDB1630-U and RCA HSDB2A) 101 | 102 | - Misecu 5MP Wifi AI Cam (Hisilicon server) 103 | 104 | - Lorex N8428-Z 4K NVR (Dahua server) 105 | 106 | - Hiseeu HB613-P 3 MP (Hisilicon server, tested by [abratchik](https://github.com/abratchik)) 107 | 108 | - HomeViz OB10, K4W10, OW10 (Hisilicon server, tested by [acburnett](https://github.com/acburnett)) 109 | 110 | If your camera works with Alarm Server - create an issue with some details about it and a picture, and we'll post it here. 111 | 112 | ## Docker 113 | 114 | There is a pre-built image `toxuin/alarmserver`. It is a multi-architecture image and will work both on Intel/AMD machines, and your Raspberry PI too. 115 | 116 | Usage: `docker run -d -v $PWD/config.yml:/config.yml -v $PWD/ftp:/ftp -p 21:21 -p 15002:15002 toxuin/alarmserver` 117 | 118 | Explanation: 119 | 120 | - `-d` makes it run in the background, so you don't have to stare at its logs for it to keep running 121 | 122 | - `-v $PWD/config.yml:/config.yml` passes through your config from your machine into the container. Make sure the file exists. 123 | 124 | - `-v $PWD/ftp:/ftp` passes through a folder `ftp` from where you're running this command into the container. Not needed if you don't need FTP. 125 | 126 | - `-p 21:21` allows your machine to pass through port 21 that is used for FTP server. Not needed if you're not using FTP server. 127 | 128 | - `-p 15002:15002` same as above, but for port 15002 that's used by HiSilicon alarms server. Not needed if you don't need HiSilicon server. 129 | 130 | ## Feedback 131 | 132 | This project was created for author's personal use and while it will probably work for you too, author has no idea if you use it the same way as author. To fit everyone's usage scenario better, it would be beneficial if you could describe how YOU use the Alarm Server. 133 | 134 | If you just started to use Alarm Server in your network (or use it for long time!), if you like it (or hate it!) - feel free to create an issue and just share how it works for you and what cameras you have. I would be curious to know if it fits your use case well (or not at all!). 135 | 136 | If you have any feature suggestions - create an issue, and we'll probably figure something out. 137 | 138 | ## License 139 | 140 | MIT License 141 | -------------------------------------------------------------------------------- /buses/mqtt/mqtt.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "fmt" 5 | MQTT "github.com/eclipse/paho.mqtt.golang" 6 | "github.com/toxuin/alarmserver/config" 7 | "math/rand" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | type Bus struct { 13 | Debug bool 14 | client MQTT.Client 15 | } 16 | 17 | func (mqtt *Bus) Initialize(config config.MqttConfig) { 18 | fmt.Println("Initializing MQTT bus...") 19 | mqttOpts := MQTT.NewClientOptions().AddBroker("tcp://" + config.Server + ":" + config.Port) 20 | mqttOpts.SetUsername(config.Username) 21 | if config.Password != "" { 22 | mqttOpts.SetPassword(config.Password) 23 | } 24 | mqttOpts.SetAutoReconnect(true) 25 | mqttOpts.SetMaxReconnectInterval(10 * time.Second) 26 | 27 | mqttOpts.SetClientID("alarmserver-go-" + strconv.Itoa(rand.Intn(100))) 28 | mqttOpts.SetKeepAlive(2 * time.Second) 29 | mqttOpts.SetPingTimeout(1 * time.Second) 30 | mqttOpts.SetWill(config.TopicRoot+"/alarmserver", `{ "status": "down" }`, 0, false) 31 | 32 | mqttOpts.OnConnect = func(client MQTT.Client) { 33 | fmt.Printf("MQTT: CONNECTED TO %s\n", config.Server) 34 | mqtt.SendMessage(config.TopicRoot+"/alarmserver", `{ "status": "up" }`) 35 | } 36 | 37 | mqttOpts.SetConnectionLostHandler(func(client MQTT.Client, err error) { 38 | fmt.Printf("MQTT ERROR - connection lost: %+v\n", err) 39 | }) 40 | mqttOpts.SetReconnectingHandler(func(client MQTT.Client, options *MQTT.ClientOptions) { 41 | fmt.Println("MQTT: ...trying to reconnect...") 42 | }) 43 | 44 | mqttOpts.DefaultPublishHandler = func(client MQTT.Client, msg MQTT.Message) { 45 | if mqtt.Debug { 46 | fmt.Printf(" MQTT: TOPIC: %s\n MQTT: MESSAGE: %s\n", msg.Topic(), msg.Payload()) 47 | } 48 | } 49 | 50 | mqtt.client = MQTT.NewClient(mqttOpts) 51 | if token := mqtt.client.Connect(); token.Wait() && token.Error() != nil { 52 | panic(token.Error()) 53 | } 54 | } 55 | 56 | func (mqtt *Bus) SendMessage(topic string, payload interface{}) { 57 | if !mqtt.client.IsConnected() { 58 | fmt.Println("MQTT: CLIENT NOT CONNECTED") 59 | return 60 | } 61 | if token := mqtt.client.Publish(topic, 0, false, payload); token.Wait() && token.Error() != nil { 62 | fmt.Printf("MQTT ERROR, %s\n", token.Error()) 63 | } 64 | if mqtt.Debug { 65 | fmt.Printf("MQTT: Sent message to %s\n", topic) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /buses/webhooks/webhooks.go: -------------------------------------------------------------------------------- 1 | package webhooks 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/toxuin/alarmserver/config" 8 | "io" 9 | "net/http" 10 | "strings" 11 | "text/template" 12 | ) 13 | 14 | type Bus struct { 15 | Debug bool 16 | webhooks []config.WebhookConfig 17 | client *http.Client 18 | } 19 | 20 | type WebhookPayload struct { 21 | CameraName string `json:"cameraName"` 22 | EventType string `json:"eventType"` 23 | Extra string `json:"extra"` 24 | } 25 | 26 | func (webhooks *Bus) Initialize(conf config.WebhooksConfig) { 27 | fmt.Println("Initializing Webhook bus...") 28 | webhooks.client = &http.Client{} 29 | webhooks.webhooks = conf.Items 30 | // SET DEFAULT VALUES 31 | for index, item := range webhooks.webhooks { 32 | if item.Method == "" { 33 | item.Method = http.MethodPost 34 | } 35 | webhooks.webhooks[index] = item 36 | } 37 | 38 | for _, url := range conf.Urls { 39 | basicWebhook := config.WebhookConfig{ 40 | Url: url, 41 | Method: http.MethodPost, 42 | } 43 | webhooks.webhooks = append(webhooks.webhooks, basicWebhook) 44 | } 45 | } 46 | 47 | func (webhooks *Bus) SendMessage(cameraName string, eventType string, extra string) { 48 | for _, webhook := range webhooks.webhooks { 49 | payload := WebhookPayload{CameraName: cameraName, EventType: eventType, Extra: extra} 50 | go webhooks.send(webhook, payload) 51 | } 52 | } 53 | 54 | func (webhooks *Bus) send(webhook config.WebhookConfig, payload WebhookPayload) { 55 | payloadJson, err := json.Marshal(payload) 56 | if err != nil { 57 | fmt.Println("WEBHOOKS: Error marshaling payload to JSON", err) 58 | return 59 | } 60 | 61 | var templateVars = map[string]interface{}{ 62 | "Camera": payload.CameraName, 63 | "Event": payload.EventType, 64 | "Extra": payload.Extra, 65 | } 66 | 67 | // PARSE WEBHOOK URL AS TEMPLATE 68 | urlTemplate, err := template.New("webhookUrl").Parse(webhook.Url) 69 | if err != nil { 70 | fmt.Printf("WEBHOOKS: Error parsing webhook URL as template: %s\n", webhook.Url) 71 | if webhooks.Debug { 72 | fmt.Println("Webhooks: Error", err) 73 | } 74 | return 75 | } 76 | var urlBuffer bytes.Buffer 77 | err = urlTemplate.Execute(&urlBuffer, templateVars) 78 | if err != nil { 79 | fmt.Printf("WEBHOOKS: Error rendering webhook URL as template: %s\n", webhook.Url) 80 | if webhooks.Debug { 81 | fmt.Println("Webhooks: Error", err) 82 | } 83 | return 84 | } 85 | url := urlBuffer.String() 86 | 87 | // PARSE BODY AS TEMPLATE 88 | body := bytes.NewBuffer(payloadJson) 89 | if webhook.BodyTemplate != "" { 90 | bodyTemplate, err := template.New("payload").Parse(webhook.BodyTemplate) 91 | if err != nil { 92 | fmt.Printf("WEBHOOKS: Error parsing webhook body as template: %s\n", webhook.Url) 93 | if webhooks.Debug { 94 | fmt.Println("Webhooks: Error", err) 95 | } 96 | return 97 | } 98 | 99 | var bodyBuffer bytes.Buffer 100 | err = bodyTemplate.Execute(&bodyBuffer, templateVars) 101 | if err != nil { 102 | fmt.Printf("WEBHOOKS: Error rendering webhook body as template: %s\n", webhook.Url) 103 | if webhooks.Debug { 104 | fmt.Println("Webhooks: Error", err) 105 | } 106 | return 107 | } 108 | body = &bodyBuffer 109 | } 110 | 111 | request, err := http.NewRequest(webhook.Method, url, body) 112 | if err != nil { 113 | fmt.Printf("WEBHOOKS: Error creating %s request to %s\n", webhook.Method, webhook.Url) 114 | if webhooks.Debug { 115 | fmt.Println("Webhooks: Error", err) 116 | } 117 | } 118 | request.Header.Add("Content-Type", "application/json") 119 | if len(webhook.Headers) > 0 { 120 | for _, header := range webhook.Headers { 121 | headerParts := strings.Split(header, ": ") 122 | if len(headerParts) < 2 { 123 | continue 124 | } 125 | request.Header.Set(headerParts[0], headerParts[1]) 126 | } 127 | } 128 | 129 | response, err := webhooks.client.Do(request) 130 | if err != nil { 131 | fmt.Printf("WEBHOOKS: Error delivering payload %s to %s\n", payloadJson, webhook.Url) 132 | if webhooks.Debug { 133 | fmt.Println("Webhooks: Error", err) 134 | } 135 | } 136 | if response == nil { 137 | fmt.Printf("WEBHOOKS: Got no response from %s\n", webhook.Url) 138 | return 139 | } 140 | if response.StatusCode != 200 { 141 | fmt.Printf( 142 | "WEBHOOKS: Got bad status code delivering payload to %s: %v\n", 143 | webhook.Url, 144 | response.StatusCode, 145 | ) 146 | } 147 | if webhooks.Debug { 148 | bodyBytes, _ := io.ReadAll(response.Body) 149 | bodyStr := string(bodyBytes) 150 | if len(bodyStr) == 0 { 151 | bodyStr = "*empty*" 152 | } 153 | fmt.Printf("WEBHOOKS: Response body: %s\n", bodyStr) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/viper" 6 | "github.com/toxuin/alarmserver/servers/dahua" 7 | "github.com/toxuin/alarmserver/servers/hikvision" 8 | "strings" 9 | ) 10 | 11 | type Config struct { 12 | Debug bool `json:"debug"` 13 | Mqtt MqttConfig `json:"mqtt"` 14 | Webhooks WebhooksConfig `json:"webhooks"` 15 | Hisilicon HisiliconConfig `json:"hisilicon"` 16 | Hikvision HikvisionConfig `json:"hikvision"` 17 | Dahua DahuaConfig `json:"dahua"` 18 | Ftp FtpConfig `json:"ftp"` 19 | } 20 | 21 | type MqttConfig struct { 22 | Enabled bool `json:"enabled"` 23 | Server string `json:"server"` 24 | Port string `json:"port"` 25 | Username string `json:"username"` 26 | Password string `json:"password"` 27 | TopicRoot string `json:"topicRoot"` 28 | } 29 | 30 | type WebhooksConfig struct { 31 | Enabled bool `json:"enabled"` 32 | Items []WebhookConfig `json:"items"` 33 | Urls []string `json:"urls"` 34 | } 35 | 36 | type WebhookConfig struct { 37 | Url string `json:"url"` 38 | Method string `json:"method"` 39 | Headers []string `json:"headers"` 40 | BodyTemplate string `json:"bodyTemplate"` 41 | } 42 | 43 | type HisiliconConfig struct { 44 | Enabled bool `json:"enabled"` 45 | Port string `json:"port"` 46 | } 47 | 48 | type HikvisionConfig struct { 49 | Enabled bool `json:"enabled"` 50 | Cams []hikvision.HikCamera `json:"cams"` 51 | } 52 | 53 | type DahuaConfig struct { 54 | Enabled bool `json:"enabled"` 55 | Cams []dahua.DhCamera `json:"cams"` 56 | } 57 | 58 | type FtpConfig struct { 59 | Enabled bool `json:"enabled"` 60 | Port int `json:"port"` 61 | AllowFiles bool `json:"allowFiles"` 62 | Password string `json:"password"` 63 | RootPath string `json:"rootPath"` 64 | } 65 | 66 | func (c *Config) SetDefaults() { 67 | viper.SetConfigName("config") 68 | viper.SetConfigType("yaml") 69 | viper.AddConfigPath(".") 70 | viper.AddConfigPath("./config/") 71 | viper.AddConfigPath("/config/") 72 | 73 | viper.SetDefault("debug", false) 74 | viper.SetDefault("mqtt.port", 1883) 75 | viper.SetDefault("mqtt.topicRoot", "camera-alerts") 76 | viper.SetDefault("mqtt.server", "mqtt.example.com") 77 | viper.SetDefault("mqtt.username", "anonymous") 78 | viper.SetDefault("mqtt.password", "") 79 | viper.SetDefault("hisilicon.enabled", true) 80 | viper.SetDefault("hisilicon.port", 15002) 81 | viper.SetDefault("hikvision.enabled", false) 82 | viper.SetDefault("dahua.enabled", false) 83 | viper.SetDefault("ftp.enabled", false) 84 | viper.SetDefault("ftp.port", 21) 85 | viper.SetDefault("ftp.allowFiles", true) 86 | viper.SetDefault("ftp.password", "root") 87 | viper.SetDefault("ftp.rootPath", "./ftp") 88 | 89 | _ = viper.BindEnv("debug", "DEBUG") 90 | _ = viper.BindEnv("mqtt.port", "MQTT_PORT") 91 | _ = viper.BindEnv("mqtt.topicRoot", "MQTT_TOPIC_ROOT") 92 | _ = viper.BindEnv("mqtt.server", "MQTT_SERVER") 93 | _ = viper.BindEnv("mqtt.username", "MQTT_USERNAME") 94 | _ = viper.BindEnv("mqtt.password", "MQTT_PASSWORD") 95 | _ = viper.BindEnv("hisilicon.enabled", "HISILICON_ENABLED") 96 | _ = viper.BindEnv("hisilicon.port", "HISILICON_PORT", "TCP_PORT") 97 | _ = viper.BindEnv("hikvision.enabled", "HIKVISION_ENABLED") 98 | _ = viper.BindEnv("hikvision.cams", "HIKVISION_CAMS") 99 | _ = viper.BindEnv("dahua.enabled", "DAHUA_ENABLED") 100 | _ = viper.BindEnv("dahua.cams", "DAHUA_CAMS") 101 | _ = viper.BindEnv("ftp.enabled", "FTP_ENABLED") 102 | _ = viper.BindEnv("ftp.port", "FTP_PORT") 103 | _ = viper.BindEnv("ftp.allowFiles", "FTP_ALLOW_FILES") 104 | _ = viper.BindEnv("ftp.password", "FTP_PASSWORD") 105 | _ = viper.BindEnv("ftp.rootPath", "FTP_ROOT_PATH") 106 | 107 | err := viper.ReadInConfig() 108 | if err != nil { 109 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 110 | fmt.Println("Config file not found, writing default config...") 111 | err := viper.SafeWriteConfig() 112 | if err != nil { 113 | panic(fmt.Errorf("error saving default config file: %s \n", err)) 114 | } 115 | } else { 116 | panic(fmt.Errorf("error reading config file: %s \n", err)) 117 | } 118 | } 119 | } 120 | 121 | func (c *Config) Load() *Config { 122 | myConfig := Config{ 123 | Debug: viper.GetBool("debug"), 124 | Mqtt: MqttConfig{}, 125 | Webhooks: WebhooksConfig{}, 126 | Hisilicon: HisiliconConfig{}, 127 | Hikvision: HikvisionConfig{ 128 | Enabled: viper.GetBool("hikvision.enabled"), 129 | }, 130 | Dahua: DahuaConfig{ 131 | Enabled: viper.GetBool("dahua.enabled"), 132 | }, 133 | } 134 | 135 | if viper.IsSet("mqtt") { 136 | err := viper.Sub("mqtt").Unmarshal(&myConfig.Mqtt) 137 | if err != nil { 138 | panic(fmt.Errorf("unable to decode mqtt config, %v", err)) 139 | } 140 | } 141 | if viper.IsSet("webhooks") { 142 | err := viper.Sub("webhooks").Unmarshal(&myConfig.Webhooks) 143 | if err != nil { 144 | panic(fmt.Errorf("unable to decode webhooks config, %v", err)) 145 | } 146 | } 147 | if viper.IsSet("hisilicon") { 148 | err := viper.Sub("hisilicon").Unmarshal(&myConfig.Hisilicon) 149 | if err != nil { 150 | panic(fmt.Errorf("unable to decode hisilicon config, %v", err)) 151 | } 152 | } 153 | if viper.IsSet("ftp") { 154 | err := viper.Sub("ftp").Unmarshal(&myConfig.Ftp) 155 | if err != nil { 156 | panic(fmt.Errorf("unable to decode FTP config, %v", err)) 157 | } 158 | } 159 | 160 | if !myConfig.Mqtt.Enabled && !myConfig.Webhooks.Enabled { 161 | panic("Both MQTT and Webhook buses are disabled. Nothing to do!") 162 | } 163 | 164 | if !myConfig.Hisilicon.Enabled && !myConfig.Hikvision.Enabled && !myConfig.Dahua.Enabled && !myConfig.Ftp.Enabled { 165 | panic("No Servers are enabled. Nothing to do!") 166 | } 167 | 168 | if viper.IsSet("hikvision.cams") { 169 | hikvisionCamsConfig := viper.Sub("hikvision.cams") 170 | if hikvisionCamsConfig != nil { 171 | camConfigs := viper.GetStringMapString("hikvision.cams") 172 | 173 | for camName := range camConfigs { 174 | camConfig := viper.Sub("hikvision.cams." + camName) 175 | // CONSTRUCT CAMERA URL 176 | url := "" 177 | if camConfig.GetBool("https") { 178 | url += "https://" 179 | } else { 180 | url += "http://" 181 | } 182 | url += camConfig.GetString("address") + "/ISAPI/" 183 | 184 | camera := hikvision.HikCamera{ 185 | Name: camName, 186 | Url: url, 187 | Username: camConfig.GetString("username"), 188 | Password: camConfig.GetString("password"), 189 | } 190 | if camConfig.GetBool("rawTcp") { 191 | camera.BrokenHttp = true 192 | } 193 | if myConfig.Debug { 194 | fmt.Printf("Added Hikvision camera:\n"+ 195 | " name: %s \n"+ 196 | " url: %s \n"+ 197 | " username: %s \n"+ 198 | " password set: %t\n"+ 199 | " rawRcp: %t\n", 200 | camera.Name, 201 | camera.Url, 202 | camera.Username, 203 | camera.Password != "", 204 | camera.BrokenHttp, 205 | ) 206 | } 207 | 208 | myConfig.Hikvision.Cams = append(myConfig.Hikvision.Cams, camera) 209 | } 210 | } 211 | } 212 | 213 | if viper.IsSet("dahua.cams") { 214 | camConfigs := viper.GetStringMapString("dahua.cams") 215 | for camName := range camConfigs { 216 | camConfig := viper.Sub("dahua.cams." + camName) 217 | // CONSTRUCT CAMERA URL 218 | url := "" 219 | if camConfig.GetBool("https") { 220 | url += "https://" 221 | } else { 222 | url += "http://" 223 | } 224 | url += camConfig.GetString("address") 225 | var eventsFilter []string = nil 226 | if camConfig.IsSet("events") { 227 | allEvents := strings.Split(camConfig.GetString("events"), ",") 228 | if len(allEvents) > 0 { 229 | eventsFilter = make([]string, 0) 230 | for _, eventName := range allEvents { 231 | if eventName != "" { 232 | eventsFilter = append(eventsFilter, strings.TrimSpace(eventName)) 233 | } 234 | } 235 | } 236 | } 237 | channel := "" 238 | if camConfig.IsSet("channel") { 239 | channel = camConfig.GetString("channel") 240 | } 241 | camera := dahua.DhCamera{ 242 | Debug: myConfig.Debug, 243 | Name: camName, 244 | Url: url, 245 | Username: camConfig.GetString("username"), 246 | Password: camConfig.GetString("password"), 247 | Channel: channel, 248 | Events: eventsFilter, 249 | } 250 | 251 | if myConfig.Debug { 252 | fmt.Printf("Added Dahua camera:\n"+ 253 | " name: %s \n"+ 254 | " url: %s \n"+ 255 | " username: %s \n"+ 256 | " password set: %t\n", 257 | camera.Name, 258 | camera.Url, 259 | camera.Username, 260 | camera.Password != "", 261 | ) 262 | } 263 | 264 | myConfig.Dahua.Cams = append(myConfig.Dahua.Cams, camera) 265 | } 266 | } 267 | return &myConfig 268 | } 269 | 270 | func (c *Config) Printout() { 271 | fmt.Printf("CONFIG:\n"+ 272 | " SERVER: Hisilicon - enabled: %t\n"+ 273 | " port: %s\n"+ 274 | " SERVER: Hikvision - enabled: %t\n"+ 275 | " camera count: %d\n"+ 276 | " SERVER Dahua enabled: %t\n"+ 277 | " camera count: %d\n"+ 278 | " SERVER: FTP - enabled: %t\n"+ 279 | " port: %d\n"+ 280 | " files allowed: %t\n"+ 281 | " password set: %t\n"+ 282 | " root path: %s\n"+ 283 | " BUS: MQTT - enabled: %t\n"+ 284 | " port: %s\n"+ 285 | " topicRoot: %s\n"+ 286 | " server: %s\n"+ 287 | " username: %s\n"+ 288 | " password set: %t\n"+ 289 | " BUS: Webhooks - enabled: %t\n"+ 290 | " count: %d\n", 291 | c.Hisilicon.Enabled, 292 | c.Hisilicon.Port, 293 | c.Hikvision.Enabled, 294 | len(c.Hikvision.Cams), 295 | c.Dahua.Enabled, 296 | len(c.Dahua.Cams), 297 | c.Ftp.Enabled, 298 | c.Ftp.Port, 299 | c.Ftp.AllowFiles, 300 | c.Ftp.Password != "", 301 | c.Ftp.RootPath, 302 | c.Mqtt.Enabled, 303 | c.Mqtt.Port, 304 | c.Mqtt.TopicRoot, 305 | c.Mqtt.Server, 306 | c.Mqtt.Username, 307 | c.Mqtt.Password != "", 308 | c.Webhooks.Enabled, 309 | len(c.Webhooks.Items)+len(c.Webhooks.Urls), 310 | ) 311 | } 312 | -------------------------------------------------------------------------------- /docs/config.yaml: -------------------------------------------------------------------------------- 1 | debug: false 2 | 3 | hikvision: 4 | enabled: true 5 | cams: 6 | myCam: 7 | address: 192.168.1.69 8 | https: false 9 | username: admin 10 | password: admin1234 11 | rawTcp: false 12 | myDoorbell: 13 | address: 192.168.1.13 14 | https: false 15 | username: admin 16 | password: admin666 17 | # USE RAW TCP IF HTTP STREAMING DOES NOT WORK 18 | rawTcp: true 19 | 20 | hisilicon: 21 | enabled: true 22 | port: 15002 23 | 24 | dahua: 25 | enabled: true 26 | cams: 27 | myCam: 28 | address: 192.168.1.13 29 | https: false 30 | username: admin 31 | password: admin1234 32 | # USE channel FOR NVRs TO SELECT THE CAMERA, IF NOT USED - DELETE OR COMMENT 33 | channel: 1 34 | # IF ALL EVENTS ARE NEEDED - DELETE OR COMMENT FOLLOWING LINE 35 | events: VideoMotion,CrossLineDetection,AlarmLocal,VideoLoss,VideoBlind 36 | 37 | ftp: 38 | enabled: true 39 | port: 21 40 | password: "root" 41 | allowFiles: true 42 | rootPath: "./ftp" 43 | 44 | mqtt: 45 | enabled: true 46 | username: alarmserver 47 | password: "assword" 48 | port: 1883 49 | server: "mqtt.example.com" 50 | topicroot: camera-alerts 51 | 52 | webhooks: 53 | enabled: true 54 | items: 55 | - url: "https://webhook.site/52d57401-0ea3-4e43-80a0-ceb02fba2d1e" 56 | method: "GET" # DEFAULTS TO POST 57 | headers: 58 | - "X-Beep: boop" 59 | 60 | # YOU CAN USE TEMPLATE VARIABLES TO FORM THE URL: .Camera, .Event, .Extra 61 | - url: "https://example.com/webhooks/{{ .Camera }}/events/{{ .Event }}" 62 | # YOU CAN ALSO USE TEMPLATE VARIABLES IN THE PAYLOAD BODY! 63 | # BELOW EXAMPLE DELIVERS RAW EVENT TO THE ENDPOINT 64 | bodyTemplate: '{{ .Extra }}' 65 | 66 | - url: "https://api.telegram.org/bot121212121:token/sendMessage?chat_id=43434343434&text=hello" 67 | 68 | # SIMPLE SHORTHAND FORM FOR THE SAME STUFF AS ABOVE, WILL PERFORM A POST TO EACH URL 69 | urls: 70 | - "https://example.com/camera-webhooks" 71 | - "https://example.com/another-endpoint" 72 | -------------------------------------------------------------------------------- /docs/hisilicon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toxuin/alarmserver/655dc8a0804e4bdd30c1e17fd52881cfb92278f5/docs/hisilicon.jpg -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/toxuin/alarmserver 2 | 3 | go 1.21.5 4 | 5 | require ( 6 | github.com/eclipse/paho.mqtt.golang v1.4.3 7 | github.com/icholy/digest v0.1.15 8 | github.com/spf13/viper v1.12.0 9 | goftp.io/server/v2 v2.0.0 10 | ) 11 | 12 | require ( 13 | github.com/fsnotify/fsnotify v1.5.4 // indirect 14 | github.com/gorilla/websocket v1.5.0 // indirect 15 | github.com/hashicorp/hcl v1.0.0 // indirect 16 | github.com/magiconair/properties v1.8.6 // indirect 17 | github.com/mitchellh/mapstructure v1.5.0 // indirect 18 | github.com/pelletier/go-toml v1.9.5 // indirect 19 | github.com/pelletier/go-toml/v2 v2.0.2 // indirect 20 | github.com/spf13/afero v1.8.2 // indirect 21 | github.com/spf13/cast v1.5.0 // indirect 22 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 23 | github.com/spf13/pflag v1.0.5 // indirect 24 | github.com/subosito/gotenv v1.4.0 // indirect 25 | golang.org/x/net v0.18.0 // indirect 26 | golang.org/x/sync v0.1.0 // indirect 27 | golang.org/x/sys v0.6.0 // indirect 28 | golang.org/x/text v0.8.0 // indirect 29 | gopkg.in/ini.v1 v1.66.6 // indirect 30 | gopkg.in/yaml.v2 v2.4.0 // indirect 31 | gopkg.in/yaml.v3 v3.0.1 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 29 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 30 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 31 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 32 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 33 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 34 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 35 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 36 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 37 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 38 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 39 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 40 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 41 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 42 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 43 | github.com/ChrisMcKee/allot v0.0.0-20191127021100-daeab59628b6 h1:FZKkGrxDJWXhtGeeLp2JbvlHFPiU37WWbFFWGLJWMnY= 44 | github.com/ChrisMcKee/allot v0.0.0-20191127021100-daeab59628b6/go.mod h1:kqUR9OlUcZdk6HAukmFKw0u6iFw9ElvtSkG9Ws+ty7Y= 45 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 46 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 47 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 48 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 49 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 50 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 51 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 52 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 53 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 54 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 55 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 56 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 57 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 58 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 59 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 60 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 61 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 62 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 63 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 64 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 65 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 66 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 67 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 68 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 69 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 70 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 71 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 72 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 73 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 74 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 75 | github.com/eclipse/paho.mqtt.golang v1.3.2 h1:ICzfxSyrR8bOsh9l8JBBOwO1tc2C26oEyody0ml0L6E= 76 | github.com/eclipse/paho.mqtt.golang v1.3.2/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= 77 | github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y= 78 | github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= 79 | github.com/eclipse/paho.mqtt.golang v1.4.1 h1:tUSpviiL5G3P9SZZJPC4ZULZJsxQKXxfENpMvdbAXAI= 80 | github.com/eclipse/paho.mqtt.golang v1.4.1/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA= 81 | github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= 82 | github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= 83 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 84 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 85 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 86 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 87 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 88 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 89 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 90 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 91 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 92 | github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= 93 | github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= 94 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= 95 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 96 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 97 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 98 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 99 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 100 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 101 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 102 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 103 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 104 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 105 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 106 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 107 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 108 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 109 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 110 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 111 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 112 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 113 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 114 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 115 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 116 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 117 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 118 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 119 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 120 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 121 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 122 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 123 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 124 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 125 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 126 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 127 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 128 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 129 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 130 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 131 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 132 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 133 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 134 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 135 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 136 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 137 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 138 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 139 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 140 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 141 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 142 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 143 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 144 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 145 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 146 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 147 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 148 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 149 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 150 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 151 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 152 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 153 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 154 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 155 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 156 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 157 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 158 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 159 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 160 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 161 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 162 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 163 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 164 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 165 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 166 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 167 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 168 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 169 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 170 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 171 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 172 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 173 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 174 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 175 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 176 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 177 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 178 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 179 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 180 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 181 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 182 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 183 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 184 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 185 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 186 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 187 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 188 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 189 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 190 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 191 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 192 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 193 | github.com/icholy/digest v0.1.15 h1:3vCTbaXcUjF84YlICrP/4FvfVX2TKDKgMheLwNZA+GM= 194 | github.com/icholy/digest v0.1.15/go.mod h1:uLAeDdWKIWNFMH0wqbwchbTQOmJWhzSnL7zmqSPqEEc= 195 | github.com/jlaffaye/ftp v0.0.0-20190624084859-c1312a7102bf/go.mod h1:lli8NYPQOFy3O++YmYbqVgOcQ1JPCwdOy+5zSjKJ9qY= 196 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 197 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 198 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 199 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 200 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 201 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 202 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 203 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 204 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 205 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 206 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 207 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 208 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 209 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 210 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 211 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 212 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 213 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 214 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 215 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 216 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 217 | github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= 218 | github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 219 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 220 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 221 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 222 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 223 | github.com/minio/minio-go/v6 v6.0.46/go.mod h1:qD0lajrGW49lKZLtXKtCB4X/qkMf0a5tBvN2PaZg7Gg= 224 | github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= 225 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 226 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 227 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 228 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 229 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 230 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 231 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 232 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 233 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 234 | github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= 235 | github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 236 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 237 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 238 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 239 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 240 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 241 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 242 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 243 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 244 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 245 | github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= 246 | github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 247 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 248 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 249 | github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0= 250 | github.com/pelletier/go-toml/v2 v2.0.0-beta.8/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= 251 | github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw= 252 | github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI= 253 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 254 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 255 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 256 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 257 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 258 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 259 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 260 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 261 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 262 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 263 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 264 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 265 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 266 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 267 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 268 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 269 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 270 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 271 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 272 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 273 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 274 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 275 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 276 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 277 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 278 | github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 279 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 280 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 281 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 282 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 283 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 284 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 285 | github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= 286 | github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= 287 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 288 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 289 | github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= 290 | github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 291 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 292 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 293 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 294 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 295 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 296 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 297 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 298 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 299 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 300 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 301 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= 302 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 303 | github.com/spf13/viper v1.11.0 h1:7OX/1FS6n7jHD1zGrZTM7WtY13ZELRyosK4k93oPr44= 304 | github.com/spf13/viper v1.11.0/go.mod h1:djo0X/bA5+tYVoCn+C7cAYJGcVn/qYLFTG8gdUsX7Zk= 305 | github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= 306 | github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= 307 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 308 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 309 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 310 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 311 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 312 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 313 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 314 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 315 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 316 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 317 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 318 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 319 | github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= 320 | github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= 321 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 322 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 323 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 324 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 325 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 326 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 327 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 328 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 329 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 330 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 331 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 332 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 333 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 334 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 335 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 336 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 337 | goftp.io/server/v2 v2.0.0 h1:FF8JKXXKDxAeO1uXEZz7G+IZwCDhl19dpVIlDtp3QAg= 338 | goftp.io/server/v2 v2.0.0/go.mod h1:7+H/EIq7tXdfo1Muu5p+l3oQ6rYkDZ8lY7IM5d5kVdQ= 339 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 340 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 341 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 342 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 343 | golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 344 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 345 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 346 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 347 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 348 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 349 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 350 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 351 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 352 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 353 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 354 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 355 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 356 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 357 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 358 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 359 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 360 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 361 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 362 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 363 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 364 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 365 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 366 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 367 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 368 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 369 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 370 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 371 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 372 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 373 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 374 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 375 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 376 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 377 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 378 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 379 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 380 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 381 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 382 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 383 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 384 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 385 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 386 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 387 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 388 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 389 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 390 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 391 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 392 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 393 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 394 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 395 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 396 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 397 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 398 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 399 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 400 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 401 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 402 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 403 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 404 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 405 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 406 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 407 | golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= 408 | golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 409 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 410 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 411 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 412 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 413 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 414 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 415 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 416 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 417 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 418 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 419 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 420 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 421 | golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4= 422 | golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 423 | golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ= 424 | golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 425 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 426 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 427 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 428 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 429 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 430 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 431 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 432 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 433 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 434 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 435 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 436 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 437 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 438 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 439 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 440 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 441 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 442 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 443 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 444 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 445 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 446 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 447 | golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= 448 | golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 449 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 450 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 451 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 452 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 453 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 454 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 455 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 456 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 457 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 458 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 459 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 460 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 461 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 462 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 463 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 464 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= 465 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 466 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 467 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 468 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 469 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 470 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 471 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 472 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 473 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 474 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 475 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 476 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= 477 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 478 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 479 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 480 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 481 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 482 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 483 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 484 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 485 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 486 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 487 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 488 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 489 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 490 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 491 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 492 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 493 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 494 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= 495 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 496 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= 497 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 498 | golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8= 499 | golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 500 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 501 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 502 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 503 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 504 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 505 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 506 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 507 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 508 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 509 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 510 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 511 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 512 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 513 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 514 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 515 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 516 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 517 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 518 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 519 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 520 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 521 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 522 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 523 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 524 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 525 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 526 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 527 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 528 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 529 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 530 | golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 531 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 532 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 533 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 534 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 535 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 536 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 537 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 538 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 539 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 540 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 541 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 542 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 543 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 544 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 545 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 546 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 547 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 548 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 549 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 550 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 551 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 552 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 553 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 554 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 555 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 556 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 557 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 558 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 559 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 560 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 561 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 562 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 563 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 564 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 565 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 566 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 567 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 568 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 569 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 570 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 571 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 572 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 573 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 574 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 575 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 576 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 577 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 578 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 579 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 580 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 581 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 582 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 583 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 584 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 585 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 586 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 587 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 588 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 589 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 590 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 591 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 592 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 593 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 594 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 595 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 596 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 597 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 598 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 599 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 600 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 601 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 602 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 603 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 604 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 605 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 606 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 607 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 608 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 609 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 610 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 611 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 612 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 613 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 614 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 615 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 616 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 617 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 618 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 619 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 620 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 621 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 622 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 623 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 624 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 625 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 626 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 627 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 628 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 629 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 630 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 631 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 632 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 633 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 634 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 635 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 636 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 637 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 638 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 639 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 640 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 641 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 642 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 643 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 644 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 645 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 646 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 647 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 648 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 649 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 650 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 651 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 652 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 653 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 654 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 655 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 656 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 657 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 658 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 659 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 660 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 661 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 662 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 663 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 664 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 665 | gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 666 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 667 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 668 | gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= 669 | gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 670 | gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= 671 | gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 672 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 673 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 674 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 675 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 676 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 677 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 678 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 679 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 680 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 681 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 682 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 683 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 684 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 685 | gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= 686 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 687 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 688 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 689 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 690 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 691 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 692 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 693 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 694 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 695 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 696 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/toxuin/alarmserver/buses/mqtt" 6 | "github.com/toxuin/alarmserver/buses/webhooks" 7 | conf "github.com/toxuin/alarmserver/config" 8 | "github.com/toxuin/alarmserver/servers/dahua" 9 | "github.com/toxuin/alarmserver/servers/ftp" 10 | "github.com/toxuin/alarmserver/servers/hikvision" 11 | "github.com/toxuin/alarmserver/servers/hisilicon" 12 | "os" 13 | "os/signal" 14 | "sync" 15 | "syscall" 16 | ) 17 | 18 | var config *conf.Config 19 | 20 | func init() { 21 | config.SetDefaults() 22 | } 23 | 24 | func main() { 25 | config = config.Load() 26 | fmt.Println("STARTING...") 27 | if config.Debug { 28 | config.Printout() 29 | } 30 | 31 | processesWaitGroup := sync.WaitGroup{} 32 | 33 | // INIT BUSES 34 | mqttBus := mqtt.Bus{Debug: config.Debug} 35 | if config.Mqtt.Enabled { 36 | mqttBus.Initialize(config.Mqtt) 37 | if config.Debug { 38 | fmt.Println("MQTT BUS INITIALIZED") 39 | } 40 | } 41 | 42 | webhookBus := webhooks.Bus{Debug: config.Debug} 43 | if config.Webhooks.Enabled { 44 | webhookBus.Initialize(config.Webhooks) 45 | if config.Debug { 46 | fmt.Println("WEBHOOK BUS INITIALIZED") 47 | } 48 | } 49 | 50 | messageHandler := func(cameraName string, eventType string, extra string) { 51 | if config.Mqtt.Enabled { 52 | mqttBus.SendMessage(config.Mqtt.TopicRoot+"/"+cameraName+"/"+eventType, extra) 53 | } 54 | if config.Webhooks.Enabled { 55 | webhookBus.SendMessage(cameraName, eventType, extra) 56 | } 57 | } 58 | 59 | if config.Hisilicon.Enabled { 60 | // START HISILICON ALARM SERVER 61 | hisiliconServer := hisilicon.Server{ 62 | Debug: config.Debug, 63 | WaitGroup: &processesWaitGroup, 64 | Port: config.Hisilicon.Port, 65 | MessageHandler: messageHandler, 66 | } 67 | hisiliconServer.Start() 68 | if config.Debug { 69 | fmt.Println("STARTED HISILICON SERVER") 70 | } 71 | } 72 | 73 | if config.Hikvision.Enabled { 74 | // START HIKVISION ALARM SERVER 75 | hikvisionServer := hikvision.Server{ 76 | Debug: config.Debug, 77 | WaitGroup: &processesWaitGroup, 78 | Cameras: &config.Hikvision.Cams, 79 | MessageHandler: messageHandler, 80 | } 81 | hikvisionServer.Start() 82 | if config.Debug { 83 | fmt.Println("STARTED HIKVISION SERVER") 84 | } 85 | } 86 | 87 | if config.Dahua.Enabled { 88 | // START DAHUA SERVER 89 | dhServer := dahua.Server{ 90 | Debug: config.Debug, 91 | WaitGroup: &processesWaitGroup, 92 | Cameras: &config.Dahua.Cams, 93 | MessageHandler: messageHandler, 94 | } 95 | dhServer.Start() 96 | if config.Debug { 97 | fmt.Println("STARTED DAHUA SERVER") 98 | } 99 | } 100 | 101 | if config.Ftp.Enabled { 102 | // START FTP SERVER 103 | ftpServer := ftp.Server{ 104 | Debug: config.Debug, 105 | WaitGroup: &processesWaitGroup, 106 | Port: config.Ftp.Port, 107 | AllowFiles: config.Ftp.AllowFiles, 108 | RootPath: config.Ftp.RootPath, 109 | Password: config.Ftp.Password, 110 | MessageHandler: messageHandler, 111 | } 112 | ftpServer.Start() 113 | if config.Debug { 114 | fmt.Println("STARTED FTP SERVER") 115 | } 116 | } 117 | 118 | processesWaitGroup.Wait() 119 | 120 | // START INFINITE LOOP WAITING FOR SERVERS 121 | exitSignal := make(chan os.Signal) 122 | signal.Notify(exitSignal, syscall.SIGINT, syscall.SIGTERM) 123 | <-exitSignal 124 | } 125 | -------------------------------------------------------------------------------- /servers/dahua/server.go: -------------------------------------------------------------------------------- 1 | package dahua 2 | 3 | import ( 4 | "fmt" 5 | "github.com/icholy/digest" 6 | "io" 7 | "log" 8 | "mime" 9 | "mime/multipart" 10 | "net/http" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | ) 15 | 16 | type DhCamera struct { 17 | Debug bool 18 | Name string `json:"name"` 19 | Url string `json:"url"` 20 | Username string `json:"username"` 21 | Password string `json:"password"` 22 | Channel string `json:"channel"` 23 | Events []string `json:"events"` 24 | client *http.Client 25 | } 26 | 27 | type Server struct { 28 | Debug bool 29 | WaitGroup *sync.WaitGroup 30 | Cameras *[]DhCamera 31 | MessageHandler func(cameraName string, eventType string, extra string) 32 | } 33 | 34 | type DhEvent struct { 35 | Camera *DhCamera 36 | Type string 37 | Message string 38 | } 39 | 40 | type Event struct { 41 | Code string 42 | Action string 43 | Index int 44 | Data string 45 | active bool 46 | } 47 | 48 | func (camera *DhCamera) readEvents(channel chan<- DhEvent, callback func()) { 49 | eventUrlSuffix := "/cgi-bin/eventManager.cgi?action=attach&heartbeat=10" 50 | if camera.Channel != "" { 51 | eventUrlSuffix += "&channel=" + camera.Channel 52 | } 53 | if camera.Events != nil && len(camera.Events) > 0 { 54 | eventUrlSuffix += "&codes=[" + strings.Join(camera.Events, ",") + "]" 55 | } else { 56 | eventUrlSuffix += "&codes=[All]" 57 | } 58 | request, err := http.NewRequest("GET", camera.Url+eventUrlSuffix, nil) 59 | if err != nil { 60 | fmt.Printf("DAHUA: Error: Could not connect to camera %s\n", camera.Name) 61 | fmt.Println("DAHUA: Error", err) 62 | callback() 63 | return 64 | } 65 | if camera.client.Transport == nil { // BASIC AUTH 66 | request.SetBasicAuth(camera.Username, camera.Password) 67 | } 68 | 69 | response, err := camera.client.Do(request) 70 | if err != nil { 71 | fmt.Printf("DAHUA: Error opening HTTP connection to camera %s\n", camera.Name) 72 | fmt.Println(err) 73 | callback() 74 | return 75 | } 76 | defer response.Body.Close() 77 | 78 | if response.StatusCode != 200 { 79 | fmt.Printf("DAHUA: Warning: Status Code was not 200, but %v\n", response.StatusCode) 80 | if camera.Debug { // DUMP BODY 81 | body, err := io.ReadAll(response.Body) 82 | if err != nil { 83 | log.Fatalln(err) 84 | } 85 | fmt.Println(string(body)) 86 | } 87 | } 88 | 89 | // FIGURE OUT MULTIPART BOUNDARY 90 | mediaType, params, err := mime.ParseMediaType(response.Header.Get("Content-Type")) 91 | if camera.Debug { 92 | fmt.Printf("DAHUA: Media type is %s\n", mediaType) 93 | } 94 | 95 | if params["boundary"] == "" { 96 | fmt.Println("DAHUA: ERROR: Camera " + camera.Name + " does not seem to support event streaming") 97 | callback() 98 | return 99 | } 100 | multipartBoundary := params["boundary"] 101 | 102 | event := Event{} 103 | 104 | // READ PART BY PART 105 | multipartReader := multipart.NewReader(response.Body, multipartBoundary) 106 | for { 107 | part, err := multipartReader.NextPart() 108 | if err == io.EOF { 109 | break 110 | } 111 | if err != nil { 112 | fmt.Println(err) 113 | continue 114 | } 115 | contentLength, _ := strconv.Atoi(part.Header.Get("Content-Length")) 116 | body := make([]byte, contentLength) 117 | _, err = part.Read(body) 118 | if err != nil { 119 | fmt.Println(err) 120 | continue 121 | } 122 | 123 | if camera.Debug { 124 | fmt.Printf("DAHUA: Read event body: %s\n", body) 125 | } 126 | 127 | // EXAMPLE: "Code=VideoMotion; action=Start; index=0\r\n\r\n" 128 | line := strings.Trim(string(body), " \n\r") 129 | if line == "Heartbeat" { 130 | continue 131 | } 132 | items := strings.Split(line, ";") 133 | keyValues := make(map[string]string, len(items)) 134 | for _, item := range items { 135 | parts := strings.Split(item, "=") 136 | if len(parts) > 0 { 137 | keyValues[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) 138 | } 139 | } 140 | // EXAMPLE: { Code: VideoMotion, action: Start, index: 0 } 141 | index := 0 142 | index, _ = strconv.Atoi(keyValues["index"]) 143 | event.Code = keyValues["Code"] 144 | event.Action = keyValues["action"] 145 | event.Index = index 146 | event.Data = keyValues["data"] 147 | 148 | switch event.Action { 149 | case "Start": 150 | if !event.active { 151 | if camera.Debug { 152 | fmt.Println("DAHUA: SENDING CAMERA EVENT!") 153 | } 154 | dahuaEvent := DhEvent{ 155 | Camera: camera, 156 | Type: event.Code, 157 | Message: event.Data, 158 | } 159 | if dahuaEvent.Message == "" { 160 | dahuaEvent.Message = event.Action 161 | } 162 | channel <- dahuaEvent 163 | } 164 | event.active = true 165 | case "Stop": 166 | event.active = false 167 | } 168 | } 169 | } 170 | 171 | func (server *Server) addCamera(waitGroup *sync.WaitGroup, cam *DhCamera, channel chan<- DhEvent) { 172 | waitGroup.Add(1) 173 | 174 | if server.Debug { 175 | fmt.Printf("DAHUA: Adding camera %s: %s\n", cam.Name, cam.Url) 176 | } 177 | 178 | if cam.client == nil { 179 | cam.client = &http.Client{} 180 | } 181 | 182 | // PROBE AUTH 183 | request, err := http.NewRequest("GET", cam.Url+"/cgi-bin/configManager.cgi?action=getConfig&name=General", nil) 184 | if err != nil { 185 | fmt.Printf("DAHUA: Error probing auth method for camera %s\n", cam.Name) 186 | fmt.Println(err) 187 | return 188 | } 189 | request.SetBasicAuth(cam.Username, cam.Password) 190 | response, err := cam.client.Do(request) 191 | if err != nil { 192 | fmt.Printf("DAHUA: Error probing HTTP Auth method for camera %s\n", cam.Name) 193 | fmt.Println(err) 194 | return 195 | } 196 | defer response.Body.Close() 197 | if response.StatusCode == 401 { 198 | if response.Header.Get("WWW-Authenticate") == "" { 199 | // BAD PASSWORD 200 | fmt.Printf("DAHUA: UNKNOWN AUTH METHOD FOR CAMERA %s! SKIPPING...", cam.Name) 201 | return 202 | } 203 | authMethod := strings.Split(response.Header.Get("WWW-Authenticate"), " ")[0] 204 | if authMethod == "Basic" { 205 | // BAD PASSWORD 206 | fmt.Printf("DAHUA: BAD PASSWORD FOR CAMERA %s! SKIPPING...", cam.Name) 207 | return 208 | } 209 | 210 | // TRY ANOTHER TIME WITH DIGEST TRANSPORT 211 | cam.client.Transport = &digest.Transport{ 212 | Username: cam.Username, 213 | Password: cam.Password, 214 | } 215 | response, err := cam.client.Do(request) 216 | if err != nil || response.StatusCode == 401 { 217 | if err != nil { 218 | fmt.Println(err) 219 | } 220 | // BAD PASSWORD 221 | fmt.Printf("DAHUA: BAD PASSWORD FOR CAMERA %s! SKIPPING...", cam.Name) 222 | return 223 | } 224 | 225 | if server.Debug { 226 | fmt.Println("DAHUA: USING DIGEST AUTH") 227 | } 228 | } else if server.Debug { 229 | fmt.Println("DAHUA: USING BASIC AUTH") 230 | } 231 | 232 | go func() { 233 | defer waitGroup.Done() 234 | done := false 235 | callback := func() { 236 | done = true 237 | } 238 | 239 | for { 240 | if done { 241 | break 242 | } 243 | cam.readEvents(channel, callback) 244 | } 245 | fmt.Printf("DAHUA: Closed connection to camera %s\n", cam.Name) 246 | }() 247 | } 248 | 249 | func (server *Server) Start() { 250 | if server.Cameras == nil || len(*server.Cameras) == 0 { 251 | fmt.Println("DAHUA: Error: no cameras defined") 252 | return 253 | } 254 | 255 | if server.MessageHandler == nil { 256 | fmt.Println("DAHUA: Message handler is not set for Dahua cams - that's probably not what you want") 257 | server.MessageHandler = func(cameraName string, eventType string, extra string) { 258 | fmt.Printf("DAHUA: Lost alarm: %s - %s: %s\n", cameraName, eventType, extra) 259 | } 260 | } 261 | 262 | waitGroup := sync.WaitGroup{} 263 | eventChannel := make(chan DhEvent, 5) 264 | 265 | for _, camera := range *server.Cameras { 266 | server.addCamera(&waitGroup, &camera, eventChannel) 267 | } 268 | 269 | // START MESSAGE PROCESSOR 270 | go func(waitGroup *sync.WaitGroup, channel <-chan DhEvent) { 271 | // WAIT GROUP FOR INDIVIDUAL CAMERAS 272 | defer waitGroup.Done() 273 | 274 | // EXTERNAL WAIT GROUP FOR PROCESSES 275 | defer server.WaitGroup.Done() 276 | server.WaitGroup.Add(1) 277 | 278 | for { 279 | event := <-channel 280 | go server.MessageHandler(event.Camera.Name, event.Type, event.Message) 281 | } 282 | }(&waitGroup, eventChannel) 283 | 284 | waitGroup.Wait() 285 | } 286 | -------------------------------------------------------------------------------- /servers/ftp/dumbAuth.go: -------------------------------------------------------------------------------- 1 | package ftp 2 | 3 | import ( 4 | "fmt" 5 | "goftp.io/server/v2" 6 | ) 7 | 8 | type DumbAuth struct { 9 | Debug bool 10 | Password string 11 | } 12 | 13 | func (d *DumbAuth) CheckPasswd(ctx *server.Context, username string, password string) (bool, error) { 14 | if d.Debug { 15 | fmt.Printf("FTP: %s is connecting with username %s and password %s\n", ctx.Sess.RemoteAddr().String(), username, password) 16 | } 17 | return password == d.Password, nil 18 | } 19 | -------------------------------------------------------------------------------- /servers/ftp/ftpDriver.go: -------------------------------------------------------------------------------- 1 | package ftp 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "goftp.io/server/v2" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | type Driver struct { 15 | Debug bool 16 | RootPath string 17 | AllowFileUpload bool 18 | EventChannel chan<- Event 19 | rootFInfo os.FileInfo 20 | } 21 | 22 | type EventMaker interface { 23 | MakeEvent(eventStr string) Event 24 | } 25 | 26 | func (driver *Driver) createEvent(eventStr string) Event { 27 | if driver.Debug { 28 | fmt.Printf("FTP: PARSING STRING TO EVENT %s\n", eventStr) 29 | } 30 | 31 | return Event{ 32 | Type: "ftpUpload", 33 | Message: eventStr, 34 | } 35 | } 36 | 37 | func (driver *Driver) realPath(path string) string { 38 | paths := strings.Split(path, "/") 39 | return filepath.Join(append([]string{driver.RootPath}, paths...)...) 40 | } 41 | 42 | func (driver *Driver) Stat(context *server.Context, path string) (os.FileInfo, error) { 43 | if driver.Debug { 44 | fmt.Printf("FTP: DRIVER: Stat(%s)\n", path) 45 | } 46 | 47 | if !driver.AllowFileUpload { 48 | return driver.rootFInfo, nil 49 | } 50 | 51 | basePath := driver.realPath(path) 52 | absolutePath, err := filepath.Abs(basePath) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return os.Lstat(absolutePath) 57 | } 58 | 59 | func (driver *Driver) ListDir(context *server.Context, path string, callback func(os.FileInfo) error) error { 60 | if driver.Debug { 61 | fmt.Printf("FTP: DRIVER: ListDir(%s)\n", path) 62 | } 63 | if !driver.AllowFileUpload { 64 | // THIS WILL RESULT IN AN INFINITELY DEEP TREE CONTAINING ROOT DIR CONTAINING ITSELF 65 | return callback(driver.rootFInfo) 66 | } 67 | 68 | basePath := driver.realPath(path) 69 | return filepath.Walk(basePath, func(localPath string, fInfo os.FileInfo, err error) error { 70 | if err != nil { 71 | return err 72 | } 73 | rPath, _ := filepath.Rel(basePath, localPath) 74 | if rPath == fInfo.Name() { 75 | err = callback(fInfo) 76 | if err != nil { 77 | return err 78 | } 79 | if fInfo.IsDir() { 80 | return filepath.SkipDir 81 | } 82 | } 83 | return nil 84 | }) 85 | } 86 | 87 | func (driver *Driver) DeleteDir(context *server.Context, path string) error { 88 | if driver.Debug { 89 | fmt.Printf("FTP: DRIVER: DeleteDir(%s)\n", path) 90 | } 91 | if !driver.AllowFileUpload { 92 | return nil 93 | } 94 | 95 | realPath := driver.realPath(path) 96 | fInfo, err := os.Lstat(realPath) 97 | if err != nil { 98 | return err 99 | } 100 | if !fInfo.IsDir() { 101 | return errors.New("not a directory") 102 | } 103 | return os.RemoveAll(realPath) 104 | } 105 | 106 | func (driver *Driver) DeleteFile(context *server.Context, path string) error { 107 | if driver.Debug { 108 | fmt.Printf("FTP: DRIVER: DeleteFile(%s)\n", path) 109 | } 110 | if !driver.AllowFileUpload { 111 | return nil 112 | } 113 | 114 | realPath := driver.realPath(path) 115 | fInfo, err := os.Lstat(realPath) 116 | if err != nil { 117 | return err 118 | } 119 | if !fInfo.Mode().IsRegular() { 120 | return errors.New("not a file") 121 | } 122 | return os.Remove(realPath) 123 | } 124 | 125 | func (driver *Driver) Rename(context *server.Context, fromPath string, toPath string) error { 126 | if driver.Debug { 127 | fmt.Printf("FTP: DRIVER: Rename(fromPath: %s, toPath: %s)\n", fromPath, toPath) 128 | } 129 | if !driver.AllowFileUpload { 130 | return nil 131 | } 132 | 133 | return os.Rename(driver.realPath(fromPath), driver.realPath(toPath)) 134 | } 135 | 136 | func (driver *Driver) MakeDir(context *server.Context, path string) error { 137 | if driver.Debug { 138 | fmt.Printf("FTP: DRIVER: MakeDir(%s)\n", path) 139 | } 140 | if !driver.AllowFileUpload { 141 | return nil 142 | } 143 | 144 | return os.MkdirAll(driver.realPath(path), os.ModePerm) 145 | } 146 | 147 | func (driver *Driver) GetFile(context *server.Context, path string, offset int64) (int64, io.ReadCloser, error) { 148 | if driver.Debug { 149 | fmt.Printf("FTP: DRIVER: GetFile(path: %s, offset: %v)\n", path, offset) 150 | } 151 | if !driver.AllowFileUpload { 152 | // TODO 153 | } 154 | 155 | realPath := driver.realPath(path) 156 | fInfo, err := os.Open(realPath) 157 | if err != nil { 158 | return -1, nil, err 159 | } 160 | 161 | defer func() { 162 | // CLOSE FILE 163 | if err != nil && fInfo != nil { 164 | _ = fInfo.Close() 165 | } 166 | }() 167 | 168 | info, err := fInfo.Stat() 169 | if err != nil { 170 | return -1, nil, err 171 | } 172 | 173 | _, err = fInfo.Seek(offset, io.SeekStart) 174 | if err != nil { 175 | return -1, nil, err 176 | } 177 | 178 | return info.Size() - offset, fInfo, nil 179 | } 180 | 181 | func (driver *Driver) PutFile(context *server.Context, destPath string, data io.Reader, filepos int64) (int64, error) { 182 | if driver.Debug { 183 | fmt.Printf("FTP: DRIVER: PutFile(destPath: %s, filepos: %v)\n", destPath, filepos) 184 | } 185 | 186 | go func() { 187 | var event Event = driver.createEvent(destPath) 188 | if event.CameraName == "" { 189 | event.CameraName = context.Sess.LoginUser() 190 | } 191 | // DISPATCH EVENT 192 | driver.EventChannel <- event 193 | }() 194 | 195 | if !driver.AllowFileUpload { // JUST RETURN SUCCESSFUL UPLOAD 196 | buf := &bytes.Buffer{} 197 | bytesRead, err := io.Copy(buf, data) 198 | if err != nil { 199 | return -1, err 200 | } 201 | return bytesRead, nil 202 | } 203 | 204 | realPath := driver.realPath(destPath) 205 | 206 | var isExist bool 207 | f, err := os.Lstat(realPath) 208 | if err == nil { 209 | isExist = true 210 | if f.IsDir() { 211 | return -1, errors.New("name conflict") 212 | } 213 | } else { 214 | if os.IsNotExist(err) { 215 | isExist = false 216 | } else { 217 | return -1, errors.New(fmt.Sprintln("put file error: ", err)) 218 | } 219 | } 220 | 221 | if filepos > -1 && !isExist { 222 | filepos = -1 223 | } 224 | 225 | if filepos == -1 { 226 | if isExist { 227 | err = os.Remove(realPath) 228 | if err != nil { 229 | return -1, err 230 | } 231 | } 232 | f, err := os.Create(realPath) 233 | if err != nil { 234 | return -1, err 235 | } 236 | defer f.Close() 237 | bytesRead, err := io.Copy(f, data) 238 | if err != nil { 239 | return -1, err 240 | } 241 | return bytesRead, nil 242 | } 243 | 244 | of, err := os.OpenFile(realPath, os.O_APPEND|os.O_RDWR, 0660) 245 | if err != nil { 246 | return -1, err 247 | } 248 | defer of.Close() 249 | 250 | info, err := of.Stat() 251 | if err != nil { 252 | return -1, err 253 | } 254 | if filepos > info.Size() { 255 | return -1, fmt.Errorf("offset %d is beyond file size %d", filepos, info.Size()) 256 | } 257 | 258 | _, err = of.Seek(filepos, io.SeekEnd) 259 | if err != nil { 260 | return -1, err 261 | } 262 | 263 | bytesRead, err := io.Copy(of, data) 264 | if err != nil { 265 | return -1, err 266 | } 267 | 268 | return bytesRead, nil 269 | } 270 | 271 | func NewDriver(debug bool, rootPath string, allowFileUpload bool, eventChannel chan<- Event) (server.Driver, error) { 272 | var err error 273 | rootPath, err = filepath.Abs(rootPath) 274 | if err != nil { 275 | return nil, err 276 | } 277 | 278 | // MAKE SURE FTP ROOT DIR EXISTS 279 | _ = os.MkdirAll(rootPath, os.ModePerm) 280 | 281 | // POPULATE ROOT FILE INFO 282 | absolutePath, err := filepath.Abs(rootPath) 283 | if err != nil { 284 | return nil, err 285 | } 286 | fInfo, err := os.Lstat(absolutePath) 287 | if err != nil { 288 | return nil, err 289 | } 290 | 291 | return &Driver{ 292 | Debug: debug, 293 | RootPath: rootPath, 294 | AllowFileUpload: allowFileUpload, 295 | EventChannel: eventChannel, 296 | rootFInfo: fInfo, 297 | }, nil 298 | } 299 | -------------------------------------------------------------------------------- /servers/ftp/server.go: -------------------------------------------------------------------------------- 1 | package ftp 2 | 3 | import ( 4 | "fmt" 5 | "goftp.io/server/v2" 6 | "sync" 7 | ) 8 | 9 | type Server struct { 10 | Debug bool 11 | WaitGroup *sync.WaitGroup 12 | Port int 13 | AllowFiles bool 14 | RootPath string 15 | Password string 16 | MessageHandler func(cameraName string, eventType string, extra string) 17 | } 18 | 19 | type Event struct { 20 | CameraName string `json:"camera"` 21 | Type string `json:"type"` 22 | Message string `json:"message"` 23 | } 24 | 25 | func (serv *Server) Start() { 26 | if serv.MessageHandler == nil { 27 | fmt.Println("FTP: Message handler is not set for FTP server - that's probably not what you want") 28 | serv.MessageHandler = func(cameraName string, eventType string, extra string) { 29 | fmt.Printf("FTP: Lost alarm: %s - %s: %s\n", cameraName, eventType, extra) 30 | } 31 | } 32 | // DEFAULT FTP PASSWORD 33 | if serv.Password == "" { 34 | serv.Password = "root" 35 | } 36 | 37 | go func() { 38 | defer serv.WaitGroup.Done() 39 | serv.WaitGroup.Add(1) 40 | 41 | eventChannel := make(chan Event, 5) 42 | 43 | // START MESSAGE PROCESSOR 44 | go func(channel <-chan Event) { 45 | for { 46 | event := <-channel 47 | go serv.MessageHandler(event.CameraName, event.Type, event.Message) 48 | } 49 | }(eventChannel) 50 | 51 | driver, err := NewDriver(serv.Debug, serv.RootPath, serv.AllowFiles, eventChannel) 52 | if err != nil { 53 | fmt.Println("FTP: Cannot init driver") 54 | } 55 | 56 | opt := &server.Options{ 57 | Name: "alarmserver-go", 58 | WelcomeMessage: "HI", 59 | Driver: driver, 60 | Port: serv.Port, 61 | Perm: server.NewSimplePerm("root", "root"), 62 | Auth: &DumbAuth{Debug: serv.Debug, Password: serv.Password}, 63 | } 64 | 65 | if !serv.Debug { 66 | opt.Logger = &server.DiscardLogger{} 67 | } 68 | 69 | ftpServer, err := server.NewServer(opt) 70 | if err != nil { 71 | fmt.Println("FTP: Cannot start FTP server", err) 72 | return 73 | } 74 | err = ftpServer.ListenAndServe() 75 | if err != nil { 76 | fmt.Println(fmt.Sprintf("FTP: Cannot listen on port %v", serv.Port), err) 77 | return 78 | } 79 | defer ftpServer.Shutdown() 80 | fmt.Printf("FTP: Listening on port %v", serv.Port) 81 | }() 82 | } 83 | -------------------------------------------------------------------------------- /servers/hikvision/httpEventReader.go: -------------------------------------------------------------------------------- 1 | package hikvision 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "github.com/icholy/digest" 7 | "io" 8 | "log" 9 | "mime" 10 | "mime/multipart" 11 | "net/http" 12 | "strconv" 13 | ) 14 | 15 | type HttpEventReader struct { 16 | Debug bool 17 | client *http.Client 18 | } 19 | 20 | func (eventReader *HttpEventReader) ReadEvents(camera *HikCamera, channel chan<- HikEvent, callback func()) { 21 | if eventReader.client == nil { 22 | eventReader.client = &http.Client{} 23 | if camera.AuthMethod == Digest { 24 | eventReader.client.Transport = &digest.Transport{ 25 | Username: camera.Username, 26 | Password: camera.Password, 27 | } 28 | } 29 | } 30 | 31 | request, err := http.NewRequest("GET", camera.Url+"Event/notification/alertStream", nil) 32 | if err != nil { 33 | fmt.Printf("HIK: Error: Could not connect to camera %s\n", camera.Name) 34 | fmt.Println("HIK: Error", err) 35 | callback() 36 | return 37 | } 38 | if camera.AuthMethod == Basic { 39 | request.SetBasicAuth(camera.Username, camera.Password) 40 | } 41 | 42 | response, err := eventReader.client.Do(request) 43 | if err != nil { 44 | fmt.Printf("HIK: Error opening HTTP connection to camera %s\n", camera.Name) 45 | fmt.Println(err) 46 | return 47 | } 48 | 49 | if response.StatusCode != 200 { 50 | fmt.Printf("HIK: BAD STATUS %d", response.StatusCode) 51 | } 52 | defer response.Body.Close() 53 | 54 | // FIGURE OUT MULTIPART BOUNDARY 55 | mediaType, params, err := mime.ParseMediaType(response.Header.Get("Content-Type")) 56 | if mediaType != "multipart/mixed" || params["boundary"] == "" { 57 | fmt.Println("HIK: ERROR: Camera " + camera.Name + " does not seem to support event streaming") 58 | fmt.Println(" Is it a doorbell? Try adding rawTcp to its config!") 59 | callback() 60 | return 61 | } 62 | multipartBoundary := params["boundary"] 63 | 64 | xmlEvent := XmlEvent{} 65 | 66 | // READ PART BY PART 67 | multipartReader := multipart.NewReader(response.Body, multipartBoundary) 68 | for { 69 | part, err := multipartReader.NextPart() 70 | if err == io.EOF { 71 | break 72 | } 73 | if err != nil { 74 | fmt.Println(err) 75 | continue 76 | } 77 | contentLength, _ := strconv.Atoi(part.Header.Get("Content-Length")) 78 | body := make([]byte, contentLength) 79 | _, err = part.Read(body) 80 | if err != nil { 81 | fmt.Println(err) 82 | continue 83 | } 84 | 85 | err = xml.Unmarshal(body, &xmlEvent) 86 | if err != nil { 87 | fmt.Println(err) 88 | continue 89 | } 90 | 91 | // FILL IN THE CAMERA INTO FRESHLY-UNMARSHALLED EVENT 92 | xmlEvent.Camera = camera 93 | 94 | if eventReader.Debug { 95 | log.Printf("%s event: %s (%s - %d)", xmlEvent.Camera.Name, xmlEvent.Type, xmlEvent.State, xmlEvent.Id) 96 | } 97 | 98 | switch xmlEvent.State { 99 | case "active": 100 | if !xmlEvent.Active { 101 | if eventReader.Debug { 102 | fmt.Println("HIK: SENDING CAMERA EVENT!") 103 | } 104 | event := HikEvent{Camera: camera} 105 | event.Type = xmlEvent.Type 106 | event.Message = xmlEvent.Description 107 | channel <- event 108 | } 109 | xmlEvent.Active = true 110 | case "inactive": 111 | xmlEvent.Active = false 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /servers/hikvision/server.go: -------------------------------------------------------------------------------- 1 | package hikvision 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "github.com/icholy/digest" 7 | "net/http" 8 | "strings" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | type HttpAuthMethod int 14 | 15 | const ( 16 | Basic HttpAuthMethod = iota 17 | Digest 18 | ) 19 | 20 | type HikCamera struct { 21 | Name string `json:"name"` 22 | Url string `json:"url"` 23 | Username string `json:"username"` 24 | Password string `json:"password"` 25 | EventReader HikEventReader 26 | BrokenHttp bool 27 | AuthMethod HttpAuthMethod 28 | } 29 | 30 | type HikEvent struct { 31 | Type string 32 | Message string 33 | Camera *HikCamera 34 | } 35 | 36 | type Server struct { 37 | Debug bool 38 | WaitGroup *sync.WaitGroup 39 | Cameras *[]HikCamera 40 | MessageHandler func(cameraName string, eventType string, extra string) 41 | } 42 | 43 | type XmlEvent struct { 44 | XMLName xml.Name `xml:"EventNotificationAlert"` 45 | IpAddress string `xml:"ipAddress"` 46 | Port int `xml:"portNo"` 47 | ChannelId int `xml:"channelID"` 48 | Time time.Time `xml:"dateTime"` 49 | Id int `xml:"activePostCount"` 50 | Type string `xml:"eventType"` 51 | State string `xml:"eventState"` 52 | Description string `xml:"eventDescription"` 53 | Active bool 54 | Camera *HikCamera 55 | } 56 | 57 | type HikEventReader interface { 58 | ReadEvents(camera *HikCamera, channel chan<- HikEvent, callback func()) 59 | } 60 | 61 | func (server *Server) addCamera(waitGroup *sync.WaitGroup, camera *HikCamera, eventChannel chan<- HikEvent) { 62 | waitGroup.Add(1) 63 | if !camera.BrokenHttp { 64 | camera.EventReader = &HttpEventReader{Debug: server.Debug} 65 | } else { 66 | camera.EventReader = &TcpEventReader{Debug: server.Debug} 67 | } 68 | if server.Debug { 69 | fmt.Printf("HIK: Adding camera %s: %s\n", camera.Name, camera.Url) 70 | } 71 | 72 | // PROBE AUTH 73 | client := &http.Client{} 74 | request, err := http.NewRequest("GET", camera.Url+"System/status", nil) 75 | if err != nil { 76 | fmt.Printf("HIK: Error probing auth method for camera %s\n", camera.Name) 77 | fmt.Println(err) 78 | return 79 | } 80 | request.SetBasicAuth(camera.Username, camera.Password) 81 | response, err := client.Do(request) 82 | if err != nil { 83 | fmt.Printf("HIK: Error probing HTTP Auth method for camera %s\n", camera.Name) 84 | fmt.Println(err) 85 | return 86 | } 87 | if response.StatusCode == 401 { 88 | if response.Header.Get("WWW-Authenticate") == "" { 89 | // BAD PASSWORD 90 | fmt.Printf("HIK: UNKNOWN AUTH METHOD FOR CAMERA %s! SKIPPING...", camera.Name) 91 | return 92 | } 93 | authMethod := strings.Split(response.Header.Get("WWW-Authenticate"), " ")[0] 94 | if authMethod == "Basic" { 95 | // BAD PASSWORD 96 | fmt.Printf("HIK: BAD PASSWORD FOR CAMERA %s! SKIPPING...", camera.Name) 97 | return 98 | } 99 | 100 | // TRY ANOTHER TIME WITH DIGEST TRANSPORT 101 | client.Transport = &digest.Transport{ 102 | Username: camera.Username, 103 | Password: camera.Password, 104 | } 105 | response, err := client.Do(request) 106 | if err != nil || response.StatusCode == 401 { 107 | if err != nil { 108 | fmt.Println(err) 109 | } 110 | // BAD PASSWORD 111 | fmt.Printf("HIK: BAD PASSWORD FOR CAMERA %s! SKIPPING...", camera.Name) 112 | return 113 | } 114 | 115 | camera.AuthMethod = Digest 116 | if server.Debug { 117 | fmt.Println("HIK: USING DIGEST AUTH") 118 | if camera.BrokenHttp { 119 | fmt.Println("HIK: WARNING: rawTCP CONFIG VALUE Digest AUTH COMBO IS NOT SUPPORTED!") 120 | fmt.Println(" PLEASE OPEN A GITHUB ISSUE AT https://github.com/toxuin/alarmserver/issues") 121 | fmt.Println(" AND INCLUDE YOUR CAMERA MODEL. THANK YOU!") 122 | } 123 | } 124 | } else { 125 | camera.AuthMethod = Basic 126 | if server.Debug { 127 | fmt.Println("HIK: USING BASIC AUTH") 128 | } 129 | } 130 | 131 | go func() { 132 | defer waitGroup.Done() 133 | done := false 134 | callback := func() { 135 | done = true 136 | } 137 | 138 | for { 139 | if done { 140 | break 141 | } 142 | camera.EventReader.ReadEvents(camera, eventChannel, callback) 143 | } 144 | fmt.Printf("HIK: Closed connection to camera %s\n", camera.Name) 145 | }() 146 | } 147 | 148 | func (server *Server) Start() { 149 | if server.Cameras == nil || len(*server.Cameras) == 0 { 150 | fmt.Println("HIK: Error: no cameras defined") 151 | return 152 | } 153 | 154 | if server.MessageHandler == nil { 155 | fmt.Println("HIK: Message handler is not set for Hikvision cams - that's probably not what you want") 156 | server.MessageHandler = func(cameraName string, eventType string, extra string) { 157 | fmt.Printf("HIK: Lost alarm: %s - %s: %s\n", cameraName, eventType, extra) 158 | } 159 | } 160 | 161 | cameraWaitGroup := sync.WaitGroup{} 162 | eventChannel := make(chan HikEvent, 5) 163 | 164 | // START ALL CAMERA LISTENERS 165 | for _, camera := range *server.Cameras { 166 | server.addCamera(&cameraWaitGroup, &camera, eventChannel) 167 | } 168 | 169 | // START MESSAGE PROCESSOR 170 | go func(camWaitGroup *sync.WaitGroup, channel <-chan HikEvent) { 171 | // WAIT GROUP FOR INDIVIDUAL CAMERAS 172 | defer camWaitGroup.Done() 173 | 174 | // EXTERNAL WAIT GROUP FOR PROCESSES 175 | defer server.WaitGroup.Done() 176 | server.WaitGroup.Add(1) 177 | for { 178 | event := <-channel 179 | go server.MessageHandler(event.Camera.Name, event.Type, event.Message) 180 | } 181 | }(&cameraWaitGroup, eventChannel) 182 | 183 | cameraWaitGroup.Wait() 184 | } 185 | -------------------------------------------------------------------------------- /servers/hikvision/tcpEventReader.go: -------------------------------------------------------------------------------- 1 | package hikvision 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/xml" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/textproto" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | type TcpEventReader struct { 16 | Debug bool 17 | } 18 | 19 | func (eventReader *TcpEventReader) ReadEvents(camera *HikCamera, channel chan<- HikEvent, callback func()) { 20 | // PARSE THE ADDRESS OUTTA CAMERA URL 21 | cameraUrl, err := url.Parse(camera.Url) 22 | if err != nil { 23 | fmt.Printf("HIK-TCP: Error parsing address of camera %s: %s\n", camera.Name, camera.Url) 24 | callback() 25 | return 26 | } 27 | 28 | if cameraUrl.Scheme == "https:" { 29 | fmt.Printf("HIK-TCP: Cannot read events for camera %s: HTTPS support is not implemented\n", camera.Name) 30 | callback() 31 | return 32 | } 33 | 34 | for { 35 | var address, host string 36 | if strings.Contains(cameraUrl.Host, ":") { 37 | address = cameraUrl.Host 38 | host = strings.Split(cameraUrl.Host, ":")[1] 39 | } else { 40 | address = cameraUrl.Host + ":80" 41 | host = cameraUrl.Host 42 | } 43 | 44 | // BASE64-ENCODED VALUE FOR BASIC HTTP AUTH HEADER 45 | basicAuth := base64.StdEncoding.EncodeToString([]byte(camera.Username + ":" + camera.Password)) 46 | 47 | textConn, err := textproto.Dial("tcp", address) 48 | if err != nil { 49 | fmt.Printf("HIK: Error opening TCP connection to camera %s\n", camera.Name) 50 | break 51 | } 52 | 53 | // SEND INITIAL REQUEST 54 | err = textConn.PrintfLine("GET /ISAPI/Event/notification/alertStream HTTP/1.1\r\n"+ 55 | "Host: %s\r\n"+ 56 | "Authorization: Basic %s\r\n\r\n\r\n", 57 | host, 58 | basicAuth, 59 | ) 60 | if err != nil { 61 | fmt.Println("HIK-TCP: Error sending auth request") 62 | fmt.Println(err) 63 | break 64 | } 65 | 66 | // READ AND PARSE HTTP STATUS 67 | httpStatusLine, err := textConn.ReadLine() 68 | if err != nil { 69 | fmt.Println("HIK-TCP: Could not get status header") 70 | fmt.Println(err) 71 | return 72 | } 73 | if !strings.Contains(httpStatusLine, "HTTP/1.1") { 74 | fmt.Printf("HIK-TCP: Bad response from camera %s: %s", camera.Name, httpStatusLine) 75 | return 76 | } 77 | statusParts := strings.SplitN(strings.Split(httpStatusLine, "HTTP/1.1 ")[1], " ", 2) 78 | statusCode := statusParts[0] 79 | statusMessage := statusParts[1] 80 | 81 | // READ HTTP HEADERS 82 | var headers = make(map[string]string) 83 | for { 84 | headerLine, err := textConn.ReadLine() 85 | if err == io.EOF { 86 | // CONNECTION CLOSED 87 | return 88 | } 89 | if strings.Trim(headerLine, " ") == "" { 90 | // END OF HEADERS 91 | break 92 | } 93 | 94 | if eventReader.Debug { 95 | fmt.Println(" " + headerLine) 96 | } 97 | 98 | headerKey := strings.SplitN(headerLine, ": ", 2)[0] 99 | headerValue := strings.SplitN(headerLine, ": ", 2)[1] 100 | headers[headerKey] = headerValue 101 | } 102 | if eventReader.Debug { 103 | fmt.Println("HIK-TCP: HEADERS:") 104 | fmt.Println(headers) 105 | } 106 | 107 | // PRINT ERROR 108 | if statusCode != "200" { 109 | contentLen, err := strconv.Atoi(headers["Content-Length"]) 110 | if err != nil { 111 | fmt.Println("HIK-TCP: Error reading error message, dammit") 112 | return 113 | } 114 | errorBody := make([]byte, contentLen) 115 | _, _ = io.ReadFull(textConn.R, errorBody) 116 | fmt.Printf("HIK-TCP: HTTP Error authenticating with camera %s: %s - %s\n", camera.Name, statusCode, statusMessage) 117 | fmt.Println(string(errorBody)) 118 | return 119 | } 120 | 121 | // READ ACTUAL EVENTS 122 | var eventString string 123 | xmlEvent := XmlEvent{} 124 | for { 125 | line, err := textConn.ReadLine() 126 | if err == io.EOF { // CONNECTION CLOSED 127 | return 128 | } 129 | if err != nil { 130 | fmt.Println("ERROR READING FROM CONNECTION") 131 | fmt.Println(err) 132 | break 133 | } 134 | 135 | if strings.Trim(line, " ") == "" { 136 | // FOUND END OF ONE EVENT IN STREAM 137 | if strings.Contains(eventString, ">HTTP/1.1 ") { 138 | // PART OF THE LAST PACKET IS STUCK TO THE NEXT PACKET 139 | eventString = strings.SplitN(eventString, "HTTP/1.1", 2)[0] 140 | } 141 | 142 | err = xml.Unmarshal([]byte(eventString), &xmlEvent) 143 | xmlEvent.Camera = camera 144 | if err != nil { 145 | fmt.Println("HIK-TCP: Error unmarshalling xml event!") 146 | continue 147 | } 148 | if eventReader.Debug { 149 | log.Printf("%s event: %s (%s - %d), %s", xmlEvent.Camera.Name, xmlEvent.Type, xmlEvent.State, xmlEvent.Id, xmlEvent.Description) 150 | } 151 | 152 | switch xmlEvent.State { 153 | case "active": 154 | if !xmlEvent.Active { 155 | if eventReader.Debug { 156 | fmt.Println("HIK-TCP: SENDING CAMERA EVENT!") 157 | } 158 | event := HikEvent{Camera: camera} 159 | event.Type = xmlEvent.Type 160 | event.Message = xmlEvent.Description 161 | channel <- event 162 | } 163 | xmlEvent.Active = true 164 | case "inactive": 165 | xmlEvent.Active = false 166 | } 167 | 168 | eventString = "" 169 | } else { 170 | eventString += line 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /servers/hisilicon/server.go: -------------------------------------------------------------------------------- 1 | package hisilicon 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | // Converts 0x1704A8C0 to 192.168.4.23 15 | func hexIpToCIDR(hexAddr string) string { 16 | hexAddrStr := fmt.Sprintf("%v", hexAddr)[2:] 17 | ipAddrHexParts := strings.Split(hexAddrStr, "") 18 | 19 | var decParts []string 20 | lastPart := "" 21 | for ind, part := range ipAddrHexParts { 22 | if ind%2 == 0 { 23 | lastPart = part 24 | } else { 25 | decParts = append(decParts, lastPart+part) 26 | } 27 | } 28 | var strParts []string 29 | for _, part := range decParts { 30 | dec, _ := strconv.ParseInt(part, 16, 64) 31 | // PREPEND RESULT TO SLICE 32 | strParts = append(strParts, "") 33 | copy(strParts[1:], strParts) 34 | strParts[0] = strconv.Itoa(int(dec)) 35 | } 36 | ipAddr := fmt.Sprintf("%s", strings.Join(strParts[:], ".")) 37 | return ipAddr 38 | } 39 | 40 | type Server struct { 41 | Debug bool 42 | WaitGroup *sync.WaitGroup 43 | Port string 44 | MessageHandler func(cameraName string, eventType string, extra string) 45 | } 46 | 47 | func (server *Server) handleTcpConnection(conn net.Conn) { 48 | defer conn.Close() 49 | 50 | if server.Debug { 51 | fmt.Printf("HISI: DEVICE CONNECTED: %s\n", conn.RemoteAddr().String()) 52 | } 53 | var buf bytes.Buffer 54 | 55 | _, err := io.Copy(&buf, conn) 56 | if err != nil { 57 | fmt.Printf("HISI: TCP READ ERROR: %s\n", err) 58 | return 59 | } 60 | bufString := buf.String() 61 | resultString := bufString[strings.IndexByte(bufString, '{'):] 62 | if server.Debug { 63 | fmt.Printf("HISI: DEVICE ALERT: %s\n", resultString) 64 | } 65 | 66 | var dataMap map[string]interface{} 67 | 68 | if err := json.Unmarshal([]byte(resultString), &dataMap); err != nil { 69 | fmt.Printf("HISI: JSON PARSE ERROR: %s\n", err) 70 | return 71 | } 72 | if dataMap["Address"] != nil { 73 | hexAddrStr := fmt.Sprintf("%v", dataMap["Address"]) 74 | if len(hexAddrStr) < 2 { 75 | fmt.Printf("HISI: BAD DEVICE ADDRESS: %s\n", hexAddrStr) 76 | return 77 | } 78 | dataMap["ipAddr"] = hexIpToCIDR(hexAddrStr) 79 | } 80 | 81 | jsonBytes, err := json.Marshal(dataMap) 82 | if err != nil { 83 | fmt.Printf("HISI: JSON STRINGIFY ERROR: %s\n", err) 84 | return 85 | } 86 | 87 | if dataMap["SerialID"] == nil { 88 | fmt.Println("HISI: UNKNOWN DEVICE SERIAL ID") 89 | fmt.Println(dataMap) 90 | return 91 | } 92 | 93 | serialId := fmt.Sprintf("%v", dataMap["SerialID"]) 94 | event := fmt.Sprintf("%v", dataMap["Event"]) 95 | 96 | server.MessageHandler(serialId, event, string(jsonBytes)) 97 | } 98 | 99 | func (server *Server) Start() { 100 | if server.Port == "" { 101 | server.Port = "15002" // DEFAULT PORT 102 | } 103 | if server.MessageHandler == nil { 104 | fmt.Println("HISI: Message handler is not set for HiSilicon cams - that's probably not what you want") 105 | server.MessageHandler = func(cameraName string, eventType string, extra string) { 106 | fmt.Printf("HISI: Lost alarm: %s - %s: %s\n", cameraName, eventType, extra) 107 | } 108 | } 109 | 110 | go func() { 111 | defer server.WaitGroup.Done() 112 | server.WaitGroup.Add(1) 113 | 114 | // START TCP SERVER 115 | tcpListener, err := net.Listen("tcp4", ":"+server.Port) 116 | if err != nil { 117 | panic(err) 118 | } 119 | defer tcpListener.Close() 120 | 121 | for { 122 | conn, err := tcpListener.Accept() 123 | if err != nil { 124 | panic(err) 125 | } 126 | go server.handleTcpConnection(conn) 127 | } 128 | }() 129 | } 130 | --------------------------------------------------------------------------------