├── .github ├── FUNDING.yml └── workflows │ ├── binary-release.yml │ ├── dev-docker.yml │ ├── main-docker.yml │ └── readme-dockerhub.yml ├── .gitignore ├── .goreleaser.yaml ├── .version ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets ├── Screenshot 2024-06-19 at 22-54-16 WatchYourPorts - Dashboards - Grafana.png ├── Screenshot01.png ├── Screenshot1.png ├── Screenshot2.png ├── Screenshot3.png ├── database-file.png ├── logo.png └── network-switch1.png ├── cmd └── WatchYourPorts │ └── main.go ├── configs ├── WatchYourPorts.service ├── WatchYourPorts@.service ├── docker-export.sh ├── install.sh └── postinstall.sh ├── docker-compose-auth.yml ├── docker-compose-local.yml ├── docker-compose.yml ├── go.mod ├── go.sum └── internal ├── check ├── error.go └── path.go ├── conf └── getconfig.go ├── influx └── influx.go ├── models └── models.go ├── scan └── scan.go ├── web ├── addr.go ├── api-port.go ├── config.go ├── const-var.go ├── history.go ├── index.go ├── public │ ├── css │ │ └── index.css │ ├── favicon.png │ ├── js │ │ ├── history.js │ │ ├── index.js │ │ ├── scan.js │ │ └── sort.js │ └── version ├── routine-scan.go ├── scanpage.go ├── templates │ ├── config.html │ ├── footer.html │ ├── header.html │ ├── history.html │ ├── index.html │ └── scan.html └── webgui.go └── yaml └── readwrite.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['https://boosty.to/aceberg/donate', 'https://github.com/aceberg#donate'] 4 | -------------------------------------------------------------------------------- /.github/workflows/binary-release.yml: -------------------------------------------------------------------------------- 1 | name: Binary-release 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [created] 7 | 8 | jobs: 9 | generate: 10 | name: Create release-artifacts 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout the repository 14 | uses: actions/checkout@master 15 | 16 | - uses: actions/setup-go@v4 17 | with: 18 | go-version: 'stable' 19 | - run: go version 20 | 21 | - uses: goreleaser/goreleaser-action@v5 22 | with: 23 | distribution: goreleaser 24 | version: latest 25 | args: release --clean 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/dev-docker.yml: -------------------------------------------------------------------------------- 1 | name: Dev-Docker 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | IMAGE_NAME: watchyourports 8 | TAGS: dev 9 | 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Build and Push Docker Image to docker.io 20 | uses: mr-smithers-excellent/docker-build-push@v6 21 | with: 22 | image: ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }} 23 | tags: ${{ env.TAGS }} 24 | registry: docker.io 25 | username: ${{ secrets.DOCKER_USERNAME }} 26 | password: ${{ secrets.DOCKER_PASSWORD }} -------------------------------------------------------------------------------- /.github/workflows/main-docker.yml: -------------------------------------------------------------------------------- 1 | name: Main-Docker 2 | 3 | on: 4 | workflow_dispatch: 5 | # push: 6 | # branches: [ "main" ] 7 | # paths: 8 | # - 'Dockerfile' 9 | # - 'src/**' 10 | 11 | env: 12 | IMAGE_NAME: watchyourports 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Get version tag from env file 23 | uses: c-py/action-dotenv-to-setenv@v5 24 | with: 25 | env-file: .version 26 | 27 | - name: Set up QEMU 28 | uses: docker/setup-qemu-action@v3 29 | 30 | - name: Set up Docker Buildx 31 | id: buildx 32 | uses: docker/setup-buildx-action@v3 33 | 34 | - name: Login to GHCR 35 | uses: docker/login-action@v3 36 | with: 37 | registry: ghcr.io 38 | username: ${{ github.actor }} 39 | password: ${{ secrets.GITHUB_TOKEN }} 40 | 41 | - name: Login to Docker Hub 42 | uses: docker/login-action@v3 43 | with: 44 | username: ${{ secrets.DOCKER_USERNAME }} 45 | password: ${{ secrets.DOCKER_PASSWORD }} 46 | 47 | - name: Build and push 48 | uses: docker/build-push-action@v6 49 | with: 50 | context: . 51 | platforms: linux/amd64,linux/i386,linux/arm/v6,linux/arm/v7,linux/arm64 52 | push: true 53 | tags: | 54 | ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest 55 | ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }} 56 | ghcr.io/${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest 57 | ghcr.io/${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }} -------------------------------------------------------------------------------- /.github/workflows/readme-dockerhub.yml: -------------------------------------------------------------------------------- 1 | name: Readme-DockerHub 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "main" ] 7 | paths: 8 | - 'README.md' 9 | 10 | env: 11 | IMAGE_NAME: watchyourports 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Sync README.md to DockerHub 22 | uses: ms-jpq/sync-dockerhub-readme@v1 23 | with: 24 | username: ${{ secrets.DOCKER_USERNAME }} 25 | password: ${{ secrets.DOCKER_PASSWORD }} 26 | repository: ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }} 27 | readme: "./README.md" 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore tmp 2 | tmp/ -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: WatchYourPorts 2 | builds: 3 | - main: ./cmd/WatchYourPorts/ 4 | binary: WatchYourPorts 5 | id: default 6 | env: [CGO_ENABLED=0] 7 | goos: 8 | - linux 9 | - windows 10 | - darwin 11 | goarch: 12 | - 386 13 | - amd64 14 | - arm 15 | - arm64 16 | goarm: 17 | - "5" 18 | - "6" 19 | - "7" 20 | ignore: 21 | - goos: darwin 22 | goarch: 386 23 | - goos: darwin 24 | goarch: arm 25 | - goos: windows 26 | goarch: 386 27 | - goos: windows 28 | goarch: arm 29 | 30 | nfpms: 31 | - maintainer: aceberg 32 | description: Open ports monitor. Exports data to InfluxDB2/Grafana 33 | homepage: https://github.com/aceberg/WatchYourPorts 34 | license: MIT 35 | section: utils 36 | formats: 37 | - deb 38 | - rpm 39 | - apk 40 | - termux.deb 41 | contents: 42 | - src: ./configs/WatchYourPorts.service 43 | dst: /lib/systemd/system/WatchYourPorts.service 44 | - src: ./configs/WatchYourPorts@.service 45 | dst: /lib/systemd/system/WatchYourPorts@.service 46 | scripts: 47 | postinstall: ./configs/postinstall.sh 48 | 49 | archives: 50 | - files: 51 | - LICENSE 52 | - README.md 53 | - CHANGELOG.md 54 | - src: ./configs/WatchYourPorts.service 55 | dst: WatchYourPorts.service 56 | - src: ./configs/WatchYourPorts@.service 57 | dst: WatchYourPorts@.service 58 | - src: ./configs/install.sh 59 | dst: install.sh 60 | wrap_in_directory: true 61 | format_overrides: 62 | - goos: windows 63 | format: zip 64 | 65 | checksum: 66 | name_template: "checksums.txt" 67 | -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | internal/web/public/version -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Change Log 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.1.2] - 2024-06-29 6 | ### Added 7 | - New themes 8 | - Sort on index page 9 | - Line numbers 10 | - Sort by State 11 | 12 | ### Changed 13 | - Upd to latest Bootstrap and Icons 14 | - Deduplicated JS 15 | 16 | ## [0.1.1] - 2024-06-24 17 | ### Added 18 | - README 19 | - Actions for Docker 20 | 21 | ## [0.0.0] - 2024-06-12 22 | ### Added 23 | - First commit 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | 3 | RUN apk add build-base 4 | COPY . /src 5 | RUN cd /src/cmd/WatchYourPorts/ && CGO_ENABLED=0 go build -o /WatchYourPorts . 6 | 7 | 8 | FROM scratch 9 | 10 | WORKDIR /data/WatchYourPorts 11 | WORKDIR /app 12 | 13 | COPY --from=builder /WatchYourPorts /app/ 14 | 15 | ENTRYPOINT ["./WatchYourPorts"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Andrew Erlikh 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG_NAME=WatchYourPorts 2 | USR_NAME=aceberg 3 | 4 | mod: 5 | rm go.mod || true && \ 6 | rm go.sum || true && \ 7 | go mod init github.com/$(USR_NAME)/$(PKG_NAME) && \ 8 | go mod tidy 9 | 10 | run: 11 | cd cmd/$(PKG_NAME)/ && \ 12 | go run . #-n http://192.168.2.3:8850 13 | 14 | fmt: 15 | go fmt ./... 16 | 17 | lint: 18 | golangci-lint run 19 | golint ./... 20 | 21 | check: fmt lint 22 | 23 | go-build: 24 | cd cmd/$(PKG_NAME)/ && \ 25 | CGO_ENABLED=0 go build -o ../../tmp/$(PKG_NAME) . 26 | 27 | docker-build: 28 | docker build -t $(USR_NAME)/$(PKG_NAME) . 29 | 30 | docker-run: 31 | docker rm $(PKG_NAME) || true 32 | docker run --name $(PKG_NAME) \ 33 | -e "TZ=Asia/Novosibirsk" \ 34 | -v ~/.dockerdata/$(PKG_NAME):/data/$(PKG_NAME) \ 35 | $(USR_NAME)/$(PKG_NAME) 36 | 37 | clean: 38 | rm tmp/$(PKG_NAME) || true 39 | docker rmi -f $(USR_NAME)/$(PKG_NAME) 40 | 41 | dev: docker-build docker-run -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Main-Docker](https://github.com/aceberg/watchyourports/actions/workflows/main-docker.yml/badge.svg)](https://github.com/aceberg/watchyourports/actions/workflows/main-docker.yml) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/aceberg/watchyourports)](https://goreportcard.com/report/github.com/aceberg/watchyourports) 3 | [![Maintainability](https://api.codeclimate.com/v1/badges/e8f67994120fc7936aeb/maintainability)](https://codeclimate.com/github/aceberg/WatchYourPorts/maintainability) 4 | ![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/aceberg/watchyourports) 5 | 6 |

7 | 8 | WatchYourPorts

9 | 10 | Open ports inventory for local servers. Exports data to InfluxDB2/Grafana 11 | 12 | - [Quick start](https://github.com/aceberg/watchyourports#quick-start) 13 | - [Login](https://github.com/aceberg/watchyourports#login) 14 | - [Import ports from Docker](https://github.com/aceberg/watchyourports#import-ports-from-docker) 15 | - [Config](https://github.com/aceberg/watchyourports#config) 16 | - [Options](https://github.com/aceberg/watchyourports#options) 17 | - [Local network only](https://github.com/aceberg/watchyourports#local-network-only) 18 | - [API](https://github.com/aceberg/watchyourports#api) 19 | - [Thanks](https://github.com/aceberg/watchyourports#thanks) 20 | 21 | 22 | ![Screenshot](https://raw.githubusercontent.com/aceberg/WatchYourPorts/main/assets/Screenshot1.png) 23 |
24 | More screenshots 25 | 26 | 27 |
28 | 29 | ## Quick start 30 | 31 | ```sh 32 | docker run --name wyp \ 33 | -e "TZ=Asia/Novosibirsk" \ 34 | -v ~/.dockerdata/WatchYourPorts:/data/WatchYourPorts \ 35 | -p 8853:8853 \ 36 | aceberg/watchyourports 37 | ``` 38 | Or use [docker-compose.yml](docker-compose.yml) 39 | 40 | 41 | ## Auth 42 | You can limit access to WYP with [ForAuth](https://github.com/aceberg/ForAuth). Here is an example: [docker-compose-auth.yml](docker-compose-auth.yml) 43 | Also, SSO tools like Authelia should work. 44 | 45 | ## Import ports from Docker 46 | 1. Run [docker-export.sh](configs/docker-export.sh) on a server, where Docker is installed. `$ADDR` is IP or domain name of the server, without `http(s)://` prefix. It will be used to ping ports. 47 | ```sh 48 | ./docker-export.sh $ADDR 49 | ``` 50 | 2. Paste the output to `hosts.yaml` file in WatchYourPorts config dir 51 | 3. You can add as many servers to `hosts.yaml`, as you want 52 | 53 | 54 | ## Config 55 | 56 | 57 | Configuration can be done through `config.yaml` file or GUI, or environment variables 58 | 59 | | Variable | Description | Default | 60 | | -------- | ----------- | ------- | 61 | | HOST | Listen address | 0.0.0.0 | 62 | | PORT | Port for web GUI | 8853 | 63 | | THEME | Any theme name from https://bootswatch.com in lowcase or [additional](https://github.com/aceberg/aceberg-bootswatch-fork) | grass | 64 | | COLOR | Background color: light or dark | dark | 65 | | TIMEOUT | How often watched ports are scanned (minutes) | 10 | 66 | | HIST_TRIM | How many port states are saved in memory and displayed | 90 | 67 | | TZ | Set your timezone for correct time | "" | 68 | 69 | ### InfluxDB2 config 70 | This config matches Grafana's config for InfluxDB data source 71 | 72 | | Variable | Description | Default | Example | 73 | | -------- | ----------- | ------- | ------- | 74 | | INFLUX_ENABLE | Enable export to InfluxDB2 | false | true | 75 | | INFLUX_SKIP_TLS | Skip TLS Verify | false | true | 76 | | INFLUX_ADDR | Address:port of InfluxDB2 server | | https://192.168.2.3:8086/ | 77 | | INFLUX_BUCKET | InfluxDB2 bucket | | test | 78 | | INFLUX_ORG | InfluxDB2 org | | home | 79 | | INFLUX_TOKEN | Secret token, generated by InfluxDB2 | | | 80 | 81 | ## Options 82 | 83 | | Key | Description | Default | 84 | | -------- | ----------- | ------- | 85 | | -d | Path to config dir | /data/WatchYourPorts | 86 | | -n | Path to local JS and Themes ([node-bootstrap](https://github.com/aceberg/my-dockerfiles/tree/main/node-bootstrap)) | "" | 87 | 88 | ## Local network only 89 | By default, this app pulls themes, icons and fonts from the internet. But, in some cases, it may be useful to have an independent from global network setup. I created a separate [image](https://github.com/aceberg/my-dockerfiles/tree/main/node-bootstrap) with all necessary modules and fonts. 90 | ```sh 91 | docker run --name node-bootstrap \ 92 | -v ~/.dockerdata/icons:/app/icons \ # For local images 93 | -p 8850:8850 \ 94 | aceberg/node-bootstrap 95 | ``` 96 | ```sh 97 | docker run --name wyp \ 98 | -v ~/.dockerdata/WatchYourPorts:/data/WatchYourPorts \ 99 | -p 8853:8853 \ 100 | aceberg/watchyourports -n "http://$YOUR_IP:8850" 101 | ``` 102 | Or use [docker-compose](docker-compose-local.yml) 103 | 104 | ## API 105 | ```http 106 | GET /api/all 107 | ``` 108 | Returns all data about saved addresses in `json`. 109 |
110 | Response example 111 | 112 | ```json 113 | { 114 | "192.168.2.2": { 115 | "Name": "SomeAddrName", 116 | "Addr": "192.168.2.2", 117 | "PortMap": {}, // All saved ports will be here 118 | "Total": 0, 119 | "Watching": 0, 120 | "Online": 0, 121 | "Offline": 0 122 | }, 123 | } 124 | ``` 125 |

126 | 127 | ```http 128 | GET /api/history 129 | ``` 130 | All history data from memory. 131 |
132 | Response example 133 | 134 | ```json 135 | { 136 | "192.168.2.3:8849": { 137 | "Name": "OS", 138 | "Addr": "192.168.2.3", 139 | "Port": 8849, 140 | "PortName": "MiniBoard", 141 | "State": [ 142 | { 143 | "Date": "2024-06-28 22:42:45", 144 | "State": true 145 | }, 146 | { 147 | "Date": "2024-06-28 22:52:45", 148 | "State": true 149 | } 150 | ], 151 | "NowState": true 152 | }, 153 | } 154 | ``` 155 |

156 | 157 | ```http 158 | GET /api/port/:addr 159 | ``` 160 | Returns current PortMap for `addr`. 161 |
162 | Request example 163 | 164 | ```bash 165 | curl http://0.0.0.0:8853/api/port/192.168.2.2 166 | ``` 167 |
168 |
169 | Response example 170 | 171 | ```json 172 | { 173 | "8850": { 174 | "Name": "node-bootstrap", 175 | "Port": 8850, 176 | "State": true, 177 | "Watch": true 178 | }, 179 | "8851": { 180 | "Name": "Exercise Diary", 181 | "Port": 8851, 182 | "State": true, 183 | "Watch": true 184 | }, 185 | 186 | } 187 | ``` 188 |

189 | 190 | ```http 191 | GET /api/port/:addr/:port 192 | ``` 193 | Gets state of one port 194 |
195 | Request example 196 | 197 | ```bash 198 | curl http://0.0.0.0:8853/api/port/192.168.2.2/8844 199 | ``` 200 |
201 |
202 | Response example 203 | 204 | ```json 205 | { 206 | "Name": "git-syr", 207 | "Port": 8844, 208 | "State": true, 209 | "Watch": true 210 | } 211 | ``` 212 |

213 | 214 | ## Thanks 215 | - All go packages listed in [dependencies](https://github.com/aceberg/watchyourports/network/dependencies) 216 | - [Bootstrap](https://getbootstrap.com/) 217 | - Themes: [Free themes for Bootstrap](https://bootswatch.com) 218 | - Favicon and logo: [Flaticon](https://www.flaticon.com/icons/) -------------------------------------------------------------------------------- /assets/Screenshot 2024-06-19 at 22-54-16 WatchYourPorts - Dashboards - Grafana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceberg/WatchYourPorts/fcc75abf455a64e198eed63427f0c3b0c27d941e/assets/Screenshot 2024-06-19 at 22-54-16 WatchYourPorts - Dashboards - Grafana.png -------------------------------------------------------------------------------- /assets/Screenshot01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceberg/WatchYourPorts/fcc75abf455a64e198eed63427f0c3b0c27d941e/assets/Screenshot01.png -------------------------------------------------------------------------------- /assets/Screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceberg/WatchYourPorts/fcc75abf455a64e198eed63427f0c3b0c27d941e/assets/Screenshot1.png -------------------------------------------------------------------------------- /assets/Screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceberg/WatchYourPorts/fcc75abf455a64e198eed63427f0c3b0c27d941e/assets/Screenshot2.png -------------------------------------------------------------------------------- /assets/Screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceberg/WatchYourPorts/fcc75abf455a64e198eed63427f0c3b0c27d941e/assets/Screenshot3.png -------------------------------------------------------------------------------- /assets/database-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceberg/WatchYourPorts/fcc75abf455a64e198eed63427f0c3b0c27d941e/assets/database-file.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceberg/WatchYourPorts/fcc75abf455a64e198eed63427f0c3b0c27d941e/assets/logo.png -------------------------------------------------------------------------------- /assets/network-switch1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceberg/WatchYourPorts/fcc75abf455a64e198eed63427f0c3b0c27d941e/assets/network-switch1.png -------------------------------------------------------------------------------- /cmd/WatchYourPorts/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | 6 | _ "time/tzdata" 7 | 8 | "github.com/aceberg/WatchYourPorts/internal/web" 9 | ) 10 | 11 | const dirPath = "/data/WatchYourPorts" 12 | const nodePath = "" 13 | 14 | func main() { 15 | dirPtr := flag.String("d", dirPath, "Path to config dir") 16 | nodePtr := flag.String("n", nodePath, "Path to node modules") 17 | flag.Parse() 18 | 19 | web.Gui(*dirPtr, *nodePtr) // webgui.go 20 | } 21 | -------------------------------------------------------------------------------- /configs/WatchYourPorts.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=WatchYourPorts 3 | Documentation=https://github.com/aceberg/WatchYourPorts 4 | After=network-online.target 5 | Wants=network-online.target 6 | 7 | [Service] 8 | ExecStart=/usr/bin/WatchYourPorts -d /etc/WatchYourPorts/ 9 | Restart=on-failure 10 | 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /configs/WatchYourPorts@.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=WatchYourPorts 3 | Documentation=https://github.com/aceberg/WatchYourPorts 4 | After=network-online.target 5 | Wants=network-online.target 6 | 7 | [Service] 8 | User=%i 9 | ExecStart=/usr/bin/WatchYourPorts -d /home/%i/.config/WatchYourPorts/ 10 | Restart=on-failure 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /configs/docker-export.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script generates a WatchYourPorts config from Docker containers. 4 | 5 | # HOW TO USE 6 | # 1. Run this script on a server, where Docker is installed: 7 | # ./docker-export.sh $ADDR 8 | # $ADDR is IP or domain name of the server, without http(s):// prefix 9 | # It will be used to ping ports 10 | # 2. Paste the output to hosts.yaml file in WYP config dir 11 | # 3. You can add as many servers to hosts.yaml, as you want 12 | 13 | docker ps -a --format "{{.Names}}">/tmp/wyp-docker.txt 14 | 15 | echo $1':' 16 | echo ' name:' 17 | echo ' addr: '$1 18 | echo ' portmap:' 19 | 20 | while read NAME; do 21 | PORT=`docker inspect $NAME | grep HostPort | sed '1!d;s/"HostPort": //;s/,//;s/"//g'` 22 | 23 | if [ ${#PORT} -ge 1 ]; then 24 | echo ' '$PORT':' 25 | echo ' name: '$NAME 26 | echo ' port:'$PORT 27 | echo ' state: false' 28 | echo ' watch: true' 29 | fi 30 | done `; 17 | } 18 | 19 | let html = ` 20 | 21 | ${i}. 22 | ${hist.Name} 23 | ${hist.Addr} 24 | ${hist.Port} 25 | ${hist.PortName} 26 | ${allState} 27 | `; 28 | 29 | return html; 30 | } 31 | 32 | async function loadHistory() { 33 | 34 | let url = '/api/history'; 35 | let histMap = await (await fetch(url)).json(); 36 | if (histMap != null) { 37 | histArray = Object.values(histMap); 38 | } 39 | 40 | displayArrayData(histArray); 41 | } 42 | 43 | function sortBy(field) { 44 | sortByAny(histArray, field); 45 | } -------------------------------------------------------------------------------- /internal/web/public/js/index.js: -------------------------------------------------------------------------------- 1 | var addrsArray = {}; 2 | 3 | loadAddrs(); 4 | 5 | function createHTML(addr, i) { 6 | let off = ''; 7 | 8 | if (addr.Offline == 0) { 9 | off = `0`; 10 | } else { 11 | off = `${addr.Offline}`; 12 | } 13 | 14 | let html = ` 15 | 16 | ${i}. 17 | 18 | ${addr.Name} 19 | 20 | 21 | ${addr.Addr} 22 | 23 | ${addr.Total} 24 | ${addr.Watching} 25 | ${addr.Online} 26 | ${off} 27 | 28 | `; 29 | 30 | return html; 31 | } 32 | 33 | async function loadAddrs() { 34 | 35 | let url = '/api/all'; 36 | let addrsMap = await (await fetch(url)).json(); 37 | if (addrsMap != null) { 38 | addrsArray = Object.values(addrsMap); 39 | } 40 | 41 | displayArrayData(addrsArray); 42 | } 43 | 44 | function sortBy(field) { 45 | sortByAny(addrsArray, field); 46 | } -------------------------------------------------------------------------------- /internal/web/public/js/scan.js: -------------------------------------------------------------------------------- 1 | var addr = ''; 2 | var stop = false; 3 | var portMap = {}; 4 | var portArray = []; 5 | 6 | // Run, when page loadad 7 | window.addEventListener('load', function() { 8 | loadSavedPorts(); 9 | }); 10 | 11 | function delRow(id) { 12 | document.getElementById(id).innerHTML = ""; 13 | } 14 | 15 | function stopScan() { 16 | stop = true; 17 | } 18 | 19 | async function scanAddr() { 20 | let begin = document.getElementById("begin").value; 21 | let end = document.getElementById("end").value; 22 | let savedPorts = []; 23 | 24 | if (begin == "") { 25 | begin = 1 26 | } 27 | if (end == "") { 28 | end = 65535 29 | } 30 | let port = {}; 31 | stop = false; 32 | 33 | if (portMap != null) { 34 | savedPorts = Object.keys(portMap); 35 | } 36 | // console.log("Saved ports:", savedPorts); 37 | 38 | document.getElementById('stopBtn').style.visibility = "visible"; 39 | 40 | for (let i = begin ; i <= end; i++) { 41 | 42 | if (stop) { 43 | break; 44 | } 45 | 46 | let url = '/api/port/'+addr+'/'+i; 47 | port = await (await fetch(url)).json(); 48 | 49 | document.getElementById("curPort").innerHTML = "Scanning port "+i; 50 | 51 | if ((port.State) && (!savedPorts.includes(port.Port.toString()))) { 52 | html = createHTML(port, '', true); 53 | document.getElementById('tBody').insertAdjacentHTML('afterbegin', html); 54 | } 55 | } 56 | 57 | document.getElementById('stopBtn').style.visibility = "hidden"; 58 | } 59 | 60 | function createHTML(port, i, found) { 61 | let state = ``; 62 | let checked = ``; 63 | let sup = ``; 64 | 65 | if (found) { 66 | sup = ` new`; 67 | } 68 | 69 | if (port.Watch) { 70 | checked = `checked`; 71 | 72 | if (port.State) { 73 | state = ``; 74 | } else { 75 | state = ``; 76 | } 77 | } else { 78 | state = ``; 79 | } 80 | 81 | let html = ` 82 | 83 | ${i}. 84 | 85 | 86 | 87 | 88 | ${port.Port}${sup} 89 | 90 | 91 | 92 | 93 | ${state} 94 | 95 | 96 |
97 | 98 |
99 | 100 | 101 | 102 | 103 | 104 | 105 | `; 106 | 107 | return html; 108 | } 109 | 110 | async function loadSavedPorts() { 111 | 112 | addr = document.getElementById("pageAddr").value; 113 | 114 | let url = '/api/port/'+addr; 115 | portMap = await (await fetch(url)).json(); 116 | if (portMap != null) { 117 | portArray = Object.values(portMap); 118 | } 119 | 120 | displayArrayData(portArray); // sort.js 121 | } 122 | 123 | function sortBy(field) { 124 | sortByAny(portArray, field); // sort.js 125 | } -------------------------------------------------------------------------------- /internal/web/public/js/sort.js: -------------------------------------------------------------------------------- 1 | var oldField = ''; 2 | 3 | function displayArrayData(someArray) { 4 | document.getElementById('tBody').innerHTML = ""; 5 | 6 | let i = 0; 7 | for (let item of someArray){ 8 | i = i + 1; 9 | html = createHTML(item, i); 10 | document.getElementById('tBody').insertAdjacentHTML('beforeend', html); 11 | } 12 | } 13 | 14 | function sortByAny(someArray, field) { 15 | // console.log("Field =", field); 16 | 17 | if (field != oldField) { 18 | someArray.sort(byFieldDown(field)); 19 | oldField = field; 20 | } else { 21 | someArray.sort(byFieldUp(field)); 22 | oldField = ''; 23 | } 24 | 25 | if (field == 'State') { 26 | someArray.sort(byFieldUp('Watch')); 27 | } 28 | 29 | displayArrayData(someArray); 30 | } 31 | 32 | function byFieldUp(fieldName){ 33 | return (a, b) => a[fieldName] < b[fieldName] ? 1 : -1; 34 | } 35 | 36 | function byFieldDown(fieldName){ 37 | return (a, b) => a[fieldName] > b[fieldName] ? 1 : -1; 38 | } -------------------------------------------------------------------------------- /internal/web/public/version: -------------------------------------------------------------------------------- 1 | VERSION=0.1.2 -------------------------------------------------------------------------------- /internal/web/routine-scan.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "log/slog" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/aceberg/WatchYourPorts/internal/influx" 9 | "github.com/aceberg/WatchYourPorts/internal/models" 10 | "github.com/aceberg/WatchYourPorts/internal/scan" 11 | ) 12 | 13 | func routineScan(quit chan bool) { 14 | var lastDate time.Time 15 | 16 | if appConfig.Timeout == 0 { 17 | appConfig.Timeout = 10 18 | } 19 | 20 | for { 21 | select { 22 | case <-quit: 23 | return 24 | default: 25 | nowDate := time.Now() 26 | plusDate := lastDate.Add(time.Duration(appConfig.Timeout) * time.Minute) 27 | 28 | if nowDate.After(plusDate) { 29 | 30 | startScan() 31 | 32 | lastDate = time.Now() 33 | } 34 | 35 | time.Sleep(time.Duration(1) * time.Second) 36 | } 37 | } 38 | } 39 | 40 | func startScan() { 41 | var changed bool 42 | 43 | slog.Info("port scan started") 44 | 45 | for _, addr := range allAddrs { 46 | changed = false 47 | tmpPortMap := addr.PortMap 48 | 49 | for _, port := range addr.PortMap { 50 | 51 | if port.Watch { 52 | port.State = scan.IsOpen(addr.Addr, port.Port) 53 | tmpPortMap[port.Port] = port 54 | changed = true 55 | 56 | // History 57 | portStr := strconv.Itoa(port.Port) 58 | oneHist := histAll[addr.Addr+":"+portStr] 59 | oneHist.Name = addr.Name 60 | oneHist.Addr = addr.Addr 61 | oneHist.Port = port.Port 62 | oneHist.PortName = port.Name 63 | oneHist.NowState = port.State 64 | oneHist.State = append(oneHist.State, 65 | models.HistState{ 66 | Date: time.Now().Format("2006-01-02 15:04:05"), 67 | State: port.State, 68 | }, 69 | ) 70 | l := len(oneHist.State) 71 | if l > appConfig.HistTrim { 72 | 73 | oneHist.State = oneHist.State[l-appConfig.HistTrim : l] 74 | } 75 | 76 | histAll[addr.Addr+":"+portStr] = oneHist 77 | 78 | if appConfig.InfluxEnable { 79 | influx.Add(appConfig, oneHist) 80 | } 81 | } 82 | } 83 | 84 | if changed { 85 | tmpAddr := addr 86 | tmpAddr.PortMap = tmpPortMap 87 | allAddrs[addr.Addr] = tmpAddr 88 | } 89 | } 90 | 91 | slog.Info("port scan done!") 92 | } 93 | -------------------------------------------------------------------------------- /internal/web/scanpage.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "net/http" 5 | "slices" 6 | "strconv" 7 | 8 | "github.com/gin-gonic/gin" 9 | 10 | "github.com/aceberg/WatchYourPorts/internal/models" 11 | "github.com/aceberg/WatchYourPorts/internal/yaml" 12 | ) 13 | 14 | func scanHandler(c *gin.Context) { 15 | var guiData models.GuiData 16 | guiData.Config = appConfig 17 | 18 | addr, ok := c.GetQuery("addr") 19 | if ok { 20 | guiData.OneAddr = allAddrs[addr] 21 | } 22 | 23 | c.HTML(http.StatusOK, "header.html", guiData) 24 | c.HTML(http.StatusOK, "scan.html", guiData) 25 | } 26 | 27 | func scanSaveHandler(c *gin.Context) { 28 | 29 | addr := c.PostForm("addr") 30 | 31 | names := c.PostFormArray("name") 32 | ports := c.PostFormArray("port") 33 | states := c.PostFormArray("state") 34 | watchs := c.PostFormArray("watch") 35 | 36 | tmpMap := make(map[int]models.PortItem) 37 | onePort := models.PortItem{} 38 | 39 | for i, port := range ports { 40 | if slices.Contains(watchs, port) { 41 | onePort.Watch = true 42 | } else { 43 | onePort.Watch = false 44 | } 45 | 46 | onePort.Name = names[i] 47 | onePort.Port, _ = strconv.Atoi(port) 48 | onePort.State, _ = strconv.ParseBool(states[i]) 49 | 50 | tmpMap[onePort.Port] = onePort 51 | } 52 | 53 | oneAddr := allAddrs[addr] 54 | oneAddr.PortMap = tmpMap 55 | allAddrs[addr] = oneAddr 56 | 57 | // log.Println("SAVEALL:", allAddrs) 58 | 59 | yaml.Write(appConfig.YamlPath, allAddrs) 60 | 61 | c.Redirect(http.StatusFound, "/scan/?addr="+addr) 62 | } 63 | -------------------------------------------------------------------------------- /internal/web/templates/config.html: -------------------------------------------------------------------------------- 1 | {{ define "config.html" }} 2 | 3 | 4 |
5 |
6 |
7 |
8 |
Config
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
Host
Port
Theme
Color mode
Timeout (minutes)
Trim History
51 |
52 |
53 |
54 |
Version
55 |
56 | {{ .Version }} 57 |
58 |
59 |
60 |
61 |
62 |
InfluxDB2 config
63 |
64 | 65 | 66 | 67 | 68 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 105 | 106 | 107 | 108 | 109 | 110 | 111 |
Enable 69 |
70 | {{ if .Config.InfluxEnable }} 71 | 72 | {{ else }} 73 | 74 | {{ end }} 75 |
76 |
Address
Token
Org
Bucket
Skip TLS verify 97 |
98 | {{ if .Config.InfluxSkipTLS }} 99 | 100 | {{ else }} 101 | 102 | {{ end }} 103 |
104 |
112 |
113 |
114 |
115 |
About
116 |
117 |

● After changing Host or Port the app must be restarted

118 |

Timeout (minutes) - how often watched ports are scanned

119 |

Trim History - how many port states are saved in memory and displayed on the History page

120 |

● If you find this app useful, please, donate

121 |

● Commission your own app (Golang, HTML/JS). Contact and prices here

122 |
123 |
124 |
125 |
126 | 127 | 128 | {{ template "footer.html" }} 129 | {{ end }} -------------------------------------------------------------------------------- /internal/web/templates/footer.html: -------------------------------------------------------------------------------- 1 | {{ define "footer.html"}} 2 | 3 | 4 | {{ end }} -------------------------------------------------------------------------------- /internal/web/templates/header.html: -------------------------------------------------------------------------------- 1 | {{ define "header.html"}} 2 | 3 | 4 | 5 | 6 | WatchYourPorts 7 | 8 | 9 | 10 | 11 | {{ if eq .Config.NodePath "" }} 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{ else }} 19 | 20 | 21 | 22 | {{ end }} 23 | 24 | 25 | 57 | {{ end }} -------------------------------------------------------------------------------- /internal/web/templates/history.html: -------------------------------------------------------------------------------- 1 | {{ define "history.html" }} 2 | 3 | 4 | 5 | 6 |
7 |
8 |
9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
Name Addr Port PortName State
23 |
24 |
25 |
26 |
27 |
28 | 29 | {{ template "footer.html" }} 30 | {{ end }} -------------------------------------------------------------------------------- /internal/web/templates/index.html: -------------------------------------------------------------------------------- 1 | {{ define "index.html" }} 2 | 3 | 4 | 5 | 6 |
7 |
8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Name Addr Total Watching Online Offline
33 |
34 |
35 |
36 |
37 |
38 | 39 | {{ template "footer.html" }} 40 | {{ end }} -------------------------------------------------------------------------------- /internal/web/templates/scan.html: -------------------------------------------------------------------------------- 1 | {{ define "scan.html" }} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 50 | 51 |
52 |
53 |
54 |

{{ .OneAddr.Name }}: {{ .OneAddr.Addr }} 55 | 58 |

59 |
60 |
61 |
62 | 63 | 64 | 65 | 66 | 67 |
68 | 72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | 80 |
81 |
82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 |
Name Port State Watch Del
93 |
94 |
95 | 96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | 104 | {{ template "footer.html" }} 105 | {{ end }} -------------------------------------------------------------------------------- /internal/web/webgui.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "html/template" 5 | "log/slog" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | 10 | "github.com/aceberg/WatchYourPorts/internal/check" 11 | "github.com/aceberg/WatchYourPorts/internal/conf" 12 | "github.com/aceberg/WatchYourPorts/internal/models" 13 | "github.com/aceberg/WatchYourPorts/internal/yaml" 14 | ) 15 | 16 | // Gui - start web server 17 | func Gui(dirPath, nodePath string) { 18 | 19 | confPath := dirPath + "/config.yaml" 20 | check.Path(confPath) 21 | 22 | appConfig = conf.Get(confPath) 23 | 24 | appConfig.DirPath = dirPath 25 | appConfig.YamlPath = dirPath + "/hosts.yaml" 26 | appConfig.ConfPath = confPath 27 | appConfig.NodePath = nodePath 28 | 29 | slog.Info("config", "path", appConfig.DirPath) 30 | 31 | allAddrs = yaml.Read(appConfig.YamlPath) 32 | 33 | histAll = make(map[string]models.HistData) 34 | 35 | quitScan = make(chan bool) 36 | go routineScan(quitScan) 37 | 38 | address := appConfig.Host + ":" + appConfig.Port 39 | 40 | slog.Info("=================================== ") 41 | slog.Info("Web GUI at http://" + address) 42 | slog.Info("=================================== ") 43 | 44 | gin.SetMode(gin.ReleaseMode) 45 | router := gin.Default() 46 | 47 | templ := template.Must(template.New("").ParseFS(templFS, "templates/*")) 48 | router.SetHTMLTemplate(templ) // templates 49 | 50 | router.StaticFS("/fs/", http.FS(pubFS)) // public 51 | 52 | router.GET("/api/all", apiAllAddrs) // api-port.go 53 | router.GET("/api/history", apiHistory) // api-port.go 54 | router.GET("/api/port/:addr", apiAddrPortMap) // api-port.go 55 | router.GET("/api/port/:addr/:port", apiPortScan) // api-port.go 56 | 57 | router.GET("/", indexHandler) // index.go 58 | router.GET("/config/", configHandler) // config.go 59 | router.GET("/history/", historyHandler) // history.go 60 | router.GET("/scan/", scanHandler) // scanpage.go 61 | 62 | router.POST("/addr_add/", addHandler) // addr.go 63 | router.POST("/addr_del/", delHandler) // addr.go 64 | router.POST("/addr_save/", renameHandler) // addr.go 65 | router.POST("/config/", saveConfigHandler) // config.go 66 | router.POST("/config_influx/", saveInfluxHandler) // config.go 67 | router.POST("/scan_save/", scanSaveHandler) // scanpage.go 68 | 69 | err := router.Run(address) 70 | check.IfError(err) 71 | } 72 | -------------------------------------------------------------------------------- /internal/yaml/readwrite.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "gopkg.in/yaml.v3" 8 | 9 | "github.com/aceberg/WatchYourPorts/internal/check" 10 | "github.com/aceberg/WatchYourPorts/internal/models" 11 | ) 12 | 13 | // Read - read .yaml file to struct 14 | func Read(path string) map[string]models.AddrToScan { 15 | 16 | file, err := os.ReadFile(path) 17 | check.IfError(err) 18 | 19 | plans := make(map[string]models.AddrToScan) 20 | 21 | err = yaml.Unmarshal(file, &plans) 22 | check.IfError(err) 23 | 24 | return plans 25 | } 26 | 27 | // Write - write struct to .yaml file 28 | func Write(path string, plans map[string]models.AddrToScan) { 29 | 30 | yamlData, err := yaml.Marshal(&plans) 31 | check.IfError(err) 32 | 33 | err = os.WriteFile(path, yamlData, 0644) 34 | check.IfError(err) 35 | 36 | slog.Info("writing new plan file to " + path) 37 | } 38 | --------------------------------------------------------------------------------