├── .github └── workflows │ ├── docker.yaml │ └── hydrun.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── Hydrunfile ├── LICENSE ├── Makefile ├── README.md ├── api └── proto │ └── v1 │ ├── metadata.proto │ ├── node_and_port_scan.proto │ └── node_wake.proto ├── assets ├── demo.webp ├── initial.png ├── liwasc Icon.afdesign ├── liwasc Logo.afdesign └── setup.png ├── cmd ├── liwasc-backend │ └── main.go └── liwasc-frontend │ └── main.go ├── configs ├── sql-migrate │ ├── node_and_port_scan.yaml │ └── node_wake.yaml └── sqlboiler │ ├── mac2vendor.toml │ ├── node_and_port_scan.toml │ └── node_wake.toml ├── db └── sqlite │ └── migrations │ ├── node_and_port_scan │ └── 1616878410007.sql │ └── node_wake │ └── 1616878455601.sql ├── examples └── liwasc-backend-config.yaml ├── go.mod ├── go.sum ├── pkg ├── api │ └── proto │ │ └── v1 │ │ ├── metadata.pb.go │ │ ├── node_and_port_scan.pb.go │ │ └── node_wake.pb.go ├── components │ ├── about_modal.go │ ├── controlled.go │ ├── data_shell.go │ ├── expandable_section.go │ ├── form_group.go │ ├── home.go │ ├── inspector.go │ ├── mobile_metadata.go │ ├── modal.go │ ├── navbar.go │ ├── node_table.go │ ├── notification_drawer.go │ ├── port_list.go │ ├── port_selection_list.go │ ├── progress_button.go │ ├── property.go │ ├── settings_form.go │ ├── setup_form.go │ ├── setup_shell.go │ ├── status.go │ └── toolbar.go ├── db │ └── sqlite │ │ ├── migrations │ │ ├── node_and_port_scan │ │ │ └── migrations.go │ │ └── node_wake │ │ │ └── migrations.go │ │ └── models │ │ ├── mac2vendor │ │ ├── boil_main_test.go │ │ ├── boil_queries.go │ │ ├── boil_queries_test.go │ │ ├── boil_suites_test.go │ │ ├── boil_table_names.go │ │ ├── boil_types.go │ │ ├── sqlite3_main_test.go │ │ ├── vendordb.go │ │ └── vendordb_test.go │ │ ├── node_and_port_scan │ │ ├── boil_main_test.go │ │ ├── boil_queries.go │ │ ├── boil_queries_test.go │ │ ├── boil_suites_test.go │ │ ├── boil_table_names.go │ │ ├── boil_types.go │ │ ├── gorp_migrations.go │ │ ├── gorp_migrations_test.go │ │ ├── node_scans.go │ │ ├── node_scans_test.go │ │ ├── nodes.go │ │ ├── nodes_test.go │ │ ├── port_scans.go │ │ ├── port_scans_test.go │ │ ├── ports.go │ │ ├── ports_test.go │ │ └── sqlite3_main_test.go │ │ └── node_wake │ │ ├── boil_main_test.go │ │ ├── boil_queries.go │ │ ├── boil_queries_test.go │ │ ├── boil_suites_test.go │ │ ├── boil_table_names.go │ │ ├── boil_types.go │ │ ├── gorp_migrations.go │ │ ├── gorp_migrations_test.go │ │ ├── node_wakes.go │ │ ├── node_wakes_test.go │ │ └── sqlite3_main_test.go ├── networking │ └── interfaceinspector.go ├── persisters │ ├── external_source.go │ ├── mac2vendor.go │ ├── node_and_port_scan.go │ ├── node_wake.go │ ├── ports2packets.go │ ├── service_names_port_numbers.go │ └── sqlite.go ├── providers │ ├── data_provider.go │ ├── identity_provider.go │ └── setup_provider.go ├── scanners │ ├── node.go │ ├── ports.go │ └── wake.go ├── servers │ └── liwasc.go ├── services │ ├── metadata.go │ ├── node_and_port_scan.go │ └── node_wake.go ├── validators │ ├── context.go │ └── oidc.go └── wakers │ └── wake_on_lan.go └── web ├── icon.png ├── index.css └── logo.svg /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Docker CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build-linux: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Set up metadata 15 | id: meta 16 | uses: docker/metadata-action@v3 17 | with: 18 | images: pojntfx/liwasc-backend 19 | tags: type=semver,pattern={{version}} 20 | - name: Set up QEMU 21 | uses: docker/setup-qemu-action@v1 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v1 24 | - name: Login to Docker Hub 25 | uses: docker/login-action@v1 26 | with: 27 | username: ${{ secrets.DOCKER_USERNAME }} 28 | password: ${{ secrets.DOCKER_TOKEN }} 29 | - name: Build image 30 | uses: docker/build-push-action@v2 31 | with: 32 | context: . 33 | file: ./Dockerfile 34 | platforms: linux/amd64,linux/arm64/v8,linux/arm/v7 35 | push: false 36 | tags: pojntfx/liwasc-backend:unstable 37 | labels: ${{ steps.meta.outputs.labels }} 38 | - name: Push pre-release image to Docker Hub 39 | if: ${{ github.ref == 'refs/heads/main' }} 40 | uses: docker/build-push-action@v2 41 | with: 42 | context: . 43 | file: ./Dockerfile 44 | platforms: linux/amd64,linux/arm64/v8,linux/arm/v7 45 | push: true 46 | tags: pojntfx/liwasc-backend:unstable 47 | labels: ${{ steps.meta.outputs.labels }} 48 | - name: Push release image to Docker Hub 49 | if: startsWith(github.ref, 'refs/tags/v') 50 | uses: docker/build-push-action@v2 51 | with: 52 | context: . 53 | file: ./Dockerfile 54 | platforms: linux/amd64,linux/arm64/v8,linux/arm/v7 55 | push: true 56 | tags: ${{ steps.meta.outputs.tags }} 57 | labels: ${{ steps.meta.outputs.labels }} 58 | -------------------------------------------------------------------------------- /.github/workflows/hydrun.yaml: -------------------------------------------------------------------------------- 1 | name: hydrun CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build-linux: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Set up QEMU 15 | uses: docker/setup-qemu-action@v1 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v1 18 | - name: Set up hydrun 19 | run: | 20 | curl -L -o /tmp/hydrun "https://github.com/pojntfx/hydrun/releases/latest/download/hydrun.linux-$(uname -m)" 21 | sudo install /tmp/hydrun /usr/local/bin 22 | - name: Build backend with hydrun 23 | run: hydrun -a amd64,arm64,arm/v7 ./Hydrunfile 24 | - name: Build frontend with hydrun 25 | run: hydrun "./Hydrunfile frontend" 26 | - name: Publish pre-release to GitHub releases 27 | if: ${{ github.ref == 'refs/heads/main' }} 28 | uses: marvinpinto/action-automatic-releases@latest 29 | with: 30 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 31 | automatic_release_tag: unstable 32 | prerelease: true 33 | files: | 34 | out/release/liwasc-backend/* 35 | out/release/liwasc-frontend/* 36 | - name: Publish release to GitHub releases 37 | if: startsWith(github.ref, 'refs/tags/v') 38 | uses: marvinpinto/action-automatic-releases@latest 39 | with: 40 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 41 | prerelease: false 42 | files: | 43 | out/release/liwasc-backend/* 44 | out/release/liwasc-frontend/* 45 | - name: Publish release to GitHub pages 46 | if: startsWith(github.ref, 'refs/tags/v') 47 | uses: JamesIves/github-pages-deploy-action@4.1.0 48 | with: 49 | branch: gh-pages 50 | folder: out/release/liwasc-frontend-github-pages 51 | git-config-name: GitHub Pages Bot 52 | git-config-email: bot@example.com 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | web/*.wasm 2 | out 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at felicitas@pojtinger.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build container 2 | FROM debian AS build 3 | 4 | # Setup environment 5 | RUN mkdir -p /data 6 | WORKDIR /data 7 | 8 | # Build the release 9 | COPY . . 10 | RUN ./Hydrunfile 11 | 12 | # Extract the release 13 | RUN mkdir -p /out 14 | RUN cp out/release/liwasc-backend/liwasc-backend.linux-$(uname -m) /out/liwasc-backend 15 | 16 | # Release container 17 | FROM debian 18 | 19 | # Add certificates 20 | RUN apt update 21 | RUN apt install -y ca-certificates 22 | 23 | # Add the release 24 | COPY --from=build /out/liwasc-backend /usr/local/bin/liwasc-backend 25 | 26 | CMD /usr/local/bin/liwasc-backend 27 | -------------------------------------------------------------------------------- /Hydrunfile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install native dependencies 4 | apt update 5 | apt install -y curl make sudo build-essential sqlite3 protobuf-compiler libpcap-dev 6 | 7 | # Fix certificate authorities on armv7 8 | export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt 9 | 10 | # Install Go 11 | VERSION=1.16.4 12 | FILE=/tmp/go.tar.gz 13 | if [ "$(uname -m)" = 'x86_64' ]; then 14 | curl -L -o ${FILE} https://golang.org/dl/go${VERSION}.linux-amd64.tar.gz 15 | elif [ "$(uname -m)" = 'armv7l' ]; then 16 | curl -L -o ${FILE} https://golang.org/dl/go${VERSION}.linux-armv6l.tar.gz 17 | else 18 | curl -L -o ${FILE} https://golang.org/dl/go${VERSION}.linux-arm64.tar.gz 19 | fi 20 | tar -C /usr/local -xzf ${FILE} 21 | export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin 22 | 23 | # Install dependencies 24 | USER=root make depend 25 | 26 | # Make release 27 | if [ "$1" = 'frontend' ]; then 28 | make release-frontend release-frontend-github-pages 29 | else 30 | make release-backend 31 | fi 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | backend: 4 | go build -o out/liwasc-backend/liwasc-backend cmd/liwasc-backend/main.go 5 | 6 | frontend: 7 | rm -f web/app.wasm 8 | GOOS=js GOARCH=wasm go build -o web/app.wasm cmd/liwasc-frontend/main.go 9 | go build -o /tmp/liwasc-frontend-build cmd/liwasc-frontend/main.go 10 | rm -rf out/liwasc-frontend 11 | /tmp/liwasc-frontend-build -build 12 | cp -r web/* out/liwasc-frontend/web 13 | 14 | build: backend frontend 15 | 16 | release-backend: 17 | CGO_ENABLED=1 go build -ldflags="-extldflags=-static" -tags sqlite_omit_load_extension,netgo -o out/release/liwasc-backend/liwasc-backend.linux-$$(uname -m) cmd/liwasc-backend/main.go 18 | 19 | release-frontend: frontend 20 | rm -rf out/release/liwasc-frontend 21 | mkdir -p out/release/liwasc-frontend 22 | cd out/liwasc-frontend && tar -czvf ../release/liwasc-frontend/liwasc-frontend.tar.gz . 23 | 24 | release-frontend-github-pages: frontend 25 | rm -rf out/release/liwasc-frontend-github-pages 26 | mkdir -p out/release/liwasc-frontend-github-pages 27 | /tmp/liwasc-frontend-build -build -path liwasc -out out/release/liwasc-frontend-github-pages 28 | cp -r web/* out/release/liwasc-frontend-github-pages/web 29 | 30 | release: release-backend release-frontend release-frontend-github-pages 31 | 32 | install: release-backend 33 | sudo install out/release/liwasc-backend/liwasc-backend.linux-$$(uname -m) /usr/local/bin/liwasc-backend 34 | sudo setcap cap_net_raw+ep /usr/local/bin/liwasc-backend 35 | 36 | dev: 37 | while [ -z "$$BACKEND_PID" ] || [ -n "$$(inotifywait -q -r -e modify pkg cmd web/*.css)" ]; do\ 38 | $(MAKE);\ 39 | kill -9 $$BACKEND_PID 2>/dev/null 1>&2;\ 40 | kill -9 $$FRONTEND_PID 2>/dev/null 1>&2;\ 41 | wait $$BACKEND_PID $$FRONTEND_PID;\ 42 | sudo setcap cap_net_raw+ep out/liwasc-backend/liwasc-backend;\ 43 | out/liwasc-backend/liwasc-backend & export BACKEND_PID="$$!";\ 44 | /tmp/liwasc-frontend-build -serve & export FRONTEND_PID="$$!";\ 45 | done 46 | 47 | clean: 48 | rm -rf out 49 | rm -rf pkg/api/proto/v1 50 | rm -rf pkg/db 51 | rm -rf ~/.local/share/liwasc 52 | 53 | depend: 54 | # Setup working directories 55 | mkdir -p out/tmp/etc/liwasc out/tmp/var/lib/liwasc 56 | # Setup external databases 57 | curl -L -o out/tmp/etc/liwasc/oui-database.sqlite https://mac2vendor.com/download/oui-database.sqlite 58 | curl -L -o out/tmp/etc/liwasc/service-names-port-numbers.csv https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.csv 59 | curl -L -o out/tmp/etc/liwasc/ports2packets.csv https://github.com/pojntfx/ports2packets/releases/download/weekly-csv/ports2packets.csv 60 | # Setup CLIs 61 | GO111MODULE=on go get github.com/volatiletech/sqlboiler/v4@latest 62 | GO111MODULE=on go get github.com/volatiletech/sqlboiler-sqlite3@latest 63 | GO111MODULE=on go get github.com/golang/protobuf/protoc-gen-go@latest 64 | GO111MODULE=on go get github.com/rubenv/sql-migrate/... 65 | GO111MODULE=on go get github.com/fullstorydev/grpcurl/cmd/grpcurl@latest 66 | GO111MODULE=on go get github.com/shuLhan/go-bindata/... 67 | # Setup persistence databases 68 | sql-migrate up -env="production" -config configs/sql-migrate/node_and_port_scan.yaml 69 | sql-migrate up -env="production" -config configs/sql-migrate/node_wake.yaml 70 | # Generate bindings 71 | go generate ./... 72 | -------------------------------------------------------------------------------- /api/proto/v1/metadata.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package com.pojtinger.felicitas.liwasc; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | option go_package = "github.com/pojntfx/liwasc/pkg/api/proto/v1"; 8 | 9 | service MetadataService { 10 | rpc GetMetadataForScanner(google.protobuf.Empty) 11 | returns (ScannerMetadataMessage); 12 | rpc GetMetadataForNode(NodeMetadataReferenceMessage) 13 | returns (NodeMetadataMessage); 14 | rpc GetMetadataForPort(PortMetadataReferenceMessage) 15 | returns (PortMetadataMessage); 16 | } 17 | 18 | message ScannerMetadataMessage { 19 | repeated string Subnets = 1; 20 | string Device = 2; 21 | } 22 | 23 | message NodeMetadataReferenceMessage { string MACAddress = 1; } 24 | 25 | message NodeMetadataMessage { 26 | string MACAddress = 1; 27 | string Vendor = 3; 28 | string Registry = 4; 29 | string Organization = 5; 30 | string Address = 6; 31 | bool Visible = 7; 32 | } 33 | 34 | message PortMetadataReferenceMessage { 35 | int64 PortNumber = 1; 36 | string TransportProtocol = 2; 37 | } 38 | 39 | message PortMetadataMessage { 40 | string ServiceName = 1; 41 | int64 PortNumber = 2; 42 | string TransportProtocol = 3; 43 | string Description = 4; 44 | string Assignee = 5; 45 | string Contact = 6; 46 | string RegistrationDate = 7; 47 | string ModificationDate = 8; 48 | string Reference = 9; 49 | string ServiceCode = 10; 50 | string UnauthorizedUseReported = 11; 51 | string AssignmentNotes = 12; 52 | } -------------------------------------------------------------------------------- /api/proto/v1/node_and_port_scan.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package com.pojtinger.felicitas.liwasc; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | option go_package = "github.com/pojntfx/liwasc/pkg/api/proto/v1"; 8 | 9 | service NodeAndPortScanService { 10 | rpc StartNodeScan(NodeScanStartMessage) returns (NodeScanMessage); 11 | 12 | rpc SubscribeToNodeScans(google.protobuf.Empty) 13 | returns (stream NodeScanMessage); 14 | rpc SubscribeToNodes(NodeScanMessage) returns (stream NodeMessage); 15 | rpc SubscribeToPortScans(NodeMessage) returns (stream PortScanMessage); 16 | rpc SubscribeToPorts(PortScanMessage) returns (stream PortMessage); 17 | } 18 | 19 | message NodeScanStartMessage { 20 | int64 NodeScanTimeout = 1; 21 | int64 PortScanTimeout = 2; 22 | string MACAddress = 3; // Scopes the scan to one node. Set to "" to scan all. 23 | } 24 | 25 | message NodeScanMessage { 26 | int64 ID = 1; 27 | string CreatedAt = 2; 28 | bool Done = 3; 29 | } 30 | 31 | message NodeMessage { 32 | int64 ID = 1; 33 | string CreatedAt = 2; 34 | int64 Priority = 3; 35 | string MACAddress = 4; 36 | string IPAddress = 5; 37 | int64 NodeScanID = 6; 38 | bool PoweredOn = 7; 39 | } 40 | 41 | message PortScanMessage { 42 | int64 ID = 1; 43 | string CreatedAt = 2; 44 | bool Done = 3; 45 | int64 NodeID = 4; 46 | } 47 | 48 | message PortMessage { 49 | int64 ID = 1; 50 | string CreatedAt = 2; 51 | int64 Priority = 3; 52 | int64 PortNumber = 4; 53 | string TransportProtocol = 5; 54 | int64 PortScanID = 6; 55 | } -------------------------------------------------------------------------------- /api/proto/v1/node_wake.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package com.pojtinger.felicitas.liwasc; 4 | 5 | import "google/protobuf/empty.proto"; 6 | 7 | option go_package = "github.com/pojntfx/liwasc/pkg/api/proto/v1"; 8 | 9 | service NodeWakeService { 10 | rpc StartNodeWake(NodeWakeStartMessage) returns (NodeWakeMessage); 11 | 12 | rpc SubscribeToNodeWakes(google.protobuf.Empty) 13 | returns (stream NodeWakeMessage); 14 | } 15 | 16 | message NodeWakeStartMessage { 17 | int64 NodeWakeTimeout = 1; 18 | string MACAddress = 2; 19 | } 20 | 21 | message NodeWakeMessage { 22 | int64 ID = 1; 23 | string CreatedAt = 2; 24 | bool Done = 3; 25 | int64 Priority = 4; 26 | string MACAddress = 5; 27 | bool PoweredOn = 6; 28 | } 29 | -------------------------------------------------------------------------------- /assets/demo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojntfx/liwasc/74da4132791f9ff392439530a7a42e07100271d9/assets/demo.webp -------------------------------------------------------------------------------- /assets/initial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojntfx/liwasc/74da4132791f9ff392439530a7a42e07100271d9/assets/initial.png -------------------------------------------------------------------------------- /assets/liwasc Icon.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojntfx/liwasc/74da4132791f9ff392439530a7a42e07100271d9/assets/liwasc Icon.afdesign -------------------------------------------------------------------------------- /assets/liwasc Logo.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojntfx/liwasc/74da4132791f9ff392439530a7a42e07100271d9/assets/liwasc Logo.afdesign -------------------------------------------------------------------------------- /assets/setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojntfx/liwasc/74da4132791f9ff392439530a7a42e07100271d9/assets/setup.png -------------------------------------------------------------------------------- /cmd/liwasc-frontend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/kataras/compress" 9 | "github.com/maxence-charriere/go-app/v8/pkg/app" 10 | "github.com/pojntfx/liwasc/pkg/components" 11 | ) 12 | 13 | func main() { 14 | // Client-side code 15 | { 16 | // Define the routes 17 | app.Route("/", &components.Home{}) 18 | 19 | // Start the app 20 | app.RunWhenOnBrowser() 21 | } 22 | 23 | // Server-/build-side code 24 | { 25 | // Parse the flags 26 | build := flag.Bool("build", false, "Create static build") 27 | out := flag.String("out", "out/liwasc-frontend", "Out directory for static build") 28 | path := flag.String("path", "", "Base path for static build") 29 | serve := flag.Bool("serve", false, "Build and serve the frontend") 30 | laddr := flag.String("laddr", "localhost:15125", "Address to serve the frontend on") 31 | 32 | flag.Parse() 33 | 34 | // Define the handler 35 | h := &app.Handler{ 36 | Author: "Felicitas Pojtinger", 37 | BackgroundColor: "#151515", 38 | Description: "List, wake and scan nodes in a network.", 39 | Icon: app.Icon{ 40 | Default: "/web/icon.png", 41 | }, 42 | Keywords: []string{ 43 | "network", 44 | "network-scanner", 45 | "port-scanner", 46 | "ip-scanner", 47 | "arp-scanner", 48 | "arp", 49 | "iana", 50 | "ports2packets", 51 | "liwasc", 52 | "vendor2mac", 53 | "wake-on-lan", 54 | "wol", 55 | "service-name", 56 | }, 57 | LoadingLabel: "List, wake and scan nodes in a network.", 58 | Name: "liwasc", 59 | RawHeaders: []string{ 60 | ``, 61 | ``, 62 | ``, 63 | ``, 64 | }, 65 | Styles: []string{ 66 | `https://unpkg.com/@patternfly/patternfly@4.96.2/patternfly.css`, 67 | `https://unpkg.com/@patternfly/patternfly@4.96.2/patternfly-addons.css`, 68 | `/web/index.css`, 69 | }, 70 | ThemeColor: "#151515", 71 | Title: "liwasc", 72 | } 73 | 74 | // Create static build if specified 75 | if *build { 76 | // Deploy under a path 77 | if *path != "" { 78 | h.Resources = app.GitHubPages(*path) 79 | } 80 | 81 | if err := app.GenerateStaticWebsite(*out, h); err != nil { 82 | log.Fatalf("could not build: %v\n", err) 83 | } 84 | } 85 | 86 | // Serve if specified 87 | if *serve { 88 | log.Printf("liwasc frontend listening on %v\n", *laddr) 89 | 90 | if err := http.ListenAndServe(*laddr, compress.Handler(h)); err != nil { 91 | log.Fatalf("could not open liwasc frontend: %v\n", err) 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /configs/sql-migrate/node_and_port_scan.yaml: -------------------------------------------------------------------------------- 1 | production: 2 | dialect: sqlite3 3 | datasource: out/tmp/var/lib/liwasc/node_and_port_scan.sqlite 4 | dir: db/sqlite/migrations/node_and_port_scan/ 5 | -------------------------------------------------------------------------------- /configs/sql-migrate/node_wake.yaml: -------------------------------------------------------------------------------- 1 | production: 2 | dialect: sqlite3 3 | datasource: out/tmp/var/lib/liwasc/node_wake.sqlite 4 | dir: db/sqlite/migrations/node_wake/ 5 | -------------------------------------------------------------------------------- /configs/sqlboiler/mac2vendor.toml: -------------------------------------------------------------------------------- 1 | [sqlite3] 2 | dbname = "../../out/tmp/etc/liwasc/oui-database.sqlite" 3 | -------------------------------------------------------------------------------- /configs/sqlboiler/node_and_port_scan.toml: -------------------------------------------------------------------------------- 1 | [sqlite3] 2 | dbname = "../../out/tmp/var/lib/liwasc/node_and_port_scan.sqlite" 3 | -------------------------------------------------------------------------------- /configs/sqlboiler/node_wake.toml: -------------------------------------------------------------------------------- 1 | [sqlite3] 2 | dbname = "../../out/tmp/var/lib/liwasc/node_wake.sqlite" -------------------------------------------------------------------------------- /db/sqlite/migrations/node_and_port_scan/1616878410007.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | create table node_scans ( 3 | id integer not null primary key, 4 | created_at date not null, 5 | done integer not null 6 | ); 7 | create table nodes ( 8 | id integer not null primary key, 9 | created_at date not null, 10 | mac_address text not null, 11 | ip_address text not null, 12 | node_scan_id integer not null, 13 | foreign key (node_scan_id) references node_scans(id) 14 | ); 15 | create table port_scans ( 16 | id integer not null primary key, 17 | created_at date not null, 18 | done integer not null, 19 | node_id integer not null, 20 | foreign key (node_id) references nodes(id) 21 | ); 22 | create table ports ( 23 | id integer not null primary key, 24 | created_at date not null, 25 | port_number integer not null, 26 | transport_protocol text not null, 27 | port_scan_id integer not null, 28 | foreign key (port_scan_id) references port_scans(id) 29 | ); 30 | -- +migrate Down 31 | drop table node_scans; 32 | drop table nodes; 33 | drop table port_scans; 34 | drop table ports; -------------------------------------------------------------------------------- /db/sqlite/migrations/node_wake/1616878455601.sql: -------------------------------------------------------------------------------- 1 | -- +migrate Up 2 | create table node_wakes ( 3 | id integer not null primary key, 4 | created_at date not null, 5 | done integer not null, 6 | mac_address text not null, 7 | powered_on integer not null 8 | ); 9 | -- +migrate Down 10 | drop table node_wakes; -------------------------------------------------------------------------------- /examples/liwasc-backend-config.yaml: -------------------------------------------------------------------------------- 1 | deviceName: eth0 2 | listenAddress: localhost:15123 3 | mac2vendorDatabasePath: /home/pojntfx/.local/share/liwasc/etc/liwasc/oui-database.sqlite 4 | mac2vendorDatabaseURL: https://mac2vendor.com/download/oui-database.sqlite 5 | maxConcurrentPortScans: 100 6 | nodeAndPortScanDatabasePath: /home/pojntfx/.local/share/liwasc/var/lib/liwasc/node_and_port_scan.sqlite 7 | nodeWakeDatabasePath: /home/pojntfx/.local/share/liwasc/var/lib/liwasc/node_wake.sqlite 8 | oidcClientID: myoidcclientid 9 | oidcIssuer: https://pojntfx.eu.auth0.com/ 10 | periodicNodeScanTimeout: 500 11 | periodicPortScanTimeout: 10 12 | periodicScanCronExpression: "*/10 * * * *" 13 | ports2PacketsDatabasePath: /home/pojntfx/.local/share/liwasc/etc/liwasc/ports2packets.csv 14 | ports2PacketsDatabaseURL: https://github.com/pojntfx/ports2packets/releases/download/weekly-csv/ports2packets.csv 15 | serviceNamesPortNumbersDatabasePath: /home/pojntfx/.local/share/liwasc/etc/liwasc/service-names-port-numbers.csv 16 | serviceNamesPortNumbersDatabaseURL: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.csv 17 | webSocketListenAddress: localhost:15124 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pojntfx/liwasc 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/coreos/go-oidc/v3 v3.0.0 7 | github.com/friendsofgo/errors v0.9.2 8 | github.com/fullstorydev/grpcurl v1.8.0 // indirect 9 | github.com/golang/protobuf v1.5.2 10 | github.com/google/gopacket v1.1.19 11 | github.com/j-keck/arping v1.0.1 12 | github.com/jszwec/csvutil v1.5.0 13 | github.com/kataras/compress v0.0.6 14 | github.com/mattn/go-sqlite3 v2.0.3+incompatible 15 | github.com/maxence-charriere/go-app/v8 v8.0.1 16 | github.com/mdlayher/wol v0.0.0-20200423173749-bc23029f94e1 17 | github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 18 | github.com/pkg/errors v0.9.1 19 | github.com/pojntfx/go-app-grpc-chat-backend v0.0.0-20200914214506-117c1d64fa06 20 | github.com/pojntfx/go-app-grpc-chat-frontend-web v0.0.0-20200914214440-b28bb959fef9 21 | github.com/robfig/cron/v3 v3.0.1 22 | github.com/rubenv/sql-migrate v0.0.0-20210408115534-a32ed26c37ea 23 | github.com/shuLhan/go-bindata v4.0.0+incompatible // indirect 24 | github.com/smartystreets/assertions v1.0.0 // indirect 25 | github.com/spf13/cobra v1.1.3 26 | github.com/spf13/viper v1.7.0 27 | github.com/ugjka/messenger v1.1.3 28 | github.com/volatiletech/null/v8 v8.1.2 29 | github.com/volatiletech/randomize v0.0.1 30 | github.com/volatiletech/sqlboiler-sqlite3 v0.0.0-20210314195744-a1c697a68aef // indirect 31 | github.com/volatiletech/sqlboiler/v4 v4.5.0 32 | github.com/volatiletech/strmangle v0.0.1 33 | github.com/ziutek/mymysql v1.5.4 // indirect 34 | golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 35 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c 36 | google.golang.org/grpc v1.36.1 37 | google.golang.org/protobuf v1.26.0 38 | ) 39 | -------------------------------------------------------------------------------- /pkg/components/about_modal.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "github.com/maxence-charriere/go-app/v8/pkg/app" 4 | 5 | type AboutModal struct { 6 | app.Compo 7 | 8 | Open bool 9 | Close func() 10 | 11 | ID string 12 | 13 | LogoSrc string 14 | LogoAlt string 15 | Title string 16 | 17 | Body app.UI 18 | Footer string 19 | } 20 | 21 | func (c *AboutModal) Render() app.UI { 22 | return app.Div(). 23 | Class(func() string { 24 | classes := "pf-c-backdrop" 25 | 26 | if !c.Open { 27 | classes += " pf-u-display-none" 28 | } 29 | 30 | return classes 31 | }()). 32 | Body( 33 | app.Div(). 34 | Class("pf-l-bullseye"). 35 | Body( 36 | app.Div(). 37 | Class("pf-c-about-modal-box"). 38 | Aria("role", "dialog"). 39 | Aria("modal", true). 40 | Aria("labelledby", c.ID). 41 | Body( 42 | app.Div(). 43 | Class("pf-c-about-modal-box__brand"). 44 | Body( 45 | app.Img(). 46 | Class("pf-c-about-modal-box__brand-image"). 47 | Src(c.LogoSrc). 48 | Alt(c.LogoAlt), 49 | ), 50 | app.Div(). 51 | Class("pf-c-about-modal-box__close"). 52 | Body( 53 | app.Button(). 54 | Class("pf-c-button pf-m-plain"). 55 | Type("button"). 56 | Aria("label", "Close dialog"). 57 | OnClick(func(ctx app.Context, e app.Event) { 58 | c.Close() 59 | }). 60 | Body( 61 | app.I(). 62 | Class("fas fa-times"). 63 | Aria("hidden", true), 64 | ), 65 | ), 66 | app.Div(). 67 | Class("pf-c-about-modal-box__header"). 68 | Body( 69 | app.H1(). 70 | Class("pf-c-title pf-m-4xl"). 71 | ID(c.ID). 72 | Text(c.Title), 73 | ), 74 | app.Div().Class("pf-c-about-modal-box__hero"), 75 | app.Div(). 76 | Class("pf-c-about-modal-box__content"). 77 | Body( 78 | app.Div(). 79 | Class("pf-c-content"). 80 | Body( 81 | app.Dl(). 82 | Class("pf-c-content"). 83 | Body(c.Body), 84 | ), 85 | app.P(). 86 | Class("pf-c-about-modal-box__strapline"). 87 | Text(c.Footer), 88 | ), 89 | ), 90 | ), 91 | ) 92 | } 93 | 94 | func (c *AboutModal) OnMount(ctx app.Context) { 95 | app.Window().AddEventListener("keyup", func(ctx app.Context, e app.Event) { 96 | if e.Get("key").String() == "Escape" { 97 | c.Close() 98 | } 99 | }) 100 | } 101 | -------------------------------------------------------------------------------- /pkg/components/controlled.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "github.com/maxence-charriere/go-app/v8/pkg/app" 4 | 5 | type Controlled struct { 6 | app.Compo 7 | 8 | Component app.UI 9 | Properties map[string]interface{} 10 | } 11 | 12 | func (c *Controlled) Render() app.UI { 13 | for key, value := range c.Properties { 14 | c.Defer(func(ctx app.Context) { 15 | if c.JSValue() != nil { 16 | c.JSValue().Set(key, value) 17 | } 18 | }) 19 | } 20 | 21 | return c.Component 22 | } 23 | -------------------------------------------------------------------------------- /pkg/components/expandable_section.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "github.com/maxence-charriere/go-app/v8/pkg/app" 4 | 5 | type ExpandableSection struct { 6 | app.Compo 7 | 8 | Open bool 9 | OnToggle func() 10 | Title string 11 | ClosedTitle string 12 | OpenTitle string 13 | Body []app.UI 14 | } 15 | 16 | func (c *ExpandableSection) Render() app.UI { 17 | return app.Div(). 18 | Class(func() string { 19 | classes := "pf-c-expandable-section pf-u-mt-md" 20 | 21 | if c.Open { 22 | classes += " pf-m-expanded" 23 | } 24 | 25 | return classes 26 | }()). 27 | Body( 28 | app.Button(). 29 | Type("button"). 30 | Class("pf-c-expandable-section__toggle"). 31 | Aria("label", func() string { 32 | message := c.ClosedTitle 33 | 34 | if c.Open { 35 | message = c.OpenTitle 36 | } 37 | 38 | return message 39 | }()). 40 | Aria("expanded", c.Open). 41 | OnClick(func(ctx app.Context, e app.Event) { 42 | c.OnToggle() 43 | }). 44 | Body( 45 | app.Span(). 46 | Class("pf-c-expandable-section__toggle-icon"). 47 | Body( 48 | app.I(). 49 | Class("fas fa-angle-right"). 50 | Aria("hidden", true), 51 | ), 52 | app.Span(). 53 | Class("pf-c-expandable-section__toggle-text"). 54 | Text(c.Title), 55 | ), 56 | app.Div(). 57 | Class("pf-c-expandable-section__content"). 58 | Hidden(!c.Open). 59 | Body(c.Body...), 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/components/form_group.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "github.com/maxence-charriere/go-app/v8/pkg/app" 4 | 5 | type FormGroup struct { 6 | app.Compo 7 | 8 | Required bool 9 | Label app.UI 10 | Input app.UI 11 | } 12 | 13 | func (c *FormGroup) Render() app.UI { 14 | return app. 15 | Div(). 16 | Class("pf-c-form__group"). 17 | Body( 18 | app.Div(). 19 | Class("pf-c-form__group-label"). 20 | Body( 21 | c.Label, 22 | app.If(c.Required, 23 | app. 24 | Span(). 25 | Class("pf-c-form__label-required"). 26 | Aria("hidden", true). 27 | Text("*"), 28 | ), 29 | ), 30 | app. 31 | Div(). 32 | Class("pf-c-form__group-control"). 33 | Body(c.Input), 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/components/home.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/maxence-charriere/go-app/v8/pkg/app" 8 | "github.com/pojntfx/go-app-grpc-chat-frontend-web/pkg/websocketproxy" 9 | api "github.com/pojntfx/liwasc/pkg/api/proto/v1" 10 | "github.com/pojntfx/liwasc/pkg/providers" 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/metadata" 13 | ) 14 | 15 | type Home struct { 16 | app.Compo 17 | } 18 | 19 | func (c *Home) Render() app.UI { 20 | return &providers.ConfigurationProvider{ 21 | StoragePrefix: "liwasc.configuration", 22 | StateQueryParameter: "state", 23 | CodeQueryParameter: "code", 24 | Children: func(cpcp providers.SetupProviderChildrenProps) app.UI { 25 | // This div is required so that there are no authorization loops 26 | return app.Div(). 27 | TabIndex(-1). 28 | Class("pf-x-ws-router"). 29 | Body( 30 | app.If(cpcp.Ready, 31 | // Identity provider 32 | &providers.IdentityProvider{ 33 | Issuer: cpcp.OIDCIssuer, 34 | ClientID: cpcp.OIDCClientID, 35 | RedirectURL: cpcp.OIDCRedirectURL, 36 | HomeURL: "/", 37 | Scopes: []string{"profile", "email"}, 38 | StoragePrefix: "liwasc.identity", 39 | Children: func(ipcp providers.IdentityProviderChildrenProps) app.UI { 40 | // Configuration shell 41 | if ipcp.Error != nil { 42 | return &SetupShell{ 43 | LogoSrc: "/web/logo.svg", 44 | Title: "Log in to liwasc", 45 | ShortDescription: "List, wake and scan nodes in a network.", 46 | LongDescription: `liwasc is a high-performance network and port scanner. It can 47 | quickly give you a overview of the nodes in your network, the 48 | services that run on them and manage their power status.`, 49 | HelpLink: "https://github.com/pojntfx/liwasc#Usage", 50 | Links: map[string]string{ 51 | "License": "https://github.com/pojntfx/liwasc/blob/main/LICENSE", 52 | "Source Code": "https://github.com/pojntfx/liwasc", 53 | "Documentation": "https://github.com/pojntfx/liwasc#Usage", 54 | }, 55 | 56 | BackendURL: cpcp.BackendURL, 57 | OIDCIssuer: cpcp.OIDCIssuer, 58 | OIDCClientID: cpcp.OIDCClientID, 59 | OIDCRedirectURL: cpcp.OIDCRedirectURL, 60 | 61 | SetBackendURL: cpcp.SetBackendURL, 62 | SetOIDCIssuer: cpcp.SetOIDCIssuer, 63 | SetOIDCClientID: cpcp.SetOIDCClientID, 64 | SetOIDCRedirectURL: cpcp.SetOIDCRedirectURL, 65 | ApplyConfig: cpcp.ApplyConfig, 66 | 67 | Error: ipcp.Error, 68 | } 69 | } 70 | 71 | // Configuration placeholder 72 | if ipcp.IDToken == "" || ipcp.UserInfo.Email == "" { 73 | return app.P().Text("Authorizing ...") 74 | } 75 | 76 | // gRPC Client 77 | conn, err := grpc.Dial(cpcp.BackendURL, grpc.WithContextDialer(websocketproxy.NewWebSocketProxyClient(time.Minute).Dialer), grpc.WithInsecure()) 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | // Data provider 83 | return &providers.DataProvider{ 84 | AuthenticatedContext: metadata.AppendToOutgoingContext(context.Background(), "X-Liwasc-Authorization", ipcp.IDToken), 85 | MetadataService: api.NewMetadataServiceClient(conn), 86 | NodeAndPortScanService: api.NewNodeAndPortScanServiceClient(conn), 87 | NodeWakeService: api.NewNodeWakeServiceClient(conn), 88 | Children: func(dpcp providers.DataProviderChildrenProps) app.UI { 89 | // Data shell 90 | return &DataShell{ 91 | Network: dpcp.Network, 92 | UserInfo: ipcp.UserInfo, 93 | 94 | TriggerNetworkScan: dpcp.TriggerNetworkScan, 95 | StartNodeWake: dpcp.StartNodeWake, 96 | Logout: ipcp.Logout, 97 | 98 | Error: dpcp.Error, 99 | Recover: dpcp.Recover, 100 | Ignore: dpcp.Ignore, 101 | } 102 | }, 103 | } 104 | }, 105 | }, 106 | ).Else( 107 | // Configuration shell 108 | &SetupShell{ 109 | LogoSrc: "/web/logo.svg", 110 | Title: "Log in to liwasc", 111 | ShortDescription: "List, wake and scan nodes in a network.", 112 | LongDescription: `liwasc is a high-performance network and port scanner. It can 113 | quickly give you a overview of the nodes in your network, the 114 | services that run on them and manage their power status.`, 115 | HelpLink: "https://github.com/pojntfx/liwasc#Usage", 116 | Links: map[string]string{ 117 | "License": "https://github.com/pojntfx/liwasc/blob/main/LICENSE", 118 | "Source Code": "https://github.com/pojntfx/liwasc", 119 | "Documentation": "https://github.com/pojntfx/liwasc#Usage", 120 | }, 121 | 122 | BackendURL: cpcp.BackendURL, 123 | OIDCIssuer: cpcp.OIDCIssuer, 124 | OIDCClientID: cpcp.OIDCClientID, 125 | OIDCRedirectURL: cpcp.OIDCRedirectURL, 126 | 127 | SetBackendURL: cpcp.SetBackendURL, 128 | SetOIDCIssuer: cpcp.SetOIDCIssuer, 129 | SetOIDCClientID: cpcp.SetOIDCClientID, 130 | SetOIDCRedirectURL: cpcp.SetOIDCRedirectURL, 131 | ApplyConfig: cpcp.ApplyConfig, 132 | 133 | Error: cpcp.Error, 134 | }, 135 | ), 136 | ) 137 | }, 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /pkg/components/mobile_metadata.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "github.com/maxence-charriere/go-app/v8/pkg/app" 4 | 5 | type MobileMetadata struct { 6 | app.Compo 7 | 8 | LastNodeScanDate string 9 | Subnets []string 10 | Device string 11 | } 12 | 13 | func (c *MobileMetadata) Render() app.UI { 14 | return app.Dl(). 15 | Class("pf-c-description-list"). 16 | Body( 17 | app.Div(). 18 | Class("pf-c-description-list__group"). 19 | Body( 20 | app.Dt(). 21 | Class("pf-c-description-list__term"). 22 | Body( 23 | app.Span(). 24 | Class("pf-c-description-list__text"). 25 | ID("last-scan-mobile"). 26 | Body( 27 | app.I(). 28 | Class("fas fa-history pf-u-mr-xs"). 29 | Aria("hidden", true), 30 | app.Text("Last Scan"), 31 | ), 32 | ), 33 | app.Dd(). 34 | Class("pf-c-description-list__description"). 35 | Body( 36 | app.Div(). 37 | Class("pf-c-description-list__text"). 38 | Body( 39 | app.Ul(). 40 | Class("pf-c-label-group__list"). 41 | Aria("role", "list"). 42 | Aria("labelledby", "last-scan-mobile"). 43 | Body( 44 | app.Li(). 45 | Class("pf-c-label-group__list-item"). 46 | Body( 47 | app.Span(). 48 | Class("pf-c-label"). 49 | Body( 50 | app.Span(). 51 | Class("pf-c-label__content"). 52 | Body( 53 | app.Text(c.LastNodeScanDate), 54 | ), 55 | ), 56 | ), 57 | ), 58 | ), 59 | ), 60 | ), 61 | app.Div(). 62 | Class("pf-c-description-list__group"). 63 | Body( 64 | app.Dt(). 65 | Class("pf-c-description-list__term"). 66 | Body( 67 | app.Span(). 68 | Class("pf-c-description-list__text"). 69 | ID("subnets-mobile"). 70 | Body( 71 | app.I(). 72 | Class("fas fa-network-wired pf-u-mr-xs"). 73 | Aria("hidden", true), 74 | app.Text("Subnets"), 75 | ), 76 | ), 77 | app.Dd(). 78 | Class("pf-c-description-list__description"). 79 | Body( 80 | app.Div(). 81 | Class("pf-c-description-list__text"). 82 | Body( 83 | app.Ul(). 84 | Class("pf-c-label-group__list"). 85 | Aria("role", "list"). 86 | Aria("labelledby", "subnets-mobile"). 87 | Body( 88 | app.Range(c.Subnets).Slice(func(i int) app.UI { 89 | return app.Li(). 90 | Class("pf-c-label-group__list-item"). 91 | Body( 92 | app.Span(). 93 | Class("pf-c-label"). 94 | Body( 95 | app.Span(). 96 | Class("pf-c-label__content"). 97 | Body( 98 | app.Text(c.Subnets[i]), 99 | ), 100 | ), 101 | ) 102 | }), 103 | ), 104 | ), 105 | ), 106 | ), 107 | app.Div(). 108 | Class("pf-c-description-list__group"). 109 | Body( 110 | app.Dt(). 111 | Class("pf-c-description-list__term"). 112 | Body( 113 | app.Span(). 114 | Class("pf-c-description-list__text"). 115 | ID("device-mobile"). 116 | Body( 117 | app.I(). 118 | Class("fas fa-microchip pf-u-mr-xs"). 119 | Aria("hidden", true), 120 | app.Text("Device"), 121 | ), 122 | ), 123 | app.Dd(). 124 | Class("pf-c-description-list__description"). 125 | Body( 126 | app.Dd(). 127 | Class("pf-c-description-list__description"). 128 | Body( 129 | app.Ul(). 130 | Class("pf-c-label-group__list"). 131 | Aria("role", "list"). 132 | Aria("labelledby", "device-mobile"). 133 | Body( 134 | app.Li(). 135 | Class("pf-c-label-group__list-item"). 136 | Body( 137 | app.Span(). 138 | Class("pf-c-label"). 139 | Body( 140 | app.Span(). 141 | Class("pf-c-label__content"). 142 | Body( 143 | app.Text(c.Device), 144 | ), 145 | ), 146 | ), 147 | ), 148 | ), 149 | ), 150 | ), 151 | ) 152 | } 153 | -------------------------------------------------------------------------------- /pkg/components/modal.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "github.com/maxence-charriere/go-app/v8/pkg/app" 5 | ) 6 | 7 | type Modal struct { 8 | app.Compo 9 | 10 | Open bool 11 | Close func() 12 | 13 | ID string 14 | Classes string 15 | 16 | Title string 17 | Body []app.UI 18 | Footer []app.UI 19 | } 20 | 21 | func (c *Modal) Render() app.UI { 22 | return app.Div(). 23 | Class(func() string { 24 | classes := "pf-c-backdrop" 25 | 26 | if c.Classes != "" { 27 | classes += " " + c.Classes 28 | } 29 | 30 | if !c.Open { 31 | classes += " pf-u-display-none" 32 | } 33 | 34 | return classes 35 | }()). 36 | Body( 37 | app.Div(). 38 | Class("pf-l-bullseye"). 39 | Body( 40 | app.Div(). 41 | Class("pf-c-modal-box pf-m-sm"). 42 | Aria("modal", true). 43 | Aria("labelledby", c.ID). 44 | Body( 45 | app.Button(). 46 | Class("pf-c-button pf-m-plain"). 47 | Type("button"). 48 | Aria("label", "Close dialog"). 49 | OnClick(func(ctx app.Context, e app.Event) { 50 | c.Close() 51 | }). 52 | Body( 53 | app.I(). 54 | Class("fas fa-times"). 55 | Aria("hidden", true), 56 | ), 57 | app.Header(). 58 | Class("pf-c-modal-box__header"). 59 | Body( 60 | app.H1(). 61 | Class("pf-c-modal-box__title"). 62 | ID(c.ID). 63 | Text(c.Title), 64 | ), 65 | app.Div(). 66 | Class("pf-c-modal-box__body"). 67 | Body(c.Body...), 68 | app.If( 69 | c.Footer != nil, 70 | app.Footer(). 71 | Class("pf-c-modal-box__footer"). 72 | Body(c.Footer...), 73 | ), 74 | ), 75 | ), 76 | ) 77 | } 78 | 79 | func (c *Modal) OnMount(ctx app.Context) { 80 | app.Window().AddEventListener("keyup", func(ctx app.Context, e app.Event) { 81 | if e.Get("key").String() == "Escape" { 82 | c.Close() 83 | } 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /pkg/components/navbar.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | 8 | "github.com/maxence-charriere/go-app/v8/pkg/app" 9 | ) 10 | 11 | type Navbar struct { 12 | app.Compo 13 | 14 | NotificationsDrawerOpen bool 15 | ToggleNotificationsDrawerOpen func() 16 | 17 | ToggleSettings func() 18 | ToggleAbout func() 19 | 20 | OverflowMenuExpanded bool 21 | ToggleOverflowMenuExpanded func() 22 | 23 | UserMenuExpanded bool 24 | ToggleUserMenuExpanded func() 25 | 26 | UserEmail string 27 | Logout func() 28 | } 29 | 30 | func (c *Navbar) Render() app.UI { 31 | // Get the MD5 hash for the user's gravatar 32 | avatarHash := md5.Sum([]byte(c.UserEmail)) 33 | 34 | return app.Header(). 35 | Class("pf-c-page__header"). 36 | Body( 37 | app.Div(). 38 | Class("pf-c-page__header-brand"). 39 | Body( 40 | app.A(). 41 | Href("#"). 42 | Class("pf-c-page__header-brand-link"). 43 | Body( 44 | app.Img(). 45 | Class("pf-c-brand pf-x-c-brand--nav"). 46 | Src("/web/logo.svg"). 47 | Alt("liwasc Logo"), 48 | ), 49 | ), 50 | app.Div(). 51 | Class("pf-c-page__header-tools"). 52 | Body( 53 | app.Div(). 54 | Class("pf-c-page__header-tools-group"). 55 | Body( 56 | app.Div(). 57 | Class("pf-c-page__header-tools-group"). 58 | Body( 59 | app.Div(). 60 | Class(func() string { 61 | classes := "pf-c-page__header-tools-item" 62 | 63 | if c.NotificationsDrawerOpen { 64 | classes += " pf-m-selected" 65 | } 66 | 67 | return classes 68 | }()). 69 | Body( 70 | app.Button(). 71 | Class("pf-c-button pf-m-plain"). 72 | Type("button"). 73 | Aria("label", "Unread notifications"). 74 | Aria("expanded", false). 75 | OnClick(func(ctx app.Context, e app.Event) { 76 | c.ToggleNotificationsDrawerOpen() 77 | }). 78 | Body( 79 | app.Span(). 80 | Class("pf-c-notification-badge"). 81 | Body( 82 | app.I(). 83 | Class("pf-icon-bell"). 84 | Aria("hidden", true), 85 | ), 86 | ), 87 | ), 88 | app.Div(). 89 | Class("pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-lg"). 90 | Body( 91 | app.Button(). 92 | Class("pf-c-button pf-m-plain"). 93 | Type("button"). 94 | Aria("label", "Settings"). 95 | OnClick(func(ctx app.Context, e app.Event) { 96 | c.ToggleSettings() 97 | }). 98 | Body( 99 | app.I(). 100 | Class("fas fa-cog"). 101 | Aria("hidden", true), 102 | ), 103 | ), 104 | app.Div().Class("pf-c-page__header-tools-item"). 105 | Body( 106 | app.Div(). 107 | Class(func() string { 108 | classes := "pf-c-dropdown" 109 | 110 | if c.OverflowMenuExpanded { 111 | classes += " pf-m-expanded" 112 | } 113 | 114 | return classes 115 | }()). 116 | Body( 117 | app.Button(). 118 | Class("pf-c-dropdown__toggle pf-m-plain"). 119 | ID("page-default-nav-example-dropdown-kebab-1-button"). 120 | Aria("expanded", c.OverflowMenuExpanded).Type("button"). 121 | Aria("label", "Actions"). 122 | Body( 123 | app.I(). 124 | Class("fas fa-ellipsis-v pf-u-display-none-on-lg"). 125 | Aria("hidden", true), 126 | app.I(). 127 | Class("fas fa-question-circle pf-u-display-none pf-u-display-inline-block-on-lg"). 128 | Aria("hidden", true), 129 | ).OnClick(func(ctx app.Context, e app.Event) { 130 | c.ToggleOverflowMenuExpanded() 131 | }), 132 | app.Ul(). 133 | Class("pf-c-dropdown__menu pf-m-align-right"). 134 | Aria("aria-labelledby", "page-default-nav-example-dropdown-kebab-1-button"). 135 | Hidden(!c.OverflowMenuExpanded). 136 | Body( 137 | app.Li(). 138 | Body( 139 | app.Button(). 140 | Class("pf-c-button pf-c-dropdown__menu-item pf-u-display-none-on-lg"). 141 | Type("button"). 142 | OnClick(func(ctx app.Context, e app.Event) { 143 | c.ToggleSettings() 144 | }). 145 | Body( 146 | app.Span(). 147 | Class("pf-c-button__icon pf-m-start"). 148 | Body( 149 | app.I(). 150 | Class("fas fa-cog"). 151 | Aria("hidden", true), 152 | ), 153 | app.Text("Settings"), 154 | ), 155 | ), 156 | app.Li(). 157 | Class("pf-c-divider pf-u-display-none-on-lg"). 158 | Aria("role", "separator"), 159 | app.Li(). 160 | Body( 161 | app.A(). 162 | Class("pf-c-dropdown__menu-item"). 163 | Href("https://github.com/pojntfx/liwasc#Usage"). 164 | Text("Documentation"). 165 | Target("_blank"), 166 | ), 167 | app.Li(). 168 | Body( 169 | app.Button(). 170 | Class("pf-c-button pf-c-dropdown__menu-item"). 171 | Type("button"). 172 | OnClick(func(ctx app.Context, e app.Event) { 173 | c.ToggleAbout() 174 | }). 175 | Text("About"), 176 | ), 177 | app.Li(). 178 | Class("pf-c-divider pf-u-display-none-on-md"). 179 | Aria("role", "separator"), 180 | app.Li(). 181 | Class("pf-u-display-none-on-md"). 182 | Body( 183 | app.Button(). 184 | Class("pf-c-button pf-c-dropdown__menu-item"). 185 | Type("button"). 186 | Body( 187 | app.Span(). 188 | Class("pf-c-button__icon pf-m-start"). 189 | Body( 190 | app.I(). 191 | Class("fas fa-sign-out-alt"). 192 | Aria("hidden", true), 193 | ), 194 | app.Text("Logout"), 195 | ). 196 | OnClick(func(ctx app.Context, e app.Event) { 197 | c.Logout() 198 | }), 199 | ), 200 | ), 201 | ), 202 | ), 203 | app.Div(). 204 | Class("pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-md"). 205 | Body( 206 | app.Div(). 207 | Class(func() string { 208 | classes := "pf-c-dropdown" 209 | 210 | if c.UserMenuExpanded { 211 | classes += " pf-m-expanded" 212 | } 213 | 214 | return classes 215 | }()). 216 | Body( 217 | app.Button(). 218 | Class("pf-c-dropdown__toggle pf-m-plain"). 219 | ID("page-layout-horizontal-nav-dropdown-kebab-2-button"). 220 | Aria("expanded", c.UserMenuExpanded). 221 | Type("button"). 222 | Body( 223 | app.Span(). 224 | Class("pf-c-dropdown__toggle-text"). 225 | Text(c.UserEmail), 226 | app. 227 | Span(). 228 | Class("pf-c-dropdown__toggle-icon"). 229 | Body( 230 | app.I(). 231 | Class("fas fa-caret-down"). 232 | Aria("hidden", true), 233 | ), 234 | ).OnClick(func(ctx app.Context, e app.Event) { 235 | c.ToggleUserMenuExpanded() 236 | }), 237 | app.Ul(). 238 | Class("pf-c-dropdown__menu"). 239 | Aria("labelledby", "page-layout-horizontal-nav-dropdown-kebab-2-button"). 240 | Hidden(!c.UserMenuExpanded). 241 | Body( 242 | app.Li().Body( 243 | app.Button(). 244 | Class("pf-c-button pf-c-dropdown__menu-item"). 245 | Type("button"). 246 | Body( 247 | app.Span(). 248 | Class("pf-c-button__icon pf-m-start"). 249 | Body( 250 | app.I(). 251 | Class("fas fa-sign-out-alt"). 252 | Aria("hidden", true), 253 | ), 254 | app.Text("Logout"), 255 | ). 256 | OnClick(func(ctx app.Context, e app.Event) { 257 | go c.Logout() 258 | }), 259 | ), 260 | ), 261 | ), 262 | ), 263 | ), 264 | app.Img().Class("pf-c-avatar").Src(fmt.Sprintf("https://www.gravatar.com/avatar/%v?s=150", hex.EncodeToString(avatarHash[:]))).Alt("Avatar image"), 265 | ), 266 | ), 267 | ) 268 | } 269 | -------------------------------------------------------------------------------- /pkg/components/node_table.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/maxence-charriere/go-app/v8/pkg/app" 7 | "github.com/pojntfx/liwasc/pkg/providers" 8 | ) 9 | 10 | type NodeTable struct { 11 | app.Compo 12 | 13 | Nodes []providers.Node 14 | NodeScanRunning bool 15 | SelectedMACAddress string 16 | 17 | SetSelectedMACAddress func(macAddress string) 18 | TriggerFullNetworkScan func() 19 | TriggerScopedNetworkScan func(macAddress string) 20 | StartNodeWake func(macAddress string) 21 | } 22 | 23 | func (c *NodeTable) Render() app.UI { 24 | return app.Table(). 25 | Class("pf-c-table pf-m-grid-md"). 26 | Aria("role", "grid"). 27 | Aria("label", "Nodes and their status"). 28 | Body( 29 | app.THead(). 30 | Body( 31 | app.Tr(). 32 | Aria("role", "row"). 33 | Body( 34 | app.Th(). 35 | Aria("role", "columnheader"). 36 | Scope("col"). 37 | Body( 38 | app.I().Class("fas fa-plug pf-u-mr-xs").Aria("hidden", true), 39 | app.Text("Powered On"), 40 | ), 41 | app.Th(). 42 | Aria("role", "columnheader"). 43 | Scope("col"). 44 | Body( 45 | app.I().Class("fas fa-address-card pf-u-mr-xs").Aria("hidden", true), 46 | app.Text("MAC Address"), 47 | ), 48 | app.Th(). 49 | Aria("role", "columnheader"). 50 | Scope("col"). 51 | Body( 52 | app.I().Class("fas fa-network-wired pf-u-mr-xs").Aria("hidden", true), 53 | app.Text("IP Address"), 54 | ), 55 | app.Th(). 56 | Aria("role", "columnheader"). 57 | Scope("col"). 58 | Body( 59 | app.I().Class("fas fa-industry pf-u-mr-xs").Aria("hidden", true), 60 | app.Text("Vendor"), 61 | ), 62 | app.Th(). 63 | Aria("role", "columnheader"). 64 | Scope("col"). 65 | Body( 66 | app.I().Class("fas fa-server pf-u-mr-xs").Aria("hidden", true), 67 | app.Text("Ports and Services"), 68 | ), 69 | ), 70 | ), 71 | app.TBody(). 72 | Class("pf-x-u-border-t-0"). 73 | Aria("role", "rowgroup"). 74 | Body( 75 | app.If( 76 | len(c.Nodes) == 0 && !c.NodeScanRunning, 77 | app.Tr(). 78 | Aria("role", "row"). 79 | Body( 80 | app.Td(). 81 | Aria("role", "cell"). 82 | ColSpan(5). 83 | Body( 84 | app.Div(). 85 | Class("pf-l-bullseye"). 86 | Body( 87 | app.Div(). 88 | Class("pf-c-empty-state pf-m-sm"). 89 | Body( 90 | app.Div(). 91 | Class("pf-c-empty-state__content"). 92 | Body( 93 | app.I(). 94 | Class("fas fa-binoculars pf-c-empty-state__icon"). 95 | Aria("hidden", true), 96 | app.H2(). 97 | Class("pf-c-title pf-m-lg"). 98 | Text("No nodes here yet"), 99 | app.Div(). 100 | Class("pf-c-empty-state__body"). 101 | Text("Scan the network to find out what nodes are on it."), 102 | app.Div(). 103 | Class("pf-c-empty-state__primary"). 104 | Body( 105 | // Data actions 106 | &ProgressButton{ 107 | Loading: c.NodeScanRunning, 108 | Icon: "fas fa-rocket", 109 | Text: "Trigger Scan", 110 | 111 | OnClick: func(ctx app.Context, e app.Event) { 112 | c.TriggerFullNetworkScan() 113 | }, 114 | }, 115 | ), 116 | ), 117 | ), 118 | ), 119 | ), 120 | ), 121 | ).Else( 122 | app.Range(c.Nodes).Slice(func(i int) app.UI { 123 | return app.Tr(). 124 | Class(func() string { 125 | classes := "pf-m-hoverable" 126 | 127 | if len(c.Nodes) >= i && c.Nodes[i].MACAddress == c.SelectedMACAddress { 128 | classes += " pf-m-selected" 129 | } 130 | 131 | return classes 132 | }()). 133 | Aria("role", "row"). 134 | OnClick(func(ctx app.Context, e app.Event) { 135 | c.SetSelectedMACAddress(c.Nodes[i].MACAddress) 136 | }). 137 | Body( 138 | app.Td(). 139 | Aria("role", "cell"). 140 | DataSet("label", "Powered On"). 141 | Body( 142 | app.Label(). 143 | Class("pf-c-switch pf-x-c-tooltip-wrapper"). 144 | For(fmt.Sprintf("node-row-%v", i)). 145 | Body( 146 | app.If( 147 | c.Nodes[i].PoweredOn, 148 | app.Div(). 149 | Class("pf-c-tooltip pf-x-c-tooltip pf-m-right"). 150 | Aria("role", "tooltip"). 151 | Body( 152 | app.Div(). 153 | Class("pf-c-tooltip__arrow"), 154 | app.Div(). 155 | Class("pf-c-tooltip__content"). 156 | Text("To turn this node off, please do so manually."), 157 | ), 158 | ), 159 | &Controlled{ 160 | Component: app.Input(). 161 | Class("pf-c-switch__input"). 162 | ID(fmt.Sprintf("node-row-%v", i)). 163 | Aria("label", "Node is off"). 164 | Name(fmt.Sprintf("node-row-%v", i)). 165 | Type("checkbox"). 166 | Checked(c.Nodes[i].PoweredOn). 167 | Disabled(c.Nodes[i].PoweredOn). 168 | OnClick(func(ctx app.Context, e app.Event) { 169 | e.Call("stopPropagation") 170 | 171 | c.StartNodeWake(c.Nodes[i].MACAddress) 172 | }), 173 | Properties: map[string]interface{}{ 174 | "checked": c.Nodes[i].PoweredOn, 175 | "disabled": c.Nodes[i].PoweredOn, 176 | }, 177 | }, 178 | app.Span(). 179 | Class("pf-c-switch__toggle"). 180 | Body( 181 | app.Span(). 182 | Class("pf-c-switch__toggle-icon"). 183 | Body( 184 | app.I(). 185 | Class("fas fa-lightbulb"). 186 | Aria("hidden", true), 187 | ), 188 | ), 189 | app.Span(). 190 | Class("pf-c-switch__label pf-m-on pf-l-flex pf-m-justify-content-center pf-m-align-items-center"). 191 | ID(fmt.Sprintf("node-row-%v-on", i)). 192 | Aria("hidden", true). 193 | Body( 194 | app.If( 195 | c.Nodes[i].NodeWakeRunning, 196 | app.Span(). 197 | Class("pf-c-spinner pf-m-md"). 198 | Aria("role", "progressbar"). 199 | Aria("valuetext", "Loading..."). 200 | Body( 201 | app.Span().Class("pf-c-spinner__clipper"), 202 | app.Span().Class("pf-c-spinner__lead-ball"), 203 | app.Span().Class("pf-c-spinner__tail-ball"), 204 | ), 205 | ).Else( 206 | app.Text("On"), 207 | ), 208 | ), 209 | app.Span(). 210 | Class("pf-c-switch__label pf-m-off pf-l-flex pf-m-justify-content-center pf-m-align-items-center"). 211 | ID(fmt.Sprintf("node-row-%v-off", i)). 212 | Aria("hidden", true). 213 | Body( 214 | app.If( 215 | c.Nodes[i].NodeWakeRunning, 216 | app.Span(). 217 | Class("pf-c-spinner pf-m-md"). 218 | Aria("role", "progressbar"). 219 | Aria("valuetext", "Loading..."). 220 | Body( 221 | app.Span().Class("pf-c-spinner__clipper"), 222 | app.Span().Class("pf-c-spinner__lead-ball"), 223 | app.Span().Class("pf-c-spinner__tail-ball"), 224 | ), 225 | ).Else( 226 | app.Text("Off"), 227 | ), 228 | ), 229 | ), 230 | ), 231 | app.Td(). 232 | Aria("role", "cell"). 233 | DataSet("label", "MAC Address"). 234 | Text(c.Nodes[i].MACAddress), 235 | app.Td(). 236 | Aria("role", "cell"). 237 | DataSet("label", "IP Address"). 238 | Text(c.Nodes[i].IPAddress), 239 | app.Td(). 240 | Aria("role", "cell"). 241 | DataSet("label", "Vendor"). 242 | Text(func() string { 243 | vendor := c.Nodes[i].Vendor 244 | if vendor == "" { 245 | vendor = "Unregistered" 246 | } 247 | 248 | return vendor 249 | }()), 250 | app.Td(). 251 | Aria("role", "cell"). 252 | DataSet("label", "Ports and Services"). 253 | Body( 254 | &ProgressButton{ 255 | Loading: c.Nodes[i].PortScanRunning, 256 | Icon: "fas fa-sync", 257 | 258 | OnClick: func(ctx app.Context, e app.Event) { 259 | e.Call("stopPropagation") 260 | 261 | c.TriggerScopedNetworkScan(c.Nodes[i].MACAddress) 262 | }, 263 | }, 264 | app.If( 265 | len(c.Nodes[i].Ports) > 0, 266 | &PortList{ 267 | Ports: c.Nodes[i].Ports, 268 | }, 269 | ).ElseIf( 270 | c.Nodes[i].PortScanRunning, 271 | app.Text("No open ports found yet."), 272 | ).Else( 273 | app.Text("No open ports found."), 274 | ), 275 | ), 276 | ) 277 | }), 278 | ), 279 | ), 280 | ) 281 | } 282 | -------------------------------------------------------------------------------- /pkg/components/notification_drawer.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "github.com/maxence-charriere/go-app/v8/pkg/app" 4 | 5 | type Notification struct { 6 | Message string 7 | Time string 8 | } 9 | 10 | type NotificationDrawer struct { 11 | app.Compo 12 | 13 | Notifications []Notification 14 | } 15 | 16 | func (c *NotificationDrawer) Render() app.UI { 17 | return app.Div(). 18 | Class("pf-c-notification-drawer"). 19 | Body( 20 | app.Div(). 21 | Class("pf-c-notification-drawer__header"). 22 | Body( 23 | app.H1(). 24 | Class("pf-c-notification-drawer__header-title"). 25 | Text("Events"), 26 | ), 27 | app.Div().Class("pf-c-notification-drawer__body").Body( 28 | app.Ul().Class("pf-c-notification-drawer__list").Body( 29 | app.Range(c.Notifications).Slice(func(i int) app.UI { 30 | return app.Li().Class("pf-c-notification-drawer__list-item pf-m-read pf-m-info").Body( 31 | app.Div().Class("pf-c-notification-drawer__list-item-description").Text( 32 | c.Notifications[len(c.Notifications)-1-i].Message, 33 | ), 34 | app.Div().Class("pf-c-notification-drawer__list-item-timestamp").Text( 35 | c.Notifications[len(c.Notifications)-1-i].Time, 36 | ), 37 | ) 38 | }), 39 | ), 40 | ), 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/components/port_list.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/maxence-charriere/go-app/v8/pkg/app" 7 | "github.com/pojntfx/liwasc/pkg/providers" 8 | ) 9 | 10 | type PortList struct { 11 | Ports []providers.Port 12 | 13 | expanded bool 14 | 15 | app.Compo 16 | } 17 | 18 | func (c *PortList) Render() app.UI { 19 | portsToDisplay := c.Ports 20 | if len(c.Ports) >= 3 && !c.expanded { 21 | portsToDisplay = c.Ports[:3] 22 | } 23 | 24 | return app.Div(). 25 | Class("pf-c-label-group"). 26 | Body( 27 | app.Div(). 28 | Class("pf-c-label-group__main"). 29 | Body( 30 | app.Ul(). 31 | Class("pf-c-label-group__list"). 32 | Aria("role", "list"). 33 | Aria("label", "Ports of node"). 34 | Body( 35 | app.Range(portsToDisplay).Slice(func(j int) app.UI { 36 | return app.Li(). 37 | Class("pf-c-label-group__list-item"). 38 | Body( 39 | app.Span(). 40 | Class("pf-c-label"). 41 | Body( 42 | app. 43 | Span(). 44 | Class("pf-c-label__content"). 45 | Text(GetPortID(portsToDisplay[j])), 46 | ), 47 | ) 48 | }), 49 | app.If( 50 | // Only collapse if there are more than three ports 51 | len(c.Ports) > 3, 52 | app.Li(). 53 | Class("pf-c-label-group__list-item"). 54 | Body( 55 | app.Button(). 56 | Class("pf-c-label pf-m-overflow"). 57 | OnClick(func(ctx app.Context, e app.Event) { 58 | e.Call("stopPropagation") 59 | 60 | c.dispatch(func() { 61 | c.expanded = !c.expanded 62 | }) 63 | }). 64 | Body( 65 | app.Span(). 66 | Class("pf-c-label__content"). 67 | Body( 68 | app.If( 69 | c.expanded, 70 | app.Text( 71 | fmt.Sprintf("%v less", len(c.Ports)-3), 72 | ), 73 | ).Else( 74 | app.Text( 75 | fmt.Sprintf("%v more", len(c.Ports)-3), 76 | ), 77 | ), 78 | ), 79 | ), 80 | ), 81 | ), 82 | ), 83 | ), 84 | ) 85 | } 86 | 87 | func (c *PortList) dispatch(action func()) { 88 | action() 89 | 90 | c.Update() 91 | } 92 | -------------------------------------------------------------------------------- /pkg/components/port_selection_list.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "github.com/maxence-charriere/go-app/v8/pkg/app" 5 | "github.com/pojntfx/liwasc/pkg/providers" 6 | ) 7 | 8 | type PortSelectionList struct { 9 | app.Compo 10 | 11 | Ports []providers.Port 12 | SelectedPort string 13 | SetSelectedPort func(string) 14 | } 15 | 16 | func (c *PortSelectionList) Render() app.UI { 17 | return app.Ul(). 18 | Class("pf-c-data-list pf-u-my-lg"). 19 | ID("ports-in-inspector"). 20 | Aria("role", "list"). 21 | Aria("label", "Ports"). 22 | Body( 23 | app.Range(c.Ports).Slice(func(i int) app.UI { 24 | return app.Li(). 25 | Class(func() string { 26 | classes := "pf-c-data-list__item pf-m-selectable" 27 | 28 | if c.SelectedPort == GetPortID(c.Ports[i]) { 29 | classes += " pf-m-selected" 30 | } 31 | 32 | return classes 33 | }()). 34 | Aria("labelledby", "ports-in-inspector"). 35 | TabIndex(0). 36 | OnClick(func(ctx app.Context, e app.Event) { 37 | // Reset selected port 38 | if c.SelectedPort == GetPortID(c.Ports[i]) { 39 | c.SetSelectedPort("") 40 | 41 | return 42 | } 43 | 44 | // Set selected port 45 | c.SetSelectedPort(GetPortID(c.Ports[i])) 46 | }). 47 | Body( 48 | app.Div().Class("pf-c-data-list__item-row").Body( 49 | app.Div().Class("pf-c-data-list__item-content").Body( 50 | app.Div().Class("pf-c-data-list__cell").Text(GetPortID(c.Ports[i])), 51 | ), 52 | ), 53 | ) 54 | }), 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/components/progress_button.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "github.com/maxence-charriere/go-app/v8/pkg/app" 5 | ) 6 | 7 | type ProgressButton struct { 8 | app.Compo 9 | 10 | Loading bool 11 | Icon string 12 | Text string 13 | Secondary bool 14 | Classes string 15 | 16 | OnClick func(ctx app.Context, e app.Event) 17 | } 18 | 19 | func (c *ProgressButton) Render() app.UI { 20 | return app.If( 21 | c.Text == "", 22 | app.Button(). 23 | Class(func() string { 24 | classes := "pf-c-button pf-m-plain" 25 | 26 | if c.Loading { 27 | classes += " pf-m-progress pf-m-in-progress" 28 | } 29 | 30 | return classes 31 | }()). 32 | OnClick(func(ctx app.Context, e app.Event) { 33 | c.OnClick(ctx, e) 34 | }). 35 | Body( 36 | app.If(c.Loading, 37 | app.Span(). 38 | Class("pf-c-button__progress"). 39 | Body( 40 | app.Span(). 41 | Class("pf-c-spinner pf-m-md"). 42 | Aria("role", "progressbar"). 43 | Aria("valuetext", "Loading..."). 44 | Body( 45 | app.Span().Class("pf-c-spinner__clipper"), 46 | app.Span().Class("pf-c-spinner__lead-ball"), 47 | app.Span().Class("pf-c-spinner__tail-ball"), 48 | ), 49 | )).Else( 50 | app.I(). 51 | Class(c.Icon). 52 | Aria("hidden", true), 53 | ), 54 | ), 55 | ).Else( 56 | app.Button(). 57 | Class(func() string { 58 | classes := "pf-c-button pf-m-primary" 59 | 60 | if c.Secondary { 61 | classes = "pf-c-button pf-m-secondary" 62 | } 63 | 64 | if c.Loading { 65 | classes += " pf-m-progress pf-m-in-progress" 66 | } 67 | 68 | if c.Classes != "" { 69 | classes += " " + c.Classes 70 | } 71 | 72 | return classes 73 | }()). 74 | OnClick(func(ctx app.Context, e app.Event) { 75 | c.OnClick(ctx, e) 76 | }). 77 | Body( 78 | app.If(c.Loading, 79 | app.Span(). 80 | Class("pf-c-button__progress"). 81 | Body( 82 | app.Span(). 83 | Class("pf-c-spinner pf-m-md"). 84 | Aria("role", "progressbar"). 85 | Aria("valuetext", "Loading..."). 86 | Body( 87 | app.Span().Class("pf-c-spinner__clipper"), 88 | app.Span().Class("pf-c-spinner__lead-ball"), 89 | app.Span().Class("pf-c-spinner__tail-ball"), 90 | ), 91 | )).Else( 92 | app.Span(). 93 | Class("pf-c-button__icon pf-m-start"). 94 | Body( 95 | app.I(). 96 | Class(c.Icon). 97 | Aria("hidden", true), 98 | ), 99 | ), 100 | app.Text(c.Text), 101 | ), 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /pkg/components/property.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/maxence-charriere/go-app/v8/pkg/app" 8 | ) 9 | 10 | type Property struct { 11 | app.Compo 12 | 13 | Key string 14 | Icon string 15 | Value string 16 | Link bool 17 | } 18 | 19 | func (c *Property) Render() app.UI { 20 | return app.Div(). 21 | Class("pf-c-description-list__group"). 22 | Body( 23 | app.Dt(). 24 | Class("pf-c-description-list__term"). 25 | Body( 26 | app.If( 27 | c.Icon == "", 28 | app.Span(). 29 | Class("pf-c-description-list__text"). 30 | Text(c.Key), 31 | ).Else( 32 | app.Span(). 33 | Class("pf-c-description-list__text"). 34 | Body( 35 | app.I().Class(fmt.Sprintf("%v pf-u-mr-xs", c.Icon)).Aria("hidden", true), 36 | app.Text(c.Key), 37 | ), 38 | ), 39 | ), 40 | app.Dd(). 41 | Class("pf-c-description-list__description"). 42 | Body( 43 | app.Div(). 44 | Class("pf-c-description-list__text"). 45 | Body( 46 | app.If( 47 | c.Value == "", 48 | app.Text("Unregistered"), 49 | ).Else( 50 | app.If( 51 | c.Link, 52 | app.A(). 53 | Target("_blank"). 54 | Href( 55 | fmt.Sprintf( 56 | "https://duckduckgo.com/?q=%v", 57 | url.QueryEscape(c.Value), 58 | ), 59 | ). 60 | Text(c.Value), 61 | ).Else( 62 | app.Text(c.Value), 63 | ), 64 | ), 65 | ), 66 | ), 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/components/settings_form.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/maxence-charriere/go-app/v8/pkg/app" 7 | ) 8 | 9 | type SettingsForm struct { 10 | app.Compo 11 | 12 | NodeScanTimeout int64 13 | SetNodeScanTimeout func(int64) 14 | 15 | PortScanTimeout int64 16 | SetPortScanTimeout func(int64) 17 | 18 | NodeWakeTimeout int64 19 | SetNodeWakeTimeout func(int64) 20 | 21 | Submit func() 22 | } 23 | 24 | const ( 25 | // Names and IDs 26 | nodeScanTimeoutName = "nodeScanTimeout" 27 | portScanTimeoutName = "portScanTimeout" 28 | nodeScanMACAddressName = "nodeScanMACAddressTimeout" 29 | 30 | nodeWakeTimeoutName = "nodeWakeTimeout" 31 | nodeWakeMACAddressName = "nodeWakeMACAddressTimeout" 32 | 33 | // Default values 34 | defaultNodeWakeTimeout = 600000 35 | defaultNodeScanTimeout = 500 36 | defaultPortScanTimeout = 10 37 | ) 38 | 39 | func (c *SettingsForm) Render() app.UI { 40 | return app.Form(). 41 | Class("pf-c-form"). 42 | ID("settings"). 43 | Body( 44 | // Node Scan Timeout Input 45 | &FormGroup{ 46 | Label: app. 47 | Label(). 48 | For(nodeScanTimeoutName). 49 | Class("pf-c-form__label"). 50 | Body( 51 | app. 52 | Span(). 53 | Class("pf-c-form__label-text"). 54 | Text("Node Scan Timeout (in ms)"), 55 | ), 56 | Input: &Controlled{ 57 | Component: app. 58 | Input(). 59 | Name(nodeScanTimeoutName). 60 | ID(nodeScanTimeoutName). 61 | Type("number"). 62 | Required(true). 63 | Min(1). 64 | Step(1). 65 | Placeholder(strconv.Itoa(defaultNodeScanTimeout)). 66 | Class("pf-c-form-control"). 67 | OnInput(func(ctx app.Context, e app.Event) { 68 | v, err := strconv.Atoi(ctx.JSSrc.Get("value").String()) 69 | if err != nil || v == 0 { 70 | c.Update() 71 | 72 | return 73 | } 74 | 75 | c.SetNodeScanTimeout(int64(v)) 76 | }), 77 | Properties: map[string]interface{}{ 78 | "value": c.NodeScanTimeout, 79 | }, 80 | }, 81 | Required: true, 82 | }, 83 | // Port Scan Timeout Input 84 | &FormGroup{ 85 | Label: app. 86 | Label(). 87 | For(portScanTimeoutName). 88 | Class("pf-c-form__label"). 89 | Body( 90 | app. 91 | Span(). 92 | Class("pf-c-form__label-text"). 93 | Text("Port Scan Timeout (in ms)"), 94 | ), 95 | Input: &Controlled{ 96 | Component: app. 97 | Input(). 98 | Name(portScanTimeoutName). 99 | ID(portScanTimeoutName). 100 | Type("number"). 101 | Required(true). 102 | Min(1). 103 | Step(1). 104 | Placeholder(strconv.Itoa(defaultPortScanTimeout)). 105 | Class("pf-c-form-control"). 106 | OnInput(func(ctx app.Context, e app.Event) { 107 | v, err := strconv.Atoi(ctx.JSSrc.Get("value").String()) 108 | if err != nil || v == 0 { 109 | c.Update() 110 | 111 | return 112 | } 113 | 114 | c.SetPortScanTimeout(int64(v)) 115 | }), 116 | Properties: map[string]interface{}{ 117 | "value": c.PortScanTimeout, 118 | }, 119 | }, 120 | Required: true, 121 | }, 122 | // Node Wake Timeout Input 123 | &FormGroup{ 124 | Label: app. 125 | Label(). 126 | For(nodeWakeTimeoutName). 127 | Class("pf-c-form__label"). 128 | Body( 129 | app. 130 | Span(). 131 | Class("pf-c-form__label-text"). 132 | Text("Node Wake Timeout (in ms)"), 133 | ), 134 | Input: &Controlled{ 135 | Component: app. 136 | Input(). 137 | Name(nodeWakeTimeoutName). 138 | ID(nodeWakeTimeoutName). 139 | Type("number"). 140 | Required(true). 141 | Min(1). 142 | Step(1). 143 | Placeholder(strconv.Itoa(defaultNodeWakeTimeout)). 144 | Class("pf-c-form-control"). 145 | OnInput(func(ctx app.Context, e app.Event) { 146 | v, err := strconv.Atoi(ctx.JSSrc.Get("value").String()) 147 | if err != nil || v == 0 { 148 | c.Update() 149 | 150 | return 151 | } 152 | 153 | c.SetNodeWakeTimeout(int64(v)) 154 | 155 | c.Update() 156 | }), 157 | Properties: map[string]interface{}{ 158 | "value": c.NodeWakeTimeout, 159 | }, 160 | }, 161 | Required: true, 162 | }, 163 | ).OnSubmit(func(ctx app.Context, e app.Event) { 164 | e.PreventDefault() 165 | 166 | c.Submit() 167 | }) 168 | } 169 | -------------------------------------------------------------------------------- /pkg/components/setup_form.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "github.com/maxence-charriere/go-app/v8/pkg/app" 4 | 5 | type SetupForm struct { 6 | app.Compo 7 | 8 | Error error 9 | ErrorMessage string 10 | 11 | BackendURL string 12 | SetBackendURL func(string) 13 | 14 | OIDCIssuer string 15 | SetOIDCIssuer func(string) 16 | 17 | OIDCClientID string 18 | SetOIDCClientID func(string) 19 | 20 | OIDCRedirectURL string 21 | SetOIDCRedirectURL func(string) 22 | 23 | Submit func() 24 | } 25 | 26 | const ( 27 | // Names and IDs 28 | backendURLName = "backendURLName" 29 | oidcIssuerName = "oidcIssuer" 30 | oidcClientIDName = "oidcClientID" 31 | oidcRedirectURLName = "oidcRedirectURL" 32 | 33 | // Placeholders 34 | backendURLPlaceholder = "ws://localhost:15124" 35 | oidcIssuerPlaceholder = "https://pojntfx.eu.auth0.com/" 36 | oidcRedirectURLPlaceholder = "http://localhost:15125/" 37 | ) 38 | 39 | func (c *SetupForm) Render() app.UI { 40 | return app.Form(). 41 | Class("pf-c-form"). 42 | Body( 43 | // Error display 44 | app.If(c.Error != nil, app.P(). 45 | Class("pf-c-form__helper-text pf-m-error"). 46 | Aria("live", "polite"). 47 | Body( 48 | app.Span(). 49 | Class("pf-c-form__helper-text-icon"). 50 | Body( 51 | app.I(). 52 | Class("fas fa-exclamation-circle"). 53 | Aria("hidden", true), 54 | ), 55 | app.Text(c.ErrorMessage), 56 | ), 57 | ), 58 | // Backend URL Input 59 | &FormGroup{ 60 | Label: app. 61 | Label(). 62 | For(backendURLName). 63 | Class("pf-c-form__label"). 64 | Body( 65 | app. 66 | Span(). 67 | Class("pf-c-form__label-text"). 68 | Text("Backend URL"), 69 | ), 70 | Input: &Controlled{ 71 | Component: app. 72 | Input(). 73 | Name(backendURLName). 74 | ID(backendURLName). 75 | Type("url"). 76 | Required(true). 77 | Placeholder(backendURLPlaceholder). 78 | Class("pf-c-form-control"). 79 | Aria("invalid", c.Error != nil). 80 | OnInput(func(ctx app.Context, e app.Event) { 81 | c.SetBackendURL(ctx.JSSrc.Get("value").String()) 82 | }), 83 | Properties: map[string]interface{}{ 84 | "value": c.BackendURL, 85 | }, 86 | }, 87 | Required: true, 88 | }, 89 | // OIDC Issuer Input 90 | &FormGroup{ 91 | Label: app. 92 | Label(). 93 | For(oidcIssuerName). 94 | Class("pf-c-form__label"). 95 | Body( 96 | app. 97 | Span(). 98 | Class("pf-c-form__label-text"). 99 | Text("OIDC Issuer"), 100 | ), 101 | Input: &Controlled{ 102 | Component: app. 103 | Input(). 104 | Name(oidcIssuerName). 105 | ID(oidcIssuerName). 106 | Type("url"). 107 | Required(true). 108 | Placeholder(oidcIssuerPlaceholder). 109 | Class("pf-c-form-control"). 110 | Aria("invalid", c.Error != nil). 111 | OnInput(func(ctx app.Context, e app.Event) { 112 | c.SetOIDCIssuer(ctx.JSSrc.Get("value").String()) 113 | }), 114 | Properties: map[string]interface{}{ 115 | "value": c.OIDCIssuer, 116 | }, 117 | }, 118 | Required: true, 119 | }, 120 | // OIDC Client ID 121 | &FormGroup{ 122 | Label: app. 123 | Label(). 124 | For(oidcClientIDName). 125 | Class("pf-c-form__label"). 126 | Body( 127 | app. 128 | Span(). 129 | Class("pf-c-form__label-text"). 130 | Text("OIDC Client ID"), 131 | ), 132 | Input: &Controlled{ 133 | Component: app. 134 | Input(). 135 | Name(oidcClientIDName). 136 | ID(oidcClientIDName). 137 | Type("text"). 138 | Required(true). 139 | Class("pf-c-form-control"). 140 | Aria("invalid", c.Error != nil). 141 | OnInput(func(ctx app.Context, e app.Event) { 142 | c.SetOIDCClientID(ctx.JSSrc.Get("value").String()) 143 | }), 144 | Properties: map[string]interface{}{ 145 | "value": c.OIDCClientID, 146 | }, 147 | }, 148 | Required: true, 149 | }, 150 | // OIDC Redirect URL 151 | &FormGroup{ 152 | Label: app. 153 | Label(). 154 | For(oidcRedirectURLName). 155 | Class("pf-c-form__label"). 156 | Body( 157 | app. 158 | Span(). 159 | Class("pf-c-form__label-text"). 160 | Text("OIDC Redirect URL"), 161 | ), 162 | Input: &Controlled{ 163 | Component: app. 164 | Input(). 165 | Name(oidcRedirectURLName). 166 | ID(oidcRedirectURLName). 167 | Type("url"). 168 | Required(true). 169 | Placeholder(oidcRedirectURLPlaceholder). 170 | Class("pf-c-form-control"). 171 | Aria("invalid", c.Error != nil). 172 | OnInput(func(ctx app.Context, e app.Event) { 173 | c.SetOIDCRedirectURL(ctx.JSSrc.Get("value").String()) 174 | }), 175 | Properties: map[string]interface{}{ 176 | "value": c.OIDCRedirectURL, 177 | }, 178 | }, 179 | Required: true, 180 | }, 181 | // Configuration Apply Trigger 182 | app.Div(). 183 | Class("pf-c-form__group pf-m-action"). 184 | Body( 185 | app. 186 | Button(). 187 | Type("submit"). 188 | Class("pf-c-button pf-m-primary pf-m-block"). 189 | Text("Log in"), 190 | ), 191 | ).OnSubmit(func(ctx app.Context, e app.Event) { 192 | e.PreventDefault() 193 | 194 | c.Submit() 195 | }) 196 | } 197 | -------------------------------------------------------------------------------- /pkg/components/setup_shell.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "github.com/maxence-charriere/go-app/v8/pkg/app" 5 | ) 6 | 7 | type SetupShell struct { 8 | app.Compo 9 | 10 | LogoSrc string 11 | Title string 12 | ShortDescription string 13 | LongDescription string 14 | HelpLink string 15 | Links map[string]string 16 | 17 | BackendURL string 18 | OIDCIssuer string 19 | OIDCClientID string 20 | OIDCRedirectURL string 21 | 22 | SetBackendURL, 23 | SetOIDCIssuer, 24 | SetOIDCClientID, 25 | SetOIDCRedirectURL func(string) 26 | ApplyConfig func() 27 | 28 | Error error 29 | } 30 | 31 | func (c *SetupShell) Render() app.UI { 32 | // Display the error message if error != nil 33 | errorMessage := "" 34 | if c.Error != nil { 35 | errorMessage = c.Error.Error() 36 | } 37 | 38 | return app.Div(). 39 | Class("pf-u-h-100"). 40 | Body( 41 | app.Div(). 42 | Class("pf-c-background-image"). 43 | Body( 44 | app.Raw(` 50 | 51 | 55 | 59 | 63 | 67 | 71 | 72 | 73 | 74 | `), 75 | ), 76 | app.Div().Class("pf-c-login").Body( 77 | app.Div().Class("pf-c-login__container").Body( 78 | app.Header().Class("pf-c-login__header").Body( 79 | app.Img(). 80 | Class("pf-c-brand pf-x-c-brand--main"). 81 | Src(c.LogoSrc). 82 | Alt("Logo"), 83 | ), 84 | app.Main().Class("pf-c-login__main").Body( 85 | app.Header().Class("pf-c-login__main-header").Body( 86 | app.H1().Class("pf-c-title pf-m-3xl").Text( 87 | c.Title, 88 | ), 89 | app.P().Class("pf-c-login__main-header-desc").Text( 90 | c.ShortDescription, 91 | ), 92 | ), 93 | app.Div().Class("pf-c-login__main-body").Body( 94 | &SetupForm{ 95 | Error: c.Error, 96 | ErrorMessage: errorMessage, 97 | 98 | BackendURL: c.BackendURL, 99 | SetBackendURL: c.SetBackendURL, 100 | 101 | OIDCIssuer: c.OIDCIssuer, 102 | SetOIDCIssuer: c.SetOIDCIssuer, 103 | 104 | OIDCClientID: c.OIDCClientID, 105 | SetOIDCClientID: c.SetOIDCClientID, 106 | 107 | OIDCRedirectURL: c.OIDCRedirectURL, 108 | SetOIDCRedirectURL: c.SetOIDCRedirectURL, 109 | 110 | Submit: c.ApplyConfig, 111 | }, 112 | ), 113 | app.Footer().Class("pf-c-login__main-footer").Body( 114 | app.Div().Class("pf-c-login__main-footer-band").Body( 115 | app.P().Class("pf-c-login__main-footer-band-item").Body( 116 | app.Text("Not sure what to do? "), 117 | app.A(). 118 | Href(c.HelpLink). 119 | Target("_blank"). 120 | Text("Get help."), 121 | ), 122 | ), 123 | ), 124 | ), 125 | app.Footer().Class("pf-c-login__footer").Body( 126 | app.P().Text( 127 | c.LongDescription, 128 | ), 129 | app.Ul().Class("pf-c-list pf-m-inline").Body( 130 | app.Range(c.Links).Map(func(s string) app.UI { 131 | return app.Li().Body( 132 | app. 133 | A(). 134 | Target("_blank"). 135 | Href(c.Links[s]). 136 | Text(s), 137 | ) 138 | }), 139 | ), 140 | ), 141 | ), 142 | ), 143 | ) 144 | 145 | } 146 | -------------------------------------------------------------------------------- /pkg/components/status.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "github.com/maxence-charriere/go-app/v8/pkg/app" 4 | 5 | type Status struct { 6 | app.Compo 7 | 8 | Error error 9 | ErrorText string 10 | Recover func() 11 | RecoverText string 12 | Ignore func() 13 | } 14 | 15 | func (c *Status) Render() app.UI { 16 | // Display the error message if error != nil 17 | errorMessage := "" 18 | if c.Error != nil { 19 | errorMessage = c.Error.Error() 20 | } 21 | 22 | return app.If(c.Error != nil, app.Div(). 23 | Class("pf-c-alert pf-m-danger"). 24 | Aria("label", c.ErrorText). 25 | Body( 26 | app.Div(). 27 | Class("pf-c-alert__icon"). 28 | Body( 29 | app.I(). 30 | Class("fas fa-fw fa-exclamation-circle"). 31 | Aria("hidden", true), 32 | ), 33 | app.P(). 34 | Class("pf-c-alert__title"). 35 | Body( 36 | app.Strong().Body( 37 | app.Span(). 38 | Class("pf-screen-reader"). 39 | Text(c.ErrorText), 40 | ), 41 | app.Text(c.ErrorText), 42 | ), 43 | app.Div(). 44 | Class("pf-c-alert__action"). 45 | Body( 46 | app.Button(). 47 | Class("pf-c-button pf-m-plain"). 48 | Aria("label", "Ignore error"). 49 | OnClick(func(ctx app.Context, e app.Event) { 50 | c.Ignore() 51 | }). 52 | Body( 53 | app.I(). 54 | Class("fas fa-times"). 55 | Aria("hidden", true), 56 | ), 57 | ), 58 | app.Div(). 59 | Class("pf-c-alert__description"). 60 | Body( 61 | app.P().Body( 62 | app.Code(). 63 | Text(errorMessage), 64 | ), 65 | ), 66 | app.If(c.Recover != nil, 67 | app.Div(). 68 | Class("pf-c-alert__action-group"). 69 | Body( 70 | app.Button(). 71 | Class("pf-c-button pf-m-link pf-m-inline"). 72 | Type("button"). 73 | OnClick(func(ctx app.Context, e app.Event) { 74 | c.Recover() 75 | }). 76 | Text(c.RecoverText), 77 | ), 78 | ), 79 | )).Else(app.Span()) 80 | } 81 | -------------------------------------------------------------------------------- /pkg/components/toolbar.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "github.com/maxence-charriere/go-app/v8/pkg/app" 4 | 5 | type Toolbar struct { 6 | app.Compo 7 | 8 | NodeScanRunning bool 9 | TriggerFullNetworkScan func() 10 | 11 | LastNodeScanDate string 12 | Subnets []string 13 | Device string 14 | 15 | ToggleMetadataDialogOpen func() 16 | } 17 | 18 | func (c *Toolbar) Render() app.UI { 19 | return app.Div(). 20 | Class("pf-c-toolbar pf-m-page-insets"). 21 | Body( 22 | app.Div(). 23 | Class("pf-c-toolbar__content"). 24 | Body( 25 | app.Div(). 26 | Class("pf-c-toolbar__content-section pf-m-nowrap pf-u-display-none pf-u-display-flex-on-lg"). 27 | Body( 28 | app.Div(). 29 | Class("pf-c-toolbar__item"). 30 | Body( 31 | // Data actions 32 | &ProgressButton{ 33 | Loading: c.NodeScanRunning, 34 | Icon: "fas fa-rocket", 35 | Text: "Trigger Scan", 36 | 37 | OnClick: func(ctx app.Context, e app.Event) { 38 | c.TriggerFullNetworkScan() 39 | }, 40 | }, 41 | ), 42 | app.Div(). 43 | Class("pf-c-toolbar__item"). 44 | Body( 45 | app.Div(). 46 | Class("pf-c-label-group pf-m-category"). 47 | Body( 48 | app.Div(). 49 | Class("pf-c-label-group__main"). 50 | Body( 51 | app.Span(). 52 | Class("pf-c-label-group__label"). 53 | Aria("hidden", true). 54 | ID("last-scan"). 55 | Body( 56 | app.I(). 57 | Class("fas fa-history pf-u-mr-xs"). 58 | Aria("hidden", true), 59 | app.Text("Last Scan"), 60 | ), 61 | app.Ul(). 62 | Class("pf-c-label-group__list"). 63 | Aria("role", "list"). 64 | Aria("labelledby", "last-scan"). 65 | Body( 66 | app.Li(). 67 | Class("pf-c-label-group__list-item"). 68 | Body( 69 | app.Span(). 70 | Class("pf-c-label"). 71 | Body( 72 | app.Span(). 73 | Class("pf-c-label__content"). 74 | Body( 75 | app.Text(c.LastNodeScanDate), 76 | ), 77 | ), 78 | ), 79 | ), 80 | ), 81 | ), 82 | ), 83 | app.Div().Class("pf-c-toolbar__item pf-m-pagination").Body( 84 | app.Div(). 85 | Class("pf-c-label-group pf-m-category pf-u-mr-md"). 86 | Body( 87 | app.Div(). 88 | Class("pf-c-label-group__main"). 89 | Body( 90 | app.Span(). 91 | Class("pf-c-label-group__label"). 92 | Aria("hidden", true). 93 | ID("subnets"). 94 | Body( 95 | app.I(). 96 | Class("fas fa-network-wired pf-u-mr-xs"). 97 | Aria("hidden", true), 98 | app.Text("Subnets"), 99 | ), 100 | app.Ul(). 101 | Class("pf-c-label-group__list"). 102 | Aria("role", "list"). 103 | Aria("labelledby", "subnets"). 104 | Body( 105 | app.Range(c.Subnets).Slice(func(i int) app.UI { 106 | return app.Li(). 107 | Class("pf-c-label-group__list-item"). 108 | Body( 109 | app.Span(). 110 | Class("pf-c-label"). 111 | Body( 112 | app.Span(). 113 | Class("pf-c-label__content"). 114 | Body( 115 | app.Text(c.Subnets[i]), 116 | ), 117 | ), 118 | ) 119 | }), 120 | ), 121 | ), 122 | ), 123 | app.Div(). 124 | Class("pf-c-label-group pf-m-category"). 125 | Body( 126 | app.Div(). 127 | Class("pf-c-label-group__main"). 128 | Body( 129 | app.Span(). 130 | Class("pf-c-label-group__label"). 131 | Aria("hidden", true). 132 | ID("device"). 133 | Body( 134 | app.I(). 135 | Class("fas fa-microchip pf-u-mr-xs"). 136 | Aria("hidden", true), 137 | app.Text("Device"), 138 | ), 139 | app.Ul(). 140 | Class("pf-c-label-group__list"). 141 | Aria("role", "list"). 142 | Aria("labelledby", "device"). 143 | Body( 144 | app.Li(). 145 | Class("pf-c-label-group__list-item"). 146 | Body( 147 | app.Span(). 148 | Class("pf-c-label"). 149 | Body( 150 | app.Span(). 151 | Class("pf-c-label__content"). 152 | Body( 153 | app.Text(c.Device), 154 | ), 155 | ), 156 | ), 157 | ), 158 | ), 159 | ), 160 | ), 161 | ), 162 | app.Div(). 163 | Class("pf-c-toolbar__content-section pf-m-nowrap pf-u-display-flex pf-u-display-none-on-lg"). 164 | Body( 165 | app.Div(). 166 | Class("pf-c-toolbar__item"). 167 | Body( 168 | // Data actions 169 | &ProgressButton{ 170 | Loading: c.NodeScanRunning, 171 | Icon: "fas fa-rocket", 172 | Text: "Trigger Scan", 173 | 174 | OnClick: func(ctx app.Context, e app.Event) { 175 | c.TriggerFullNetworkScan() 176 | }, 177 | }, 178 | ), 179 | app.Div(). 180 | Class("pf-c-toolbar__item pf-m-pagination"). 181 | Body( 182 | app.Button(). 183 | Class("pf-c-button pf-m-plain"). 184 | Type("button"). 185 | Aria("label", "Metadata"). 186 | OnClick(func(ctx app.Context, e app.Event) { 187 | c.ToggleMetadataDialogOpen() 188 | }). 189 | Body( 190 | app.I(). 191 | Class("fas fa-info-circle"). 192 | Aria("hidden", true), 193 | ), 194 | ), 195 | ), 196 | ), 197 | ) 198 | } 199 | -------------------------------------------------------------------------------- /pkg/db/sqlite/migrations/node_and_port_scan/migrations.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bindata. DO NOT EDIT. 2 | // sources: 3 | // ../../db/sqlite/migrations/node_and_port_scan/1616878410007.sql 4 | 5 | package node_and_port_scan 6 | 7 | 8 | import ( 9 | "bytes" 10 | "compress/gzip" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | func bindataRead(data []byte, name string) ([]byte, error) { 21 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 22 | if err != nil { 23 | return nil, fmt.Errorf("Read %q: %v", name, err) 24 | } 25 | 26 | var buf bytes.Buffer 27 | _, err = io.Copy(&buf, gz) 28 | clErr := gz.Close() 29 | 30 | if err != nil { 31 | return nil, fmt.Errorf("Read %q: %v", name, err) 32 | } 33 | if clErr != nil { 34 | return nil, err 35 | } 36 | 37 | return buf.Bytes(), nil 38 | } 39 | 40 | 41 | type asset struct { 42 | bytes []byte 43 | info fileInfoEx 44 | } 45 | 46 | type fileInfoEx interface { 47 | os.FileInfo 48 | MD5Checksum() string 49 | } 50 | 51 | type bindataFileInfo struct { 52 | name string 53 | size int64 54 | mode os.FileMode 55 | modTime time.Time 56 | md5checksum string 57 | } 58 | 59 | func (fi bindataFileInfo) Name() string { 60 | return fi.name 61 | } 62 | func (fi bindataFileInfo) Size() int64 { 63 | return fi.size 64 | } 65 | func (fi bindataFileInfo) Mode() os.FileMode { 66 | return fi.mode 67 | } 68 | func (fi bindataFileInfo) ModTime() time.Time { 69 | return fi.modTime 70 | } 71 | func (fi bindataFileInfo) MD5Checksum() string { 72 | return fi.md5checksum 73 | } 74 | func (fi bindataFileInfo) IsDir() bool { 75 | return false 76 | } 77 | func (fi bindataFileInfo) Sys() interface{} { 78 | return nil 79 | } 80 | 81 | var _bindataDbSqliteMigrationsNodeandportscan1616878410007Sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x93\xc1\x6a\xc3\x30\x0c\x86\xef\x79\x0a\x1d\x13\xb6\x3e\x41\xae\x7b\x85\x9d\x83\x6b\xa9\xc1\x2c\x96\x8c\xac\xb2\xf5\xed\x47\x12\xda\xba\x75\x3a\x36\x28\xbb\x19\xe9\xe7\xb7\xbe\x5f\x68\xb7\x83\x97\x18\x46\x75\x46\xf0\x9e\x1a\xaf\x34\xbf\xcc\xed\x27\x02\x16\xa4\x21\x7b\xc7\x19\xda\x06\x00\x20\x20\x04\x36\x1a\x49\x81\xc5\x80\x8f\xd3\x04\x49\x43\x74\x7a\x82\x0f\x3a\xbd\x2e\xa2\xd5\x02\x07\x67\x80\xb3\xd7\x59\xb9\x76\x51\x98\x2a\x93\xa6\xeb\xeb\x9f\x9f\xf8\x69\x74\x7e\x70\x88\x4a\x39\x83\xd1\x97\xdd\xb5\x43\xfa\xa9\x7b\x49\x61\xd8\x98\x64\x95\x1c\x44\x29\x8c\x3c\x8f\x03\x6d\xa9\xef\x40\xe9\x40\x4a\xec\x29\x17\x71\xb6\x01\xbb\x8a\x39\x89\xda\xbf\xa4\x5d\x60\xfd\x81\x68\x03\xe6\x31\xc7\x13\x11\x96\x58\xf8\x18\xf7\xa4\x0f\x66\x35\x75\x9c\x17\x59\x52\x31\xf1\x32\x6d\xad\xf1\x12\xef\x2f\xa1\x4b\xfd\x0d\xf9\x75\x4f\x67\xfc\xf2\x86\xde\xe4\x93\x1b\x54\x49\xd5\x0d\xf5\xf7\xe5\xdb\xca\xd5\xb6\x2a\xe7\xfe\x3b\x00\x00\xff\xff\xdf\x4f\xbe\xc5\xa6\x03\x00\x00") 82 | 83 | func bindataDbSqliteMigrationsNodeandportscan1616878410007SqlBytes() ([]byte, error) { 84 | return bindataRead( 85 | _bindataDbSqliteMigrationsNodeandportscan1616878410007Sql, 86 | "../../db/sqlite/migrations/node_and_port_scan/1616878410007.sql", 87 | ) 88 | } 89 | 90 | 91 | 92 | func bindataDbSqliteMigrationsNodeandportscan1616878410007Sql() (*asset, error) { 93 | bytes, err := bindataDbSqliteMigrationsNodeandportscan1616878410007SqlBytes() 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | info := bindataFileInfo{ 99 | name: "../../db/sqlite/migrations/node_and_port_scan/1616878410007.sql", 100 | size: 934, 101 | md5checksum: "", 102 | mode: os.FileMode(420), 103 | modTime: time.Unix(1617034594, 0), 104 | } 105 | 106 | a := &asset{bytes: bytes, info: info} 107 | 108 | return a, nil 109 | } 110 | 111 | 112 | // 113 | // Asset loads and returns the asset for the given name. 114 | // It returns an error if the asset could not be found or 115 | // could not be loaded. 116 | // 117 | func Asset(name string) ([]byte, error) { 118 | cannonicalName := strings.Replace(name, "\\", "/", -1) 119 | if f, ok := _bindata[cannonicalName]; ok { 120 | a, err := f() 121 | if err != nil { 122 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) 123 | } 124 | return a.bytes, nil 125 | } 126 | return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} 127 | } 128 | 129 | // 130 | // MustAsset is like Asset but panics when Asset would return an error. 131 | // It simplifies safe initialization of global variables. 132 | // nolint: deadcode 133 | // 134 | func MustAsset(name string) []byte { 135 | a, err := Asset(name) 136 | if err != nil { 137 | panic("asset: Asset(" + name + "): " + err.Error()) 138 | } 139 | 140 | return a 141 | } 142 | 143 | // 144 | // AssetInfo loads and returns the asset info for the given name. 145 | // It returns an error if the asset could not be found or could not be loaded. 146 | // 147 | func AssetInfo(name string) (os.FileInfo, error) { 148 | cannonicalName := strings.Replace(name, "\\", "/", -1) 149 | if f, ok := _bindata[cannonicalName]; ok { 150 | a, err := f() 151 | if err != nil { 152 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) 153 | } 154 | return a.info, nil 155 | } 156 | return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} 157 | } 158 | 159 | // 160 | // AssetNames returns the names of the assets. 161 | // nolint: deadcode 162 | // 163 | func AssetNames() []string { 164 | names := make([]string, 0, len(_bindata)) 165 | for name := range _bindata { 166 | names = append(names, name) 167 | } 168 | return names 169 | } 170 | 171 | // 172 | // _bindata is a table, holding each asset generator, mapped to its name. 173 | // 174 | var _bindata = map[string]func() (*asset, error){ 175 | "../../db/sqlite/migrations/node_and_port_scan/1616878410007.sql": bindataDbSqliteMigrationsNodeandportscan1616878410007Sql, 176 | } 177 | 178 | // 179 | // AssetDir returns the file names below a certain 180 | // directory embedded in the file by go-bindata. 181 | // For example if you run go-bindata on data/... and data contains the 182 | // following hierarchy: 183 | // data/ 184 | // foo.txt 185 | // img/ 186 | // a.png 187 | // b.png 188 | // then AssetDir("data") would return []string{"foo.txt", "img"} 189 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 190 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 191 | // AssetDir("") will return []string{"data"}. 192 | // 193 | func AssetDir(name string) ([]string, error) { 194 | node := _bintree 195 | if len(name) != 0 { 196 | cannonicalName := strings.Replace(name, "\\", "/", -1) 197 | pathList := strings.Split(cannonicalName, "/") 198 | for _, p := range pathList { 199 | node = node.Children[p] 200 | if node == nil { 201 | return nil, &os.PathError{ 202 | Op: "open", 203 | Path: name, 204 | Err: os.ErrNotExist, 205 | } 206 | } 207 | } 208 | } 209 | if node.Func != nil { 210 | return nil, &os.PathError{ 211 | Op: "open", 212 | Path: name, 213 | Err: os.ErrNotExist, 214 | } 215 | } 216 | rv := make([]string, 0, len(node.Children)) 217 | for childName := range node.Children { 218 | rv = append(rv, childName) 219 | } 220 | return rv, nil 221 | } 222 | 223 | 224 | type bintree struct { 225 | Func func() (*asset, error) 226 | Children map[string]*bintree 227 | } 228 | 229 | var _bintree = &bintree{Func: nil, Children: map[string]*bintree{ 230 | "..": {Func: nil, Children: map[string]*bintree{ 231 | "..": {Func: nil, Children: map[string]*bintree{ 232 | "db": {Func: nil, Children: map[string]*bintree{ 233 | "sqlite": {Func: nil, Children: map[string]*bintree{ 234 | "migrations": {Func: nil, Children: map[string]*bintree{ 235 | "node_and_port_scan": {Func: nil, Children: map[string]*bintree{ 236 | "1616878410007.sql": {Func: bindataDbSqliteMigrationsNodeandportscan1616878410007Sql, Children: map[string]*bintree{}}, 237 | }}, 238 | }}, 239 | }}, 240 | }}, 241 | }}, 242 | }}, 243 | }} 244 | 245 | // RestoreAsset restores an asset under the given directory 246 | func RestoreAsset(dir, name string) error { 247 | data, err := Asset(name) 248 | if err != nil { 249 | return err 250 | } 251 | info, err := AssetInfo(name) 252 | if err != nil { 253 | return err 254 | } 255 | err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) 256 | if err != nil { 257 | return err 258 | } 259 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) 260 | if err != nil { 261 | return err 262 | } 263 | return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) 264 | } 265 | 266 | // RestoreAssets restores an asset under the given directory recursively 267 | func RestoreAssets(dir, name string) error { 268 | children, err := AssetDir(name) 269 | // File 270 | if err != nil { 271 | return RestoreAsset(dir, name) 272 | } 273 | // Dir 274 | for _, child := range children { 275 | err = RestoreAssets(dir, filepath.Join(name, child)) 276 | if err != nil { 277 | return err 278 | } 279 | } 280 | return nil 281 | } 282 | 283 | func _filePath(dir, name string) string { 284 | cannonicalName := strings.Replace(name, "\\", "/", -1) 285 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) 286 | } 287 | -------------------------------------------------------------------------------- /pkg/db/sqlite/migrations/node_wake/migrations.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bindata. DO NOT EDIT. 2 | // sources: 3 | // ../../db/sqlite/migrations/node_wake/1616878455601.sql 4 | 5 | package node_wake 6 | 7 | 8 | import ( 9 | "bytes" 10 | "compress/gzip" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | func bindataRead(data []byte, name string) ([]byte, error) { 21 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 22 | if err != nil { 23 | return nil, fmt.Errorf("Read %q: %v", name, err) 24 | } 25 | 26 | var buf bytes.Buffer 27 | _, err = io.Copy(&buf, gz) 28 | clErr := gz.Close() 29 | 30 | if err != nil { 31 | return nil, fmt.Errorf("Read %q: %v", name, err) 32 | } 33 | if clErr != nil { 34 | return nil, err 35 | } 36 | 37 | return buf.Bytes(), nil 38 | } 39 | 40 | 41 | type asset struct { 42 | bytes []byte 43 | info fileInfoEx 44 | } 45 | 46 | type fileInfoEx interface { 47 | os.FileInfo 48 | MD5Checksum() string 49 | } 50 | 51 | type bindataFileInfo struct { 52 | name string 53 | size int64 54 | mode os.FileMode 55 | modTime time.Time 56 | md5checksum string 57 | } 58 | 59 | func (fi bindataFileInfo) Name() string { 60 | return fi.name 61 | } 62 | func (fi bindataFileInfo) Size() int64 { 63 | return fi.size 64 | } 65 | func (fi bindataFileInfo) Mode() os.FileMode { 66 | return fi.mode 67 | } 68 | func (fi bindataFileInfo) ModTime() time.Time { 69 | return fi.modTime 70 | } 71 | func (fi bindataFileInfo) MD5Checksum() string { 72 | return fi.md5checksum 73 | } 74 | func (fi bindataFileInfo) IsDir() bool { 75 | return false 76 | } 77 | func (fi bindataFileInfo) Sys() interface{} { 78 | return nil 79 | } 80 | 81 | var _bindataDbSqliteMigrationsNodewake1616878455601Sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x64\x8f\x41\x0a\xc2\x40\x0c\x45\xf7\x73\x8a\xbf\x54\xb4\x27\xe8\xd6\x2b\xb8\x1e\x62\x13\xca\xd0\x36\x19\xd2\x48\xed\xed\xa5\x16\x41\xec\x2e\xf0\x7e\x1e\xbc\xa6\xc1\x65\x2a\xbd\x53\x08\xee\x35\x75\x2e\xdb\x15\xf4\x18\x05\x6a\x2c\x79\xa1\x41\x66\x9c\x12\x00\x14\x46\xd1\x90\x5e\x1c\x6a\x01\x7d\x8e\x23\xaa\x97\x89\x7c\xc5\x20\xeb\xf5\x33\xda\x15\x9c\x29\xc0\x9b\xeb\xbb\xdc\x29\x9b\xca\x41\xb2\xa3\x89\xba\x4c\xcc\x2e\xf3\x8c\x90\x57\xfc\xe1\x6a\x8b\xb8\x70\x36\x3d\xfc\xa7\x73\x9b\x7e\x43\x6e\xb6\x68\x62\xb7\x7a\x08\x69\xdf\x01\x00\x00\xff\xff\x1a\xcc\x56\xf4\xf0\x00\x00\x00") 82 | 83 | func bindataDbSqliteMigrationsNodewake1616878455601SqlBytes() ([]byte, error) { 84 | return bindataRead( 85 | _bindataDbSqliteMigrationsNodewake1616878455601Sql, 86 | "../../db/sqlite/migrations/node_wake/1616878455601.sql", 87 | ) 88 | } 89 | 90 | 91 | 92 | func bindataDbSqliteMigrationsNodewake1616878455601Sql() (*asset, error) { 93 | bytes, err := bindataDbSqliteMigrationsNodewake1616878455601SqlBytes() 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | info := bindataFileInfo{ 99 | name: "../../db/sqlite/migrations/node_wake/1616878455601.sql", 100 | size: 240, 101 | md5checksum: "", 102 | mode: os.FileMode(420), 103 | modTime: time.Unix(1617034594, 0), 104 | } 105 | 106 | a := &asset{bytes: bytes, info: info} 107 | 108 | return a, nil 109 | } 110 | 111 | 112 | // 113 | // Asset loads and returns the asset for the given name. 114 | // It returns an error if the asset could not be found or 115 | // could not be loaded. 116 | // 117 | func Asset(name string) ([]byte, error) { 118 | cannonicalName := strings.Replace(name, "\\", "/", -1) 119 | if f, ok := _bindata[cannonicalName]; ok { 120 | a, err := f() 121 | if err != nil { 122 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) 123 | } 124 | return a.bytes, nil 125 | } 126 | return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} 127 | } 128 | 129 | // 130 | // MustAsset is like Asset but panics when Asset would return an error. 131 | // It simplifies safe initialization of global variables. 132 | // nolint: deadcode 133 | // 134 | func MustAsset(name string) []byte { 135 | a, err := Asset(name) 136 | if err != nil { 137 | panic("asset: Asset(" + name + "): " + err.Error()) 138 | } 139 | 140 | return a 141 | } 142 | 143 | // 144 | // AssetInfo loads and returns the asset info for the given name. 145 | // It returns an error if the asset could not be found or could not be loaded. 146 | // 147 | func AssetInfo(name string) (os.FileInfo, error) { 148 | cannonicalName := strings.Replace(name, "\\", "/", -1) 149 | if f, ok := _bindata[cannonicalName]; ok { 150 | a, err := f() 151 | if err != nil { 152 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) 153 | } 154 | return a.info, nil 155 | } 156 | return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} 157 | } 158 | 159 | // 160 | // AssetNames returns the names of the assets. 161 | // nolint: deadcode 162 | // 163 | func AssetNames() []string { 164 | names := make([]string, 0, len(_bindata)) 165 | for name := range _bindata { 166 | names = append(names, name) 167 | } 168 | return names 169 | } 170 | 171 | // 172 | // _bindata is a table, holding each asset generator, mapped to its name. 173 | // 174 | var _bindata = map[string]func() (*asset, error){ 175 | "../../db/sqlite/migrations/node_wake/1616878455601.sql": bindataDbSqliteMigrationsNodewake1616878455601Sql, 176 | } 177 | 178 | // 179 | // AssetDir returns the file names below a certain 180 | // directory embedded in the file by go-bindata. 181 | // For example if you run go-bindata on data/... and data contains the 182 | // following hierarchy: 183 | // data/ 184 | // foo.txt 185 | // img/ 186 | // a.png 187 | // b.png 188 | // then AssetDir("data") would return []string{"foo.txt", "img"} 189 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 190 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 191 | // AssetDir("") will return []string{"data"}. 192 | // 193 | func AssetDir(name string) ([]string, error) { 194 | node := _bintree 195 | if len(name) != 0 { 196 | cannonicalName := strings.Replace(name, "\\", "/", -1) 197 | pathList := strings.Split(cannonicalName, "/") 198 | for _, p := range pathList { 199 | node = node.Children[p] 200 | if node == nil { 201 | return nil, &os.PathError{ 202 | Op: "open", 203 | Path: name, 204 | Err: os.ErrNotExist, 205 | } 206 | } 207 | } 208 | } 209 | if node.Func != nil { 210 | return nil, &os.PathError{ 211 | Op: "open", 212 | Path: name, 213 | Err: os.ErrNotExist, 214 | } 215 | } 216 | rv := make([]string, 0, len(node.Children)) 217 | for childName := range node.Children { 218 | rv = append(rv, childName) 219 | } 220 | return rv, nil 221 | } 222 | 223 | 224 | type bintree struct { 225 | Func func() (*asset, error) 226 | Children map[string]*bintree 227 | } 228 | 229 | var _bintree = &bintree{Func: nil, Children: map[string]*bintree{ 230 | "..": {Func: nil, Children: map[string]*bintree{ 231 | "..": {Func: nil, Children: map[string]*bintree{ 232 | "db": {Func: nil, Children: map[string]*bintree{ 233 | "sqlite": {Func: nil, Children: map[string]*bintree{ 234 | "migrations": {Func: nil, Children: map[string]*bintree{ 235 | "node_wake": {Func: nil, Children: map[string]*bintree{ 236 | "1616878455601.sql": {Func: bindataDbSqliteMigrationsNodewake1616878455601Sql, Children: map[string]*bintree{}}, 237 | }}, 238 | }}, 239 | }}, 240 | }}, 241 | }}, 242 | }}, 243 | }} 244 | 245 | // RestoreAsset restores an asset under the given directory 246 | func RestoreAsset(dir, name string) error { 247 | data, err := Asset(name) 248 | if err != nil { 249 | return err 250 | } 251 | info, err := AssetInfo(name) 252 | if err != nil { 253 | return err 254 | } 255 | err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) 256 | if err != nil { 257 | return err 258 | } 259 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) 260 | if err != nil { 261 | return err 262 | } 263 | return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) 264 | } 265 | 266 | // RestoreAssets restores an asset under the given directory recursively 267 | func RestoreAssets(dir, name string) error { 268 | children, err := AssetDir(name) 269 | // File 270 | if err != nil { 271 | return RestoreAsset(dir, name) 272 | } 273 | // Dir 274 | for _, child := range children { 275 | err = RestoreAssets(dir, filepath.Join(name, child)) 276 | if err != nil { 277 | return err 278 | } 279 | } 280 | return nil 281 | } 282 | 283 | func _filePath(dir, name string) string { 284 | cannonicalName := strings.Replace(name, "\\", "/", -1) 285 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) 286 | } 287 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/mac2vendor/boil_main_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "database/sql" 8 | "flag" 9 | "fmt" 10 | "math/rand" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "testing" 15 | "time" 16 | 17 | "github.com/spf13/viper" 18 | "github.com/volatiletech/sqlboiler/v4/boil" 19 | ) 20 | 21 | var flagDebugMode = flag.Bool("test.sqldebug", false, "Turns on debug mode for SQL statements") 22 | var flagConfigFile = flag.String("test.config", "", "Overrides the default config") 23 | 24 | const outputDirDepth = 5 25 | 26 | var ( 27 | dbMain tester 28 | ) 29 | 30 | type tester interface { 31 | setup() error 32 | conn() (*sql.DB, error) 33 | teardown() error 34 | } 35 | 36 | func TestMain(m *testing.M) { 37 | if dbMain == nil { 38 | fmt.Println("no dbMain tester interface was ready") 39 | os.Exit(-1) 40 | } 41 | 42 | rand.Seed(time.Now().UnixNano()) 43 | 44 | flag.Parse() 45 | 46 | var err error 47 | 48 | // Load configuration 49 | err = initViper() 50 | if err != nil { 51 | fmt.Println("unable to load config file") 52 | os.Exit(-2) 53 | } 54 | 55 | // Set DebugMode so we can see generated sql statements 56 | boil.DebugMode = *flagDebugMode 57 | 58 | if err = dbMain.setup(); err != nil { 59 | fmt.Println("Unable to execute setup:", err) 60 | os.Exit(-4) 61 | } 62 | 63 | conn, err := dbMain.conn() 64 | if err != nil { 65 | fmt.Println("failed to get connection:", err) 66 | } 67 | 68 | var code int 69 | boil.SetDB(conn) 70 | code = m.Run() 71 | 72 | if err = dbMain.teardown(); err != nil { 73 | fmt.Println("Unable to execute teardown:", err) 74 | os.Exit(-5) 75 | } 76 | 77 | os.Exit(code) 78 | } 79 | 80 | func initViper() error { 81 | if flagConfigFile != nil && *flagConfigFile != "" { 82 | viper.SetConfigFile(*flagConfigFile) 83 | if err := viper.ReadInConfig(); err != nil { 84 | return err 85 | } 86 | return nil 87 | } 88 | 89 | var err error 90 | 91 | viper.SetConfigName("sqlboiler") 92 | 93 | configHome := os.Getenv("XDG_CONFIG_HOME") 94 | homePath := os.Getenv("HOME") 95 | wd, err := os.Getwd() 96 | if err != nil { 97 | wd = strings.Repeat("../", outputDirDepth) 98 | } else { 99 | wd = wd + strings.Repeat("/..", outputDirDepth) 100 | } 101 | 102 | configPaths := []string{wd} 103 | if len(configHome) > 0 { 104 | configPaths = append(configPaths, filepath.Join(configHome, "sqlboiler")) 105 | } else { 106 | configPaths = append(configPaths, filepath.Join(homePath, ".config/sqlboiler")) 107 | } 108 | 109 | for _, p := range configPaths { 110 | viper.AddConfigPath(p) 111 | } 112 | 113 | // Ignore errors here, fall back to defaults and validation to provide errs 114 | _ = viper.ReadInConfig() 115 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 116 | viper.AutomaticEnv() 117 | 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/mac2vendor/boil_queries.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "github.com/volatiletech/sqlboiler/v4/drivers" 8 | "github.com/volatiletech/sqlboiler/v4/queries" 9 | "github.com/volatiletech/sqlboiler/v4/queries/qm" 10 | ) 11 | 12 | var dialect = drivers.Dialect{ 13 | LQ: 0x22, 14 | RQ: 0x22, 15 | 16 | UseIndexPlaceholders: false, 17 | UseLastInsertID: true, 18 | UseSchema: false, 19 | UseDefaultKeyword: true, 20 | UseAutoColumns: false, 21 | UseTopClause: false, 22 | UseOutputClause: false, 23 | UseCaseWhenExistsClause: false, 24 | } 25 | 26 | // NewQuery initializes a new Query using the passed in QueryMods 27 | func NewQuery(mods ...qm.QueryMod) *queries.Query { 28 | q := &queries.Query{} 29 | queries.SetDialect(q, &dialect) 30 | qm.Apply(q, mods...) 31 | 32 | return q 33 | } 34 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/mac2vendor/boil_queries_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "math/rand" 12 | "regexp" 13 | 14 | "github.com/volatiletech/sqlboiler/v4/boil" 15 | ) 16 | 17 | var dbNameRand *rand.Rand 18 | 19 | func MustTx(transactor boil.ContextTransactor, err error) boil.ContextTransactor { 20 | if err != nil { 21 | panic(fmt.Sprintf("Cannot create a transactor: %s", err)) 22 | } 23 | return transactor 24 | } 25 | 26 | func newFKeyDestroyer(regex *regexp.Regexp, reader io.Reader) io.Reader { 27 | return &fKeyDestroyer{ 28 | reader: reader, 29 | rgx: regex, 30 | } 31 | } 32 | 33 | type fKeyDestroyer struct { 34 | reader io.Reader 35 | buf *bytes.Buffer 36 | rgx *regexp.Regexp 37 | } 38 | 39 | func (f *fKeyDestroyer) Read(b []byte) (int, error) { 40 | if f.buf == nil { 41 | all, err := ioutil.ReadAll(f.reader) 42 | if err != nil { 43 | return 0, err 44 | } 45 | 46 | all = bytes.Replace(all, []byte{'\r', '\n'}, []byte{'\n'}, -1) 47 | all = f.rgx.ReplaceAll(all, []byte{}) 48 | f.buf = bytes.NewBuffer(all) 49 | } 50 | 51 | return f.buf.Read(b) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/mac2vendor/boil_suites_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import "testing" 7 | 8 | // This test suite runs each operation test in parallel. 9 | // Example, if your database has 3 tables, the suite will run: 10 | // table1, table2 and table3 Delete in parallel 11 | // table1, table2 and table3 Insert in parallel, and so forth. 12 | // It does NOT run each operation group in parallel. 13 | // Separating the tests thusly grants avoidance of Postgres deadlocks. 14 | func TestParent(t *testing.T) { 15 | t.Run("Vendordbs", testVendordbs) 16 | } 17 | 18 | func TestDelete(t *testing.T) { 19 | t.Run("Vendordbs", testVendordbsDelete) 20 | } 21 | 22 | func TestQueryDeleteAll(t *testing.T) { 23 | t.Run("Vendordbs", testVendordbsQueryDeleteAll) 24 | } 25 | 26 | func TestSliceDeleteAll(t *testing.T) { 27 | t.Run("Vendordbs", testVendordbsSliceDeleteAll) 28 | } 29 | 30 | func TestExists(t *testing.T) { 31 | t.Run("Vendordbs", testVendordbsExists) 32 | } 33 | 34 | func TestFind(t *testing.T) { 35 | t.Run("Vendordbs", testVendordbsFind) 36 | } 37 | 38 | func TestBind(t *testing.T) { 39 | t.Run("Vendordbs", testVendordbsBind) 40 | } 41 | 42 | func TestOne(t *testing.T) { 43 | t.Run("Vendordbs", testVendordbsOne) 44 | } 45 | 46 | func TestAll(t *testing.T) { 47 | t.Run("Vendordbs", testVendordbsAll) 48 | } 49 | 50 | func TestCount(t *testing.T) { 51 | t.Run("Vendordbs", testVendordbsCount) 52 | } 53 | 54 | func TestHooks(t *testing.T) { 55 | t.Run("Vendordbs", testVendordbsHooks) 56 | } 57 | 58 | func TestInsert(t *testing.T) { 59 | t.Run("Vendordbs", testVendordbsInsert) 60 | t.Run("Vendordbs", testVendordbsInsertWhitelist) 61 | } 62 | 63 | // TestToOne tests cannot be run in parallel 64 | // or deadlocks can occur. 65 | func TestToOne(t *testing.T) {} 66 | 67 | // TestOneToOne tests cannot be run in parallel 68 | // or deadlocks can occur. 69 | func TestOneToOne(t *testing.T) {} 70 | 71 | // TestToMany tests cannot be run in parallel 72 | // or deadlocks can occur. 73 | func TestToMany(t *testing.T) {} 74 | 75 | // TestToOneSet tests cannot be run in parallel 76 | // or deadlocks can occur. 77 | func TestToOneSet(t *testing.T) {} 78 | 79 | // TestToOneRemove tests cannot be run in parallel 80 | // or deadlocks can occur. 81 | func TestToOneRemove(t *testing.T) {} 82 | 83 | // TestOneToOneSet tests cannot be run in parallel 84 | // or deadlocks can occur. 85 | func TestOneToOneSet(t *testing.T) {} 86 | 87 | // TestOneToOneRemove tests cannot be run in parallel 88 | // or deadlocks can occur. 89 | func TestOneToOneRemove(t *testing.T) {} 90 | 91 | // TestToManyAdd tests cannot be run in parallel 92 | // or deadlocks can occur. 93 | func TestToManyAdd(t *testing.T) {} 94 | 95 | // TestToManySet tests cannot be run in parallel 96 | // or deadlocks can occur. 97 | func TestToManySet(t *testing.T) {} 98 | 99 | // TestToManyRemove tests cannot be run in parallel 100 | // or deadlocks can occur. 101 | func TestToManyRemove(t *testing.T) {} 102 | 103 | func TestReload(t *testing.T) { 104 | t.Run("Vendordbs", testVendordbsReload) 105 | } 106 | 107 | func TestReloadAll(t *testing.T) { 108 | t.Run("Vendordbs", testVendordbsReloadAll) 109 | } 110 | 111 | func TestSelect(t *testing.T) { 112 | t.Run("Vendordbs", testVendordbsSelect) 113 | } 114 | 115 | func TestUpdate(t *testing.T) { 116 | t.Run("Vendordbs", testVendordbsUpdate) 117 | } 118 | 119 | func TestSliceUpdateAll(t *testing.T) { 120 | t.Run("Vendordbs", testVendordbsSliceUpdateAll) 121 | } 122 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/mac2vendor/boil_table_names.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | var TableNames = struct { 7 | Vendordb string 8 | }{ 9 | Vendordb: "vendordb", 10 | } 11 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/mac2vendor/boil_types.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "strconv" 8 | 9 | "github.com/friendsofgo/errors" 10 | "github.com/volatiletech/sqlboiler/v4/boil" 11 | "github.com/volatiletech/strmangle" 12 | ) 13 | 14 | // M type is for providing columns and column values to UpdateAll. 15 | type M map[string]interface{} 16 | 17 | // ErrSyncFail occurs during insert when the record could not be retrieved in 18 | // order to populate default value information. This usually happens when LastInsertId 19 | // fails or there was a primary key configuration that was not resolvable. 20 | var ErrSyncFail = errors.New("models: failed to synchronize data after insert") 21 | 22 | type insertCache struct { 23 | query string 24 | retQuery string 25 | valueMapping []uint64 26 | retMapping []uint64 27 | } 28 | 29 | type updateCache struct { 30 | query string 31 | valueMapping []uint64 32 | } 33 | 34 | func makeCacheKey(cols boil.Columns, nzDefaults []string) string { 35 | buf := strmangle.GetBuffer() 36 | 37 | buf.WriteString(strconv.Itoa(cols.Kind)) 38 | for _, w := range cols.Cols { 39 | buf.WriteString(w) 40 | } 41 | 42 | if len(nzDefaults) != 0 { 43 | buf.WriteByte('.') 44 | } 45 | for _, nz := range nzDefaults { 46 | buf.WriteString(nz) 47 | } 48 | 49 | str := buf.String() 50 | strmangle.PutBuffer(buf) 51 | return str 52 | } 53 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/mac2vendor/sqlite3_main_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "database/sql" 8 | "fmt" 9 | "io" 10 | "math/rand" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "regexp" 15 | 16 | _ "github.com/mattn/go-sqlite3" 17 | "github.com/pkg/errors" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | var rgxSQLitekey = regexp.MustCompile(`(?mi)((,\n)?\s+foreign key.*?\n)+`) 22 | 23 | type sqliteTester struct { 24 | dbConn *sql.DB 25 | 26 | dbName string 27 | testDBName string 28 | } 29 | 30 | func init() { 31 | dbMain = &sqliteTester{} 32 | } 33 | 34 | func (s *sqliteTester) setup() error { 35 | var err error 36 | 37 | s.dbName = viper.GetString("sqlite3.dbname") 38 | if len(s.dbName) == 0 { 39 | return errors.New("no dbname specified") 40 | } 41 | 42 | s.testDBName = filepath.Join(os.TempDir(), fmt.Sprintf("boil-sqlite3-%d.sql", rand.Int())) 43 | 44 | dumpCmd := exec.Command("sqlite3", "-cmd", ".dump", s.dbName) 45 | createCmd := exec.Command("sqlite3", s.testDBName) 46 | 47 | r, w := io.Pipe() 48 | dumpCmd.Stdout = w 49 | createCmd.Stdin = newFKeyDestroyer(rgxSQLitekey, r) 50 | 51 | if err = dumpCmd.Start(); err != nil { 52 | return errors.Wrap(err, "failed to start sqlite3 dump command") 53 | } 54 | if err = createCmd.Start(); err != nil { 55 | return errors.Wrap(err, "failed to start sqlite3 create command") 56 | } 57 | 58 | if err = dumpCmd.Wait(); err != nil { 59 | fmt.Println(err) 60 | return errors.Wrap(err, "failed to wait for sqlite3 dump command") 61 | } 62 | 63 | w.Close() // After dumpCmd is done, close the write end of the pipe 64 | 65 | if err = createCmd.Wait(); err != nil { 66 | fmt.Println(err) 67 | return errors.Wrap(err, "failed to wait for sqlite3 create command") 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (s *sqliteTester) teardown() error { 74 | if s.dbConn != nil { 75 | s.dbConn.Close() 76 | } 77 | 78 | return os.Remove(s.testDBName) 79 | } 80 | 81 | func (s *sqliteTester) conn() (*sql.DB, error) { 82 | if s.dbConn != nil { 83 | return s.dbConn, nil 84 | } 85 | 86 | var err error 87 | s.dbConn, err = sql.Open("sqlite3", fmt.Sprintf("file:%s?_loc=UTC", s.testDBName)) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | return s.dbConn, nil 93 | } 94 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_and_port_scan/boil_main_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "database/sql" 8 | "flag" 9 | "fmt" 10 | "math/rand" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "testing" 15 | "time" 16 | 17 | "github.com/spf13/viper" 18 | "github.com/volatiletech/sqlboiler/v4/boil" 19 | ) 20 | 21 | var flagDebugMode = flag.Bool("test.sqldebug", false, "Turns on debug mode for SQL statements") 22 | var flagConfigFile = flag.String("test.config", "", "Overrides the default config") 23 | 24 | const outputDirDepth = 5 25 | 26 | var ( 27 | dbMain tester 28 | ) 29 | 30 | type tester interface { 31 | setup() error 32 | conn() (*sql.DB, error) 33 | teardown() error 34 | } 35 | 36 | func TestMain(m *testing.M) { 37 | if dbMain == nil { 38 | fmt.Println("no dbMain tester interface was ready") 39 | os.Exit(-1) 40 | } 41 | 42 | rand.Seed(time.Now().UnixNano()) 43 | 44 | flag.Parse() 45 | 46 | var err error 47 | 48 | // Load configuration 49 | err = initViper() 50 | if err != nil { 51 | fmt.Println("unable to load config file") 52 | os.Exit(-2) 53 | } 54 | 55 | // Set DebugMode so we can see generated sql statements 56 | boil.DebugMode = *flagDebugMode 57 | 58 | if err = dbMain.setup(); err != nil { 59 | fmt.Println("Unable to execute setup:", err) 60 | os.Exit(-4) 61 | } 62 | 63 | conn, err := dbMain.conn() 64 | if err != nil { 65 | fmt.Println("failed to get connection:", err) 66 | } 67 | 68 | var code int 69 | boil.SetDB(conn) 70 | code = m.Run() 71 | 72 | if err = dbMain.teardown(); err != nil { 73 | fmt.Println("Unable to execute teardown:", err) 74 | os.Exit(-5) 75 | } 76 | 77 | os.Exit(code) 78 | } 79 | 80 | func initViper() error { 81 | if flagConfigFile != nil && *flagConfigFile != "" { 82 | viper.SetConfigFile(*flagConfigFile) 83 | if err := viper.ReadInConfig(); err != nil { 84 | return err 85 | } 86 | return nil 87 | } 88 | 89 | var err error 90 | 91 | viper.SetConfigName("sqlboiler") 92 | 93 | configHome := os.Getenv("XDG_CONFIG_HOME") 94 | homePath := os.Getenv("HOME") 95 | wd, err := os.Getwd() 96 | if err != nil { 97 | wd = strings.Repeat("../", outputDirDepth) 98 | } else { 99 | wd = wd + strings.Repeat("/..", outputDirDepth) 100 | } 101 | 102 | configPaths := []string{wd} 103 | if len(configHome) > 0 { 104 | configPaths = append(configPaths, filepath.Join(configHome, "sqlboiler")) 105 | } else { 106 | configPaths = append(configPaths, filepath.Join(homePath, ".config/sqlboiler")) 107 | } 108 | 109 | for _, p := range configPaths { 110 | viper.AddConfigPath(p) 111 | } 112 | 113 | // Ignore errors here, fall back to defaults and validation to provide errs 114 | _ = viper.ReadInConfig() 115 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 116 | viper.AutomaticEnv() 117 | 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_and_port_scan/boil_queries.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "github.com/volatiletech/sqlboiler/v4/drivers" 8 | "github.com/volatiletech/sqlboiler/v4/queries" 9 | "github.com/volatiletech/sqlboiler/v4/queries/qm" 10 | ) 11 | 12 | var dialect = drivers.Dialect{ 13 | LQ: 0x22, 14 | RQ: 0x22, 15 | 16 | UseIndexPlaceholders: false, 17 | UseLastInsertID: true, 18 | UseSchema: false, 19 | UseDefaultKeyword: true, 20 | UseAutoColumns: false, 21 | UseTopClause: false, 22 | UseOutputClause: false, 23 | UseCaseWhenExistsClause: false, 24 | } 25 | 26 | // NewQuery initializes a new Query using the passed in QueryMods 27 | func NewQuery(mods ...qm.QueryMod) *queries.Query { 28 | q := &queries.Query{} 29 | queries.SetDialect(q, &dialect) 30 | qm.Apply(q, mods...) 31 | 32 | return q 33 | } 34 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_and_port_scan/boil_queries_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "math/rand" 12 | "regexp" 13 | 14 | "github.com/volatiletech/sqlboiler/v4/boil" 15 | ) 16 | 17 | var dbNameRand *rand.Rand 18 | 19 | func MustTx(transactor boil.ContextTransactor, err error) boil.ContextTransactor { 20 | if err != nil { 21 | panic(fmt.Sprintf("Cannot create a transactor: %s", err)) 22 | } 23 | return transactor 24 | } 25 | 26 | func newFKeyDestroyer(regex *regexp.Regexp, reader io.Reader) io.Reader { 27 | return &fKeyDestroyer{ 28 | reader: reader, 29 | rgx: regex, 30 | } 31 | } 32 | 33 | type fKeyDestroyer struct { 34 | reader io.Reader 35 | buf *bytes.Buffer 36 | rgx *regexp.Regexp 37 | } 38 | 39 | func (f *fKeyDestroyer) Read(b []byte) (int, error) { 40 | if f.buf == nil { 41 | all, err := ioutil.ReadAll(f.reader) 42 | if err != nil { 43 | return 0, err 44 | } 45 | 46 | all = bytes.Replace(all, []byte{'\r', '\n'}, []byte{'\n'}, -1) 47 | all = f.rgx.ReplaceAll(all, []byte{}) 48 | f.buf = bytes.NewBuffer(all) 49 | } 50 | 51 | return f.buf.Read(b) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_and_port_scan/boil_suites_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import "testing" 7 | 8 | // This test suite runs each operation test in parallel. 9 | // Example, if your database has 3 tables, the suite will run: 10 | // table1, table2 and table3 Delete in parallel 11 | // table1, table2 and table3 Insert in parallel, and so forth. 12 | // It does NOT run each operation group in parallel. 13 | // Separating the tests thusly grants avoidance of Postgres deadlocks. 14 | func TestParent(t *testing.T) { 15 | t.Run("GorpMigrations", testGorpMigrations) 16 | t.Run("NodeScans", testNodeScans) 17 | t.Run("Nodes", testNodes) 18 | t.Run("PortScans", testPortScans) 19 | t.Run("Ports", testPorts) 20 | } 21 | 22 | func TestDelete(t *testing.T) { 23 | t.Run("GorpMigrations", testGorpMigrationsDelete) 24 | t.Run("NodeScans", testNodeScansDelete) 25 | t.Run("Nodes", testNodesDelete) 26 | t.Run("PortScans", testPortScansDelete) 27 | t.Run("Ports", testPortsDelete) 28 | } 29 | 30 | func TestQueryDeleteAll(t *testing.T) { 31 | t.Run("GorpMigrations", testGorpMigrationsQueryDeleteAll) 32 | t.Run("NodeScans", testNodeScansQueryDeleteAll) 33 | t.Run("Nodes", testNodesQueryDeleteAll) 34 | t.Run("PortScans", testPortScansQueryDeleteAll) 35 | t.Run("Ports", testPortsQueryDeleteAll) 36 | } 37 | 38 | func TestSliceDeleteAll(t *testing.T) { 39 | t.Run("GorpMigrations", testGorpMigrationsSliceDeleteAll) 40 | t.Run("NodeScans", testNodeScansSliceDeleteAll) 41 | t.Run("Nodes", testNodesSliceDeleteAll) 42 | t.Run("PortScans", testPortScansSliceDeleteAll) 43 | t.Run("Ports", testPortsSliceDeleteAll) 44 | } 45 | 46 | func TestExists(t *testing.T) { 47 | t.Run("GorpMigrations", testGorpMigrationsExists) 48 | t.Run("NodeScans", testNodeScansExists) 49 | t.Run("Nodes", testNodesExists) 50 | t.Run("PortScans", testPortScansExists) 51 | t.Run("Ports", testPortsExists) 52 | } 53 | 54 | func TestFind(t *testing.T) { 55 | t.Run("GorpMigrations", testGorpMigrationsFind) 56 | t.Run("NodeScans", testNodeScansFind) 57 | t.Run("Nodes", testNodesFind) 58 | t.Run("PortScans", testPortScansFind) 59 | t.Run("Ports", testPortsFind) 60 | } 61 | 62 | func TestBind(t *testing.T) { 63 | t.Run("GorpMigrations", testGorpMigrationsBind) 64 | t.Run("NodeScans", testNodeScansBind) 65 | t.Run("Nodes", testNodesBind) 66 | t.Run("PortScans", testPortScansBind) 67 | t.Run("Ports", testPortsBind) 68 | } 69 | 70 | func TestOne(t *testing.T) { 71 | t.Run("GorpMigrations", testGorpMigrationsOne) 72 | t.Run("NodeScans", testNodeScansOne) 73 | t.Run("Nodes", testNodesOne) 74 | t.Run("PortScans", testPortScansOne) 75 | t.Run("Ports", testPortsOne) 76 | } 77 | 78 | func TestAll(t *testing.T) { 79 | t.Run("GorpMigrations", testGorpMigrationsAll) 80 | t.Run("NodeScans", testNodeScansAll) 81 | t.Run("Nodes", testNodesAll) 82 | t.Run("PortScans", testPortScansAll) 83 | t.Run("Ports", testPortsAll) 84 | } 85 | 86 | func TestCount(t *testing.T) { 87 | t.Run("GorpMigrations", testGorpMigrationsCount) 88 | t.Run("NodeScans", testNodeScansCount) 89 | t.Run("Nodes", testNodesCount) 90 | t.Run("PortScans", testPortScansCount) 91 | t.Run("Ports", testPortsCount) 92 | } 93 | 94 | func TestHooks(t *testing.T) { 95 | t.Run("GorpMigrations", testGorpMigrationsHooks) 96 | t.Run("NodeScans", testNodeScansHooks) 97 | t.Run("Nodes", testNodesHooks) 98 | t.Run("PortScans", testPortScansHooks) 99 | t.Run("Ports", testPortsHooks) 100 | } 101 | 102 | func TestInsert(t *testing.T) { 103 | t.Run("GorpMigrations", testGorpMigrationsInsert) 104 | t.Run("GorpMigrations", testGorpMigrationsInsertWhitelist) 105 | t.Run("NodeScans", testNodeScansInsert) 106 | t.Run("NodeScans", testNodeScansInsertWhitelist) 107 | t.Run("Nodes", testNodesInsert) 108 | t.Run("Nodes", testNodesInsertWhitelist) 109 | t.Run("PortScans", testPortScansInsert) 110 | t.Run("PortScans", testPortScansInsertWhitelist) 111 | t.Run("Ports", testPortsInsert) 112 | t.Run("Ports", testPortsInsertWhitelist) 113 | } 114 | 115 | // TestToOne tests cannot be run in parallel 116 | // or deadlocks can occur. 117 | func TestToOne(t *testing.T) { 118 | t.Run("NodeToNodeScanUsingNodeScan", testNodeToOneNodeScanUsingNodeScan) 119 | t.Run("PortScanToNodeUsingNode", testPortScanToOneNodeUsingNode) 120 | t.Run("PortToPortScanUsingPortScan", testPortToOnePortScanUsingPortScan) 121 | } 122 | 123 | // TestOneToOne tests cannot be run in parallel 124 | // or deadlocks can occur. 125 | func TestOneToOne(t *testing.T) {} 126 | 127 | // TestToMany tests cannot be run in parallel 128 | // or deadlocks can occur. 129 | func TestToMany(t *testing.T) { 130 | t.Run("NodeScanToNodes", testNodeScanToManyNodes) 131 | t.Run("NodeToPortScans", testNodeToManyPortScans) 132 | t.Run("PortScanToPorts", testPortScanToManyPorts) 133 | } 134 | 135 | // TestToOneSet tests cannot be run in parallel 136 | // or deadlocks can occur. 137 | func TestToOneSet(t *testing.T) { 138 | t.Run("NodeToNodeScanUsingNodes", testNodeToOneSetOpNodeScanUsingNodeScan) 139 | t.Run("PortScanToNodeUsingPortScans", testPortScanToOneSetOpNodeUsingNode) 140 | t.Run("PortToPortScanUsingPorts", testPortToOneSetOpPortScanUsingPortScan) 141 | } 142 | 143 | // TestToOneRemove tests cannot be run in parallel 144 | // or deadlocks can occur. 145 | func TestToOneRemove(t *testing.T) {} 146 | 147 | // TestOneToOneSet tests cannot be run in parallel 148 | // or deadlocks can occur. 149 | func TestOneToOneSet(t *testing.T) {} 150 | 151 | // TestOneToOneRemove tests cannot be run in parallel 152 | // or deadlocks can occur. 153 | func TestOneToOneRemove(t *testing.T) {} 154 | 155 | // TestToManyAdd tests cannot be run in parallel 156 | // or deadlocks can occur. 157 | func TestToManyAdd(t *testing.T) { 158 | t.Run("NodeScanToNodes", testNodeScanToManyAddOpNodes) 159 | t.Run("NodeToPortScans", testNodeToManyAddOpPortScans) 160 | t.Run("PortScanToPorts", testPortScanToManyAddOpPorts) 161 | } 162 | 163 | // TestToManySet tests cannot be run in parallel 164 | // or deadlocks can occur. 165 | func TestToManySet(t *testing.T) {} 166 | 167 | // TestToManyRemove tests cannot be run in parallel 168 | // or deadlocks can occur. 169 | func TestToManyRemove(t *testing.T) {} 170 | 171 | func TestReload(t *testing.T) { 172 | t.Run("GorpMigrations", testGorpMigrationsReload) 173 | t.Run("NodeScans", testNodeScansReload) 174 | t.Run("Nodes", testNodesReload) 175 | t.Run("PortScans", testPortScansReload) 176 | t.Run("Ports", testPortsReload) 177 | } 178 | 179 | func TestReloadAll(t *testing.T) { 180 | t.Run("GorpMigrations", testGorpMigrationsReloadAll) 181 | t.Run("NodeScans", testNodeScansReloadAll) 182 | t.Run("Nodes", testNodesReloadAll) 183 | t.Run("PortScans", testPortScansReloadAll) 184 | t.Run("Ports", testPortsReloadAll) 185 | } 186 | 187 | func TestSelect(t *testing.T) { 188 | t.Run("GorpMigrations", testGorpMigrationsSelect) 189 | t.Run("NodeScans", testNodeScansSelect) 190 | t.Run("Nodes", testNodesSelect) 191 | t.Run("PortScans", testPortScansSelect) 192 | t.Run("Ports", testPortsSelect) 193 | } 194 | 195 | func TestUpdate(t *testing.T) { 196 | t.Run("GorpMigrations", testGorpMigrationsUpdate) 197 | t.Run("NodeScans", testNodeScansUpdate) 198 | t.Run("Nodes", testNodesUpdate) 199 | t.Run("PortScans", testPortScansUpdate) 200 | t.Run("Ports", testPortsUpdate) 201 | } 202 | 203 | func TestSliceUpdateAll(t *testing.T) { 204 | t.Run("GorpMigrations", testGorpMigrationsSliceUpdateAll) 205 | t.Run("NodeScans", testNodeScansSliceUpdateAll) 206 | t.Run("Nodes", testNodesSliceUpdateAll) 207 | t.Run("PortScans", testPortScansSliceUpdateAll) 208 | t.Run("Ports", testPortsSliceUpdateAll) 209 | } 210 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_and_port_scan/boil_table_names.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | var TableNames = struct { 7 | GorpMigrations string 8 | NodeScans string 9 | Nodes string 10 | PortScans string 11 | Ports string 12 | }{ 13 | GorpMigrations: "gorp_migrations", 14 | NodeScans: "node_scans", 15 | Nodes: "nodes", 16 | PortScans: "port_scans", 17 | Ports: "ports", 18 | } 19 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_and_port_scan/boil_types.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "strconv" 8 | 9 | "github.com/friendsofgo/errors" 10 | "github.com/volatiletech/sqlboiler/v4/boil" 11 | "github.com/volatiletech/strmangle" 12 | ) 13 | 14 | // M type is for providing columns and column values to UpdateAll. 15 | type M map[string]interface{} 16 | 17 | // ErrSyncFail occurs during insert when the record could not be retrieved in 18 | // order to populate default value information. This usually happens when LastInsertId 19 | // fails or there was a primary key configuration that was not resolvable. 20 | var ErrSyncFail = errors.New("models: failed to synchronize data after insert") 21 | 22 | type insertCache struct { 23 | query string 24 | retQuery string 25 | valueMapping []uint64 26 | retMapping []uint64 27 | } 28 | 29 | type updateCache struct { 30 | query string 31 | valueMapping []uint64 32 | } 33 | 34 | func makeCacheKey(cols boil.Columns, nzDefaults []string) string { 35 | buf := strmangle.GetBuffer() 36 | 37 | buf.WriteString(strconv.Itoa(cols.Kind)) 38 | for _, w := range cols.Cols { 39 | buf.WriteString(w) 40 | } 41 | 42 | if len(nzDefaults) != 0 { 43 | buf.WriteByte('.') 44 | } 45 | for _, nz := range nzDefaults { 46 | buf.WriteString(nz) 47 | } 48 | 49 | str := buf.String() 50 | strmangle.PutBuffer(buf) 51 | return str 52 | } 53 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_and_port_scan/sqlite3_main_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "database/sql" 8 | "fmt" 9 | "io" 10 | "math/rand" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "regexp" 15 | 16 | _ "github.com/mattn/go-sqlite3" 17 | "github.com/pkg/errors" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | var rgxSQLitekey = regexp.MustCompile(`(?mi)((,\n)?\s+foreign key.*?\n)+`) 22 | 23 | type sqliteTester struct { 24 | dbConn *sql.DB 25 | 26 | dbName string 27 | testDBName string 28 | } 29 | 30 | func init() { 31 | dbMain = &sqliteTester{} 32 | } 33 | 34 | func (s *sqliteTester) setup() error { 35 | var err error 36 | 37 | s.dbName = viper.GetString("sqlite3.dbname") 38 | if len(s.dbName) == 0 { 39 | return errors.New("no dbname specified") 40 | } 41 | 42 | s.testDBName = filepath.Join(os.TempDir(), fmt.Sprintf("boil-sqlite3-%d.sql", rand.Int())) 43 | 44 | dumpCmd := exec.Command("sqlite3", "-cmd", ".dump", s.dbName) 45 | createCmd := exec.Command("sqlite3", s.testDBName) 46 | 47 | r, w := io.Pipe() 48 | dumpCmd.Stdout = w 49 | createCmd.Stdin = newFKeyDestroyer(rgxSQLitekey, r) 50 | 51 | if err = dumpCmd.Start(); err != nil { 52 | return errors.Wrap(err, "failed to start sqlite3 dump command") 53 | } 54 | if err = createCmd.Start(); err != nil { 55 | return errors.Wrap(err, "failed to start sqlite3 create command") 56 | } 57 | 58 | if err = dumpCmd.Wait(); err != nil { 59 | fmt.Println(err) 60 | return errors.Wrap(err, "failed to wait for sqlite3 dump command") 61 | } 62 | 63 | w.Close() // After dumpCmd is done, close the write end of the pipe 64 | 65 | if err = createCmd.Wait(); err != nil { 66 | fmt.Println(err) 67 | return errors.Wrap(err, "failed to wait for sqlite3 create command") 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (s *sqliteTester) teardown() error { 74 | if s.dbConn != nil { 75 | s.dbConn.Close() 76 | } 77 | 78 | return os.Remove(s.testDBName) 79 | } 80 | 81 | func (s *sqliteTester) conn() (*sql.DB, error) { 82 | if s.dbConn != nil { 83 | return s.dbConn, nil 84 | } 85 | 86 | var err error 87 | s.dbConn, err = sql.Open("sqlite3", fmt.Sprintf("file:%s?_loc=UTC", s.testDBName)) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | return s.dbConn, nil 93 | } 94 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_wake/boil_main_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "database/sql" 8 | "flag" 9 | "fmt" 10 | "math/rand" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "testing" 15 | "time" 16 | 17 | "github.com/spf13/viper" 18 | "github.com/volatiletech/sqlboiler/v4/boil" 19 | ) 20 | 21 | var flagDebugMode = flag.Bool("test.sqldebug", false, "Turns on debug mode for SQL statements") 22 | var flagConfigFile = flag.String("test.config", "", "Overrides the default config") 23 | 24 | const outputDirDepth = 5 25 | 26 | var ( 27 | dbMain tester 28 | ) 29 | 30 | type tester interface { 31 | setup() error 32 | conn() (*sql.DB, error) 33 | teardown() error 34 | } 35 | 36 | func TestMain(m *testing.M) { 37 | if dbMain == nil { 38 | fmt.Println("no dbMain tester interface was ready") 39 | os.Exit(-1) 40 | } 41 | 42 | rand.Seed(time.Now().UnixNano()) 43 | 44 | flag.Parse() 45 | 46 | var err error 47 | 48 | // Load configuration 49 | err = initViper() 50 | if err != nil { 51 | fmt.Println("unable to load config file") 52 | os.Exit(-2) 53 | } 54 | 55 | // Set DebugMode so we can see generated sql statements 56 | boil.DebugMode = *flagDebugMode 57 | 58 | if err = dbMain.setup(); err != nil { 59 | fmt.Println("Unable to execute setup:", err) 60 | os.Exit(-4) 61 | } 62 | 63 | conn, err := dbMain.conn() 64 | if err != nil { 65 | fmt.Println("failed to get connection:", err) 66 | } 67 | 68 | var code int 69 | boil.SetDB(conn) 70 | code = m.Run() 71 | 72 | if err = dbMain.teardown(); err != nil { 73 | fmt.Println("Unable to execute teardown:", err) 74 | os.Exit(-5) 75 | } 76 | 77 | os.Exit(code) 78 | } 79 | 80 | func initViper() error { 81 | if flagConfigFile != nil && *flagConfigFile != "" { 82 | viper.SetConfigFile(*flagConfigFile) 83 | if err := viper.ReadInConfig(); err != nil { 84 | return err 85 | } 86 | return nil 87 | } 88 | 89 | var err error 90 | 91 | viper.SetConfigName("sqlboiler") 92 | 93 | configHome := os.Getenv("XDG_CONFIG_HOME") 94 | homePath := os.Getenv("HOME") 95 | wd, err := os.Getwd() 96 | if err != nil { 97 | wd = strings.Repeat("../", outputDirDepth) 98 | } else { 99 | wd = wd + strings.Repeat("/..", outputDirDepth) 100 | } 101 | 102 | configPaths := []string{wd} 103 | if len(configHome) > 0 { 104 | configPaths = append(configPaths, filepath.Join(configHome, "sqlboiler")) 105 | } else { 106 | configPaths = append(configPaths, filepath.Join(homePath, ".config/sqlboiler")) 107 | } 108 | 109 | for _, p := range configPaths { 110 | viper.AddConfigPath(p) 111 | } 112 | 113 | // Ignore errors here, fall back to defaults and validation to provide errs 114 | _ = viper.ReadInConfig() 115 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 116 | viper.AutomaticEnv() 117 | 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_wake/boil_queries.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "github.com/volatiletech/sqlboiler/v4/drivers" 8 | "github.com/volatiletech/sqlboiler/v4/queries" 9 | "github.com/volatiletech/sqlboiler/v4/queries/qm" 10 | ) 11 | 12 | var dialect = drivers.Dialect{ 13 | LQ: 0x22, 14 | RQ: 0x22, 15 | 16 | UseIndexPlaceholders: false, 17 | UseLastInsertID: true, 18 | UseSchema: false, 19 | UseDefaultKeyword: true, 20 | UseAutoColumns: false, 21 | UseTopClause: false, 22 | UseOutputClause: false, 23 | UseCaseWhenExistsClause: false, 24 | } 25 | 26 | // NewQuery initializes a new Query using the passed in QueryMods 27 | func NewQuery(mods ...qm.QueryMod) *queries.Query { 28 | q := &queries.Query{} 29 | queries.SetDialect(q, &dialect) 30 | qm.Apply(q, mods...) 31 | 32 | return q 33 | } 34 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_wake/boil_queries_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "math/rand" 12 | "regexp" 13 | 14 | "github.com/volatiletech/sqlboiler/v4/boil" 15 | ) 16 | 17 | var dbNameRand *rand.Rand 18 | 19 | func MustTx(transactor boil.ContextTransactor, err error) boil.ContextTransactor { 20 | if err != nil { 21 | panic(fmt.Sprintf("Cannot create a transactor: %s", err)) 22 | } 23 | return transactor 24 | } 25 | 26 | func newFKeyDestroyer(regex *regexp.Regexp, reader io.Reader) io.Reader { 27 | return &fKeyDestroyer{ 28 | reader: reader, 29 | rgx: regex, 30 | } 31 | } 32 | 33 | type fKeyDestroyer struct { 34 | reader io.Reader 35 | buf *bytes.Buffer 36 | rgx *regexp.Regexp 37 | } 38 | 39 | func (f *fKeyDestroyer) Read(b []byte) (int, error) { 40 | if f.buf == nil { 41 | all, err := ioutil.ReadAll(f.reader) 42 | if err != nil { 43 | return 0, err 44 | } 45 | 46 | all = bytes.Replace(all, []byte{'\r', '\n'}, []byte{'\n'}, -1) 47 | all = f.rgx.ReplaceAll(all, []byte{}) 48 | f.buf = bytes.NewBuffer(all) 49 | } 50 | 51 | return f.buf.Read(b) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_wake/boil_suites_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import "testing" 7 | 8 | // This test suite runs each operation test in parallel. 9 | // Example, if your database has 3 tables, the suite will run: 10 | // table1, table2 and table3 Delete in parallel 11 | // table1, table2 and table3 Insert in parallel, and so forth. 12 | // It does NOT run each operation group in parallel. 13 | // Separating the tests thusly grants avoidance of Postgres deadlocks. 14 | func TestParent(t *testing.T) { 15 | t.Run("GorpMigrations", testGorpMigrations) 16 | t.Run("NodeWakes", testNodeWakes) 17 | } 18 | 19 | func TestDelete(t *testing.T) { 20 | t.Run("GorpMigrations", testGorpMigrationsDelete) 21 | t.Run("NodeWakes", testNodeWakesDelete) 22 | } 23 | 24 | func TestQueryDeleteAll(t *testing.T) { 25 | t.Run("GorpMigrations", testGorpMigrationsQueryDeleteAll) 26 | t.Run("NodeWakes", testNodeWakesQueryDeleteAll) 27 | } 28 | 29 | func TestSliceDeleteAll(t *testing.T) { 30 | t.Run("GorpMigrations", testGorpMigrationsSliceDeleteAll) 31 | t.Run("NodeWakes", testNodeWakesSliceDeleteAll) 32 | } 33 | 34 | func TestExists(t *testing.T) { 35 | t.Run("GorpMigrations", testGorpMigrationsExists) 36 | t.Run("NodeWakes", testNodeWakesExists) 37 | } 38 | 39 | func TestFind(t *testing.T) { 40 | t.Run("GorpMigrations", testGorpMigrationsFind) 41 | t.Run("NodeWakes", testNodeWakesFind) 42 | } 43 | 44 | func TestBind(t *testing.T) { 45 | t.Run("GorpMigrations", testGorpMigrationsBind) 46 | t.Run("NodeWakes", testNodeWakesBind) 47 | } 48 | 49 | func TestOne(t *testing.T) { 50 | t.Run("GorpMigrations", testGorpMigrationsOne) 51 | t.Run("NodeWakes", testNodeWakesOne) 52 | } 53 | 54 | func TestAll(t *testing.T) { 55 | t.Run("GorpMigrations", testGorpMigrationsAll) 56 | t.Run("NodeWakes", testNodeWakesAll) 57 | } 58 | 59 | func TestCount(t *testing.T) { 60 | t.Run("GorpMigrations", testGorpMigrationsCount) 61 | t.Run("NodeWakes", testNodeWakesCount) 62 | } 63 | 64 | func TestHooks(t *testing.T) { 65 | t.Run("GorpMigrations", testGorpMigrationsHooks) 66 | t.Run("NodeWakes", testNodeWakesHooks) 67 | } 68 | 69 | func TestInsert(t *testing.T) { 70 | t.Run("GorpMigrations", testGorpMigrationsInsert) 71 | t.Run("GorpMigrations", testGorpMigrationsInsertWhitelist) 72 | t.Run("NodeWakes", testNodeWakesInsert) 73 | t.Run("NodeWakes", testNodeWakesInsertWhitelist) 74 | } 75 | 76 | // TestToOne tests cannot be run in parallel 77 | // or deadlocks can occur. 78 | func TestToOne(t *testing.T) {} 79 | 80 | // TestOneToOne tests cannot be run in parallel 81 | // or deadlocks can occur. 82 | func TestOneToOne(t *testing.T) {} 83 | 84 | // TestToMany tests cannot be run in parallel 85 | // or deadlocks can occur. 86 | func TestToMany(t *testing.T) {} 87 | 88 | // TestToOneSet tests cannot be run in parallel 89 | // or deadlocks can occur. 90 | func TestToOneSet(t *testing.T) {} 91 | 92 | // TestToOneRemove tests cannot be run in parallel 93 | // or deadlocks can occur. 94 | func TestToOneRemove(t *testing.T) {} 95 | 96 | // TestOneToOneSet tests cannot be run in parallel 97 | // or deadlocks can occur. 98 | func TestOneToOneSet(t *testing.T) {} 99 | 100 | // TestOneToOneRemove tests cannot be run in parallel 101 | // or deadlocks can occur. 102 | func TestOneToOneRemove(t *testing.T) {} 103 | 104 | // TestToManyAdd tests cannot be run in parallel 105 | // or deadlocks can occur. 106 | func TestToManyAdd(t *testing.T) {} 107 | 108 | // TestToManySet tests cannot be run in parallel 109 | // or deadlocks can occur. 110 | func TestToManySet(t *testing.T) {} 111 | 112 | // TestToManyRemove tests cannot be run in parallel 113 | // or deadlocks can occur. 114 | func TestToManyRemove(t *testing.T) {} 115 | 116 | func TestReload(t *testing.T) { 117 | t.Run("GorpMigrations", testGorpMigrationsReload) 118 | t.Run("NodeWakes", testNodeWakesReload) 119 | } 120 | 121 | func TestReloadAll(t *testing.T) { 122 | t.Run("GorpMigrations", testGorpMigrationsReloadAll) 123 | t.Run("NodeWakes", testNodeWakesReloadAll) 124 | } 125 | 126 | func TestSelect(t *testing.T) { 127 | t.Run("GorpMigrations", testGorpMigrationsSelect) 128 | t.Run("NodeWakes", testNodeWakesSelect) 129 | } 130 | 131 | func TestUpdate(t *testing.T) { 132 | t.Run("GorpMigrations", testGorpMigrationsUpdate) 133 | t.Run("NodeWakes", testNodeWakesUpdate) 134 | } 135 | 136 | func TestSliceUpdateAll(t *testing.T) { 137 | t.Run("GorpMigrations", testGorpMigrationsSliceUpdateAll) 138 | t.Run("NodeWakes", testNodeWakesSliceUpdateAll) 139 | } 140 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_wake/boil_table_names.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | var TableNames = struct { 7 | GorpMigrations string 8 | NodeWakes string 9 | }{ 10 | GorpMigrations: "gorp_migrations", 11 | NodeWakes: "node_wakes", 12 | } 13 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_wake/boil_types.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "strconv" 8 | 9 | "github.com/friendsofgo/errors" 10 | "github.com/volatiletech/sqlboiler/v4/boil" 11 | "github.com/volatiletech/strmangle" 12 | ) 13 | 14 | // M type is for providing columns and column values to UpdateAll. 15 | type M map[string]interface{} 16 | 17 | // ErrSyncFail occurs during insert when the record could not be retrieved in 18 | // order to populate default value information. This usually happens when LastInsertId 19 | // fails or there was a primary key configuration that was not resolvable. 20 | var ErrSyncFail = errors.New("models: failed to synchronize data after insert") 21 | 22 | type insertCache struct { 23 | query string 24 | retQuery string 25 | valueMapping []uint64 26 | retMapping []uint64 27 | } 28 | 29 | type updateCache struct { 30 | query string 31 | valueMapping []uint64 32 | } 33 | 34 | func makeCacheKey(cols boil.Columns, nzDefaults []string) string { 35 | buf := strmangle.GetBuffer() 36 | 37 | buf.WriteString(strconv.Itoa(cols.Kind)) 38 | for _, w := range cols.Cols { 39 | buf.WriteString(w) 40 | } 41 | 42 | if len(nzDefaults) != 0 { 43 | buf.WriteByte('.') 44 | } 45 | for _, nz := range nzDefaults { 46 | buf.WriteString(nz) 47 | } 48 | 49 | str := buf.String() 50 | strmangle.PutBuffer(buf) 51 | return str 52 | } 53 | -------------------------------------------------------------------------------- /pkg/db/sqlite/models/node_wake/sqlite3_main_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. 2 | // This file is meant to be re-generated in place and/or deleted at any time. 3 | 4 | package models 5 | 6 | import ( 7 | "database/sql" 8 | "fmt" 9 | "io" 10 | "math/rand" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "regexp" 15 | 16 | _ "github.com/mattn/go-sqlite3" 17 | "github.com/pkg/errors" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | var rgxSQLitekey = regexp.MustCompile(`(?mi)((,\n)?\s+foreign key.*?\n)+`) 22 | 23 | type sqliteTester struct { 24 | dbConn *sql.DB 25 | 26 | dbName string 27 | testDBName string 28 | } 29 | 30 | func init() { 31 | dbMain = &sqliteTester{} 32 | } 33 | 34 | func (s *sqliteTester) setup() error { 35 | var err error 36 | 37 | s.dbName = viper.GetString("sqlite3.dbname") 38 | if len(s.dbName) == 0 { 39 | return errors.New("no dbname specified") 40 | } 41 | 42 | s.testDBName = filepath.Join(os.TempDir(), fmt.Sprintf("boil-sqlite3-%d.sql", rand.Int())) 43 | 44 | dumpCmd := exec.Command("sqlite3", "-cmd", ".dump", s.dbName) 45 | createCmd := exec.Command("sqlite3", s.testDBName) 46 | 47 | r, w := io.Pipe() 48 | dumpCmd.Stdout = w 49 | createCmd.Stdin = newFKeyDestroyer(rgxSQLitekey, r) 50 | 51 | if err = dumpCmd.Start(); err != nil { 52 | return errors.Wrap(err, "failed to start sqlite3 dump command") 53 | } 54 | if err = createCmd.Start(); err != nil { 55 | return errors.Wrap(err, "failed to start sqlite3 create command") 56 | } 57 | 58 | if err = dumpCmd.Wait(); err != nil { 59 | fmt.Println(err) 60 | return errors.Wrap(err, "failed to wait for sqlite3 dump command") 61 | } 62 | 63 | w.Close() // After dumpCmd is done, close the write end of the pipe 64 | 65 | if err = createCmd.Wait(); err != nil { 66 | fmt.Println(err) 67 | return errors.Wrap(err, "failed to wait for sqlite3 create command") 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (s *sqliteTester) teardown() error { 74 | if s.dbConn != nil { 75 | s.dbConn.Close() 76 | } 77 | 78 | return os.Remove(s.testDBName) 79 | } 80 | 81 | func (s *sqliteTester) conn() (*sql.DB, error) { 82 | if s.dbConn != nil { 83 | return s.dbConn, nil 84 | } 85 | 86 | var err error 87 | s.dbConn, err = sql.Open("sqlite3", fmt.Sprintf("file:%s?_loc=UTC", s.testDBName)) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | return s.dbConn, nil 93 | } 94 | -------------------------------------------------------------------------------- /pkg/networking/interfaceinspector.go: -------------------------------------------------------------------------------- 1 | package networking 2 | 3 | import "net" 4 | 5 | type InterfaceInspector struct { 6 | device string 7 | } 8 | 9 | func NewInterfaceInspector(device string) *InterfaceInspector { 10 | return &InterfaceInspector{device} 11 | } 12 | 13 | func (i *InterfaceInspector) GetIPv4Subnets() ([]string, error) { 14 | iface, err := net.InterfaceByName(i.device) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | addrs, err := iface.Addrs() 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | subnets := []string{} 25 | for _, addr := range addrs { 26 | ip, _, err := net.ParseCIDR(addr.String()) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | if ip.To4() != nil { 32 | subnets = append(subnets, addr.String()) 33 | } 34 | } 35 | 36 | return subnets, nil 37 | } 38 | 39 | func (i *InterfaceInspector) GetDevice() string { 40 | return i.device 41 | } 42 | -------------------------------------------------------------------------------- /pkg/persisters/external_source.go: -------------------------------------------------------------------------------- 1 | package persisters 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type ExternalSource struct { 11 | SourceURL string 12 | DestinationPath string 13 | } 14 | 15 | func (s *ExternalSource) PullIfNotExists() error { 16 | // If CSV file does not exist, download & create it 17 | if _, err := os.Stat(s.DestinationPath); os.IsNotExist(err) { 18 | // Create leading directories 19 | leadingDir, _ := filepath.Split(s.DestinationPath) 20 | if err := os.MkdirAll(leadingDir, os.ModePerm); err != nil { 21 | return err 22 | } 23 | 24 | // Create file 25 | out, err := os.Create(s.DestinationPath) 26 | if err != nil { 27 | return err 28 | } 29 | defer out.Close() 30 | 31 | // Download file 32 | res, err := http.Get(s.SourceURL) 33 | if err != nil { 34 | return err 35 | } 36 | defer res.Body.Close() 37 | 38 | // Write to file 39 | if _, err := io.Copy(out, res.Body); err != nil { 40 | return err 41 | } 42 | } 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/persisters/mac2vendor.go: -------------------------------------------------------------------------------- 1 | package persisters 2 | 3 | //go:generate sqlboiler sqlite3 -o ../db/sqlite/models/mac2vendor -c ../../configs/sqlboiler/mac2vendor.toml 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "strconv" 9 | "strings" 10 | 11 | _ "github.com/mattn/go-sqlite3" 12 | mac2vendorModels "github.com/pojntfx/liwasc/pkg/db/sqlite/models/mac2vendor" 13 | ) 14 | 15 | type MAC2VendorPersister struct { 16 | *SQLite 17 | *ExternalSource 18 | } 19 | 20 | func NewMAC2VendorPersister(dbPath string, sourceURL string) *MAC2VendorPersister { 21 | return &MAC2VendorPersister{ 22 | &SQLite{ 23 | DBPath: dbPath, 24 | }, 25 | &ExternalSource{ 26 | SourceURL: sourceURL, 27 | DestinationPath: dbPath, 28 | }, 29 | } 30 | } 31 | 32 | func (d *MAC2VendorPersister) Open() error { 33 | // If database file does not exist, download & create it 34 | if err := d.ExternalSource.PullIfNotExists(); err != nil { 35 | return err 36 | } 37 | 38 | return d.SQLite.Open() 39 | } 40 | 41 | func (d *MAC2VendorPersister) GetVendor(mac string) (*mac2vendorModels.Vendordb, error) { 42 | oui, err := GetOUI(mac) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | vendor, err := mac2vendorModels.FindVendordb(context.Background(), d.db, int64(oui)) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return vendor, nil 53 | } 54 | 55 | func GetOUI(mac string) (uint64, error) { 56 | parsedMAC := strings.Split(mac, ":") 57 | if len(parsedMAC) < 4 { 58 | return 0, fmt.Errorf("invalid MAC Address: %v", mac) 59 | } 60 | 61 | res, err := strconv.ParseUint(strings.Join(parsedMAC[0:3], ""), 16, 64) 62 | if err != nil { 63 | return 0, err 64 | } 65 | 66 | return uint64(res), err 67 | } 68 | -------------------------------------------------------------------------------- /pkg/persisters/node_and_port_scan.go: -------------------------------------------------------------------------------- 1 | package persisters 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/pojntfx/liwasc/pkg/db/sqlite/migrations/node_and_port_scan" 8 | models "github.com/pojntfx/liwasc/pkg/db/sqlite/models/node_and_port_scan" 9 | migrate "github.com/rubenv/sql-migrate" 10 | "github.com/volatiletech/sqlboiler/v4/boil" 11 | "github.com/volatiletech/sqlboiler/v4/queries" 12 | "github.com/volatiletech/sqlboiler/v4/queries/qm" 13 | ) 14 | 15 | //go:generate sqlboiler sqlite3 -o ../db/sqlite/models/node_and_port_scan -c ../../configs/sqlboiler/node_and_port_scan.toml 16 | //go:generate go-bindata -pkg node_and_port_scan -o ../db/sqlite/migrations/node_and_port_scan/migrations.go ../../db/sqlite/migrations/node_and_port_scan 17 | 18 | type NodeAndPortScanPersister struct { 19 | *SQLite 20 | } 21 | 22 | func NewNodeAndPortScanPersister(dbPath string) *NodeAndPortScanPersister { 23 | return &NodeAndPortScanPersister{ 24 | &SQLite{ 25 | DBPath: dbPath, 26 | Migrations: migrate.AssetMigrationSource{ 27 | Asset: node_and_port_scan.Asset, 28 | AssetDir: node_and_port_scan.AssetDir, 29 | Dir: "../../db/sqlite/migrations/node_and_port_scan", 30 | }, 31 | }, 32 | } 33 | } 34 | 35 | func (d *NodeAndPortScanPersister) CreateNodeScan(nodeScan *models.NodeScan) error { 36 | return nodeScan.Insert(context.Background(), d.db, boil.Infer()) 37 | } 38 | 39 | func (d *NodeAndPortScanPersister) CreateNode(node *models.Node) error { 40 | return node.Insert(context.Background(), d.db, boil.Infer()) 41 | } 42 | 43 | func (d *NodeAndPortScanPersister) CreatePortScan(portScan *models.PortScan) error { 44 | return portScan.Insert(context.Background(), d.db, boil.Infer()) 45 | } 46 | 47 | func (d *NodeAndPortScanPersister) CreatePort(port *models.Port) error { 48 | return port.Insert(context.Background(), d.db, boil.Infer()) 49 | } 50 | 51 | func (d *NodeAndPortScanPersister) GetNodeScans() (models.NodeScanSlice, error) { 52 | return models.NodeScans(qm.OrderBy(models.NodeScanColumns.CreatedAt+" DESC")).All(context.Background(), d.db) 53 | } 54 | 55 | func (d *NodeAndPortScanPersister) GetNodeScan(nodeScanID int64) (*models.NodeScan, error) { 56 | return models.FindNodeScan(context.Background(), d.db, nodeScanID) 57 | } 58 | 59 | func (d *NodeAndPortScanPersister) GetNodes(nodeScanID int64) (models.NodeSlice, error) { 60 | return models.Nodes(models.NodeWhere.NodeScanID.EQ(nodeScanID), qm.OrderBy(models.NodeColumns.CreatedAt+" DESC")).All(context.Background(), d.db) 61 | } 62 | 63 | func (d *NodeAndPortScanPersister) GetNodeByMACAddress(macAddress string) (*models.Node, error) { 64 | return models.Nodes(models.NodeWhere.MacAddress.EQ(macAddress)).One(context.Background(), d.db) 65 | } 66 | 67 | func (d *NodeAndPortScanPersister) GetLookbackNodes() (models.NodeSlice, error) { 68 | var uniqueNodes models.NodeSlice 69 | if err := queries.Raw( 70 | fmt.Sprintf( 71 | `select *, max(%v) from %v group by %v`, 72 | models.NodeColumns.CreatedAt, 73 | models.TableNames.Nodes, 74 | models.NodeColumns.MacAddress, 75 | ), 76 | ).Bind(context.Background(), d.db, &uniqueNodes); err != nil { 77 | return nil, err 78 | } 79 | 80 | return uniqueNodes, nil 81 | } 82 | 83 | func (d *NodeAndPortScanPersister) GetPortScans(nodeID int64) (models.PortScanSlice, error) { 84 | return models.PortScans(models.PortScanWhere.NodeID.EQ(nodeID), qm.OrderBy(models.PortScanColumns.CreatedAt+" DESC")).All(context.Background(), d.db) 85 | } 86 | 87 | func (d *NodeAndPortScanPersister) GetPortScan(portScanID int64) (*models.PortScan, error) { 88 | return models.FindPortScan(context.Background(), d.db, portScanID) 89 | } 90 | 91 | func (d *NodeAndPortScanPersister) GetLatestPortScanForNodeId(macAddress string) (*models.PortScan, error) { 92 | var latestPortScan models.PortScan 93 | if err := queries.Raw( 94 | fmt.Sprintf( 95 | `select * from %v where %v = 1 and %v in (select %v from %v where %v=$1 order by %v desc) order by %v desc limit 1`, 96 | models.TableNames.PortScans, 97 | models.PortScanColumns.Done, 98 | models.PortScanColumns.NodeID, 99 | models.NodeColumns.ID, 100 | models.TableNames.Nodes, 101 | models.NodeColumns.MacAddress, 102 | models.NodeColumns.CreatedAt, 103 | models.PortScanColumns.CreatedAt, 104 | ), 105 | macAddress, 106 | ).Bind(context.Background(), d.db, &latestPortScan); err != nil { 107 | return nil, err 108 | } 109 | 110 | return &latestPortScan, nil 111 | } 112 | 113 | func (d *NodeAndPortScanPersister) GetPorts(portScanID int64) (models.PortSlice, error) { 114 | return models.Ports(models.PortWhere.PortScanID.EQ(portScanID), qm.OrderBy(models.PortColumns.CreatedAt+" DESC")).All(context.Background(), d.db) 115 | } 116 | 117 | func (d *NodeAndPortScanPersister) UpdateNodeScan(nodeScan *models.NodeScan) error { 118 | _, err := nodeScan.Update(context.Background(), d.db, boil.Infer()) 119 | 120 | return err 121 | } 122 | 123 | func (d *NodeAndPortScanPersister) UpdatePortScan(portScan *models.PortScan) error { 124 | _, err := portScan.Update(context.Background(), d.db, boil.Infer()) 125 | 126 | return err 127 | } 128 | -------------------------------------------------------------------------------- /pkg/persisters/node_wake.go: -------------------------------------------------------------------------------- 1 | package persisters 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pojntfx/liwasc/pkg/db/sqlite/migrations/node_wake" 7 | models "github.com/pojntfx/liwasc/pkg/db/sqlite/models/node_wake" 8 | migrate "github.com/rubenv/sql-migrate" 9 | "github.com/volatiletech/sqlboiler/v4/boil" 10 | "github.com/volatiletech/sqlboiler/v4/queries/qm" 11 | ) 12 | 13 | //go:generate sqlboiler sqlite3 -o ../db/sqlite/models/node_wake -c ../../configs/sqlboiler/node_wake.toml 14 | //go:generate go-bindata -pkg node_wake -o ../db/sqlite/migrations/node_wake/migrations.go ../../db/sqlite/migrations/node_wake 15 | 16 | type NodeWakePersister struct { 17 | *SQLite 18 | } 19 | 20 | func NewNodeWakePersister(dbPath string) *NodeWakePersister { 21 | return &NodeWakePersister{ 22 | &SQLite{ 23 | DBPath: dbPath, 24 | Migrations: migrate.AssetMigrationSource{ 25 | Asset: node_wake.Asset, 26 | AssetDir: node_wake.AssetDir, 27 | Dir: "../../db/sqlite/migrations/node_wake", 28 | }, 29 | }, 30 | } 31 | } 32 | 33 | func (d *NodeWakePersister) CreateNodeWake(nodeWake *models.NodeWake) error { 34 | return nodeWake.Insert(context.Background(), d.db, boil.Infer()) 35 | } 36 | 37 | func (d *NodeWakePersister) UpdateNodeWake(nodeWake *models.NodeWake) error { 38 | _, err := nodeWake.Update(context.Background(), d.db, boil.Infer()) 39 | 40 | return err 41 | } 42 | 43 | func (d *NodeWakePersister) GetNodeWakes() (models.NodeWakeSlice, error) { 44 | return models.NodeWakes(qm.OrderBy(models.NodeWakeColumns.CreatedAt)).All(context.Background(), d.db) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/persisters/ports2packets.go: -------------------------------------------------------------------------------- 1 | package persisters 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "io/ioutil" 7 | 8 | "github.com/jszwec/csvutil" 9 | ) 10 | 11 | type RawPacket struct { 12 | Port int `csv:"port"` 13 | Packet string `csv:"packet"` 14 | } 15 | 16 | type Packet struct { 17 | Port int 18 | Packet []byte 19 | } 20 | 21 | type Ports2PacketPersister struct { 22 | *ExternalSource 23 | dbPath string 24 | packets map[int]*Packet 25 | } 26 | 27 | func NewPorts2PacketPersister(dbPath string, sourceURL string) *Ports2PacketPersister { 28 | return &Ports2PacketPersister{ 29 | ExternalSource: &ExternalSource{ 30 | SourceURL: sourceURL, 31 | DestinationPath: dbPath, 32 | }, 33 | dbPath: dbPath, 34 | packets: make(map[int]*Packet), 35 | } 36 | } 37 | 38 | func (d *Ports2PacketPersister) Open() error { 39 | // If CSV file does not exist, download & create it 40 | if err := d.ExternalSource.PullIfNotExists(); err != nil { 41 | return err 42 | } 43 | 44 | // Read CSV file 45 | contents, err := ioutil.ReadFile(d.dbPath) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | var rawPackets []RawPacket 51 | if err := csvutil.Unmarshal(contents, &rawPackets); err != nil { 52 | return err 53 | } 54 | 55 | // Decode base64 encoded data 56 | for _, rawPacket := range rawPackets { 57 | content, err := base64.StdEncoding.DecodeString(rawPacket.Packet) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | d.packets[rawPacket.Port] = &Packet{rawPacket.Port, content} 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (d *Ports2PacketPersister) GetPacket(port int) (*Packet, error) { 69 | packet := d.packets[port] 70 | 71 | if packet == nil { 72 | return nil, fmt.Errorf("could not find packet for port %v", port) 73 | } 74 | 75 | return packet, nil 76 | } 77 | -------------------------------------------------------------------------------- /pkg/persisters/service_names_port_numbers.go: -------------------------------------------------------------------------------- 1 | package persisters 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/jszwec/csvutil" 10 | ) 11 | 12 | type Service struct { 13 | ServiceName string `csv:"Service Name"` 14 | PortNumber string `csv:"Port Number"` 15 | TransportProtocol string `csv:"Transport Protocol"` 16 | Description string `csv:"Description"` 17 | Assignee string `csv:"Assignee"` 18 | Contact string `csv:"Contact"` 19 | RegistrationDate string `csv:"Registration Date"` 20 | ModificationDate string `csv:"Modification Date"` 21 | Reference string `csv:"Reference"` 22 | ServiceCode string `csv:"Service Code"` 23 | UnauthorizedUseReported string `csv:"Unauthorized Use Reported"` 24 | AssignmentNotes string `csv:"Assignment Notes"` 25 | } 26 | 27 | type ServiceNamesPortNumbersPersister struct { 28 | *ExternalSource 29 | dbPath string 30 | services map[int][]Service 31 | } 32 | 33 | func NewServiceNamesPortNumbersPersister(dbPath string, sourceURL string) *ServiceNamesPortNumbersPersister { 34 | return &ServiceNamesPortNumbersPersister{ 35 | ExternalSource: &ExternalSource{ 36 | SourceURL: sourceURL, 37 | DestinationPath: dbPath, 38 | }, 39 | dbPath: dbPath, 40 | services: make(map[int][]Service), 41 | } 42 | } 43 | 44 | func (d *ServiceNamesPortNumbersPersister) Open() error { 45 | // If CSV file does not exist, download & create it 46 | if err := d.ExternalSource.PullIfNotExists(); err != nil { 47 | return err 48 | } 49 | 50 | // Read CSV file 51 | contents, err := ioutil.ReadFile(d.dbPath) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | var rawServices []Service 57 | if err := csvutil.Unmarshal(contents, &rawServices); err != nil { 58 | return err 59 | } 60 | 61 | for _, service := range rawServices { 62 | rangePoints := strings.Split(service.PortNumber, "-") 63 | 64 | // Skip services with empty ports 65 | rawStartPort := rangePoints[0] 66 | if rawStartPort == "" { 67 | continue 68 | } 69 | 70 | startPort, err := strconv.Atoi(rawStartPort) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | d.services[startPort] = append(d.services[startPort], service) 76 | 77 | // Port range 78 | if len(rangePoints) > 1 { 79 | rawEndPort := rangePoints[1] 80 | 81 | endPort, err := strconv.Atoi(rawEndPort) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | for currentPort := startPort + 1; currentPort <= endPort; currentPort++ { 87 | d.services[currentPort] = append(d.services[currentPort], service) 88 | } 89 | } 90 | } 91 | 92 | return nil 93 | } 94 | 95 | // GetService returns the services that match the port and protocol given 96 | // Use "*" as the protocol to find all services on the port independent of protocol 97 | func (d *ServiceNamesPortNumbersPersister) GetService(port int, protocol string) ([]Service, error) { 98 | allServicesForProtocol := d.services[port] 99 | if allServicesForProtocol == nil { 100 | return nil, fmt.Errorf("could not find service(s) for port %v", port) 101 | } 102 | 103 | outServices := make([]Service, 0) 104 | for _, service := range allServicesForProtocol { 105 | if service.TransportProtocol == protocol || protocol == "*" { 106 | outServices = append(outServices, service) 107 | } 108 | } 109 | 110 | if len(outServices) < 1 { 111 | return nil, fmt.Errorf("could find service(s) for port %v, but not for protocol %v on that port", port, protocol) 112 | } 113 | 114 | return outServices, nil 115 | } 116 | -------------------------------------------------------------------------------- /pkg/persisters/sqlite.go: -------------------------------------------------------------------------------- 1 | package persisters 2 | 3 | import ( 4 | "database/sql" 5 | "os" 6 | "path/filepath" 7 | 8 | migrate "github.com/rubenv/sql-migrate" 9 | ) 10 | 11 | type SQLite struct { 12 | DBPath string 13 | Migrations migrate.MigrationSource 14 | 15 | db *sql.DB 16 | } 17 | 18 | func (d *SQLite) Open() error { 19 | // Create leading directories for database 20 | leadingDir, _ := filepath.Split(d.DBPath) 21 | if err := os.MkdirAll(leadingDir, os.ModePerm); err != nil { 22 | return err 23 | } 24 | 25 | // Open the DB 26 | db, err := sql.Open("sqlite3", d.DBPath) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | // Configure the db 32 | db.SetMaxOpenConns(1) // Prevent "database locked" errors 33 | d.db = db 34 | 35 | // Run migrations if set 36 | if d.Migrations != nil { 37 | if _, err := migrate.Exec(d.db, "sqlite3", d.Migrations, migrate.Up); err != nil { 38 | return err 39 | } 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /pkg/providers/identity_provider.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/coreos/go-oidc/v3/oidc" 9 | "github.com/maxence-charriere/go-app/v8/pkg/app" 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | const ( 14 | oauth2TokenKey = "oauth2Token" 15 | idTokenKey = "idToken" 16 | userInfoKey = "userInfo" 17 | 18 | StateQueryParameter = "state" 19 | CodeQueryParameter = "code" 20 | 21 | idTokenExtraKey = "id_token" 22 | ) 23 | 24 | type IdentityProviderChildrenProps struct { 25 | IDToken string 26 | UserInfo oidc.UserInfo 27 | 28 | Logout func() 29 | 30 | Error error 31 | Recover func() 32 | } 33 | 34 | type IdentityProvider struct { 35 | app.Compo 36 | 37 | Issuer string 38 | ClientID string 39 | RedirectURL string 40 | HomeURL string 41 | Scopes []string 42 | StoragePrefix string 43 | Children func(IdentityProviderChildrenProps) app.UI 44 | 45 | oauth2Token oauth2.Token 46 | idToken string 47 | userInfo oidc.UserInfo 48 | 49 | err error 50 | } 51 | 52 | func (c *IdentityProvider) Render() app.UI { 53 | return c.Children( 54 | IdentityProviderChildrenProps{ 55 | IDToken: c.idToken, 56 | UserInfo: c.userInfo, 57 | 58 | Logout: func() { 59 | c.Defer(func(ctx app.Context) { 60 | c.logout(true, ctx) 61 | }) 62 | }, 63 | 64 | Error: c.err, 65 | Recover: c.recover, 66 | }, 67 | ) 68 | } 69 | 70 | func (c *IdentityProvider) OnMount(ctx app.Context) { 71 | // Only continue if there is no error state; this prevents endless loops 72 | if c.err == nil { 73 | c.dispatch(func(ctx app.Context) { 74 | c.authorize(ctx) 75 | }) 76 | } 77 | } 78 | 79 | func (c *IdentityProvider) OnNav(ctx app.Context) { 80 | // Only continue if there is no error state; this prevents endless loops 81 | if c.err == nil { 82 | c.dispatch(func(ctx app.Context) { 83 | c.authorize(ctx) 84 | }) 85 | } 86 | } 87 | 88 | func (c *IdentityProvider) panic(err error) { 89 | go func() { 90 | c.dispatch(func(ctx app.Context) { 91 | // Set the error 92 | c.err = err 93 | }) 94 | 95 | // Prevent infinite retries 96 | time.Sleep(time.Second) 97 | 98 | // Unset the error & enable re-trying 99 | c.err = err 100 | }() 101 | } 102 | 103 | func (c *IdentityProvider) recover() { 104 | c.dispatch(func(ctx app.Context) { 105 | // Clear the error 106 | c.err = nil 107 | 108 | // Logout 109 | c.logout(false, ctx) 110 | }) 111 | } 112 | 113 | func (c *IdentityProvider) dispatch(action func(ctx app.Context)) { 114 | c.Defer(func(ctx app.Context) { 115 | action(ctx) 116 | }) 117 | 118 | c.Update() 119 | } 120 | 121 | func (c *IdentityProvider) watch() { 122 | for { 123 | // Wait till token expires 124 | if c.oauth2Token.Expiry.After(time.Now()) { 125 | time.Sleep(c.oauth2Token.Expiry.Sub(time.Now())) 126 | } 127 | 128 | // Fetch new OAuth2 token 129 | oauth2Token, err := oauth2.StaticTokenSource(&c.oauth2Token).Token() 130 | if err != nil { 131 | c.panic(err) 132 | 133 | return 134 | } 135 | 136 | // Parse ID token 137 | idToken, ok := oauth2Token.Extra("id_token").(string) 138 | if !ok { 139 | c.panic(err) 140 | 141 | return 142 | } 143 | 144 | // Set the login state 145 | c.dispatch(func(ctx app.Context) { 146 | // Persist state in storage 147 | if err := c.persist(*oauth2Token, idToken, c.userInfo, ctx); err != nil { 148 | c.panic(err) 149 | 150 | return 151 | } 152 | 153 | c.oauth2Token = *oauth2Token 154 | c.idToken = idToken 155 | }) 156 | } 157 | } 158 | 159 | func (c *IdentityProvider) logout(withRedirect bool, ctx app.Context) { 160 | // Remove from storage 161 | c.clear(ctx) 162 | 163 | // Reload the app 164 | if withRedirect { 165 | ctx.Reload() 166 | } 167 | } 168 | 169 | func (c *IdentityProvider) rehydrate(ctx app.Context) (oauth2.Token, string, oidc.UserInfo, error) { 170 | // Read state from storage 171 | oauth2Token := oauth2.Token{} 172 | idToken := "" 173 | userInfo := oidc.UserInfo{} 174 | 175 | if err := ctx.LocalStorage().Get(c.getKey(oauth2TokenKey), &oauth2Token); err != nil { 176 | return oauth2.Token{}, "", oidc.UserInfo{}, err 177 | } 178 | if err := ctx.LocalStorage().Get(c.getKey(idTokenKey), &idToken); err != nil { 179 | return oauth2.Token{}, "", oidc.UserInfo{}, err 180 | } 181 | if err := ctx.LocalStorage().Get(c.getKey(userInfoKey), &userInfo); err != nil { 182 | return oauth2.Token{}, "", oidc.UserInfo{}, err 183 | } 184 | 185 | return oauth2Token, idToken, userInfo, nil 186 | } 187 | 188 | func (c *IdentityProvider) persist(oauth2Token oauth2.Token, idToken string, userInfo oidc.UserInfo, ctx app.Context) error { 189 | // Write state to storage 190 | if err := ctx.LocalStorage().Set(c.getKey(oauth2TokenKey), oauth2Token); err != nil { 191 | return err 192 | } 193 | if err := ctx.LocalStorage().Set(c.getKey(idTokenKey), idToken); err != nil { 194 | return err 195 | } 196 | return ctx.LocalStorage().Set(c.getKey(userInfoKey), userInfo) 197 | } 198 | 199 | func (c *IdentityProvider) clear(ctx app.Context) { 200 | // Remove from storage 201 | ctx.LocalStorage().Del(c.getKey(oauth2TokenKey)) 202 | ctx.LocalStorage().Del(c.getKey(idTokenKey)) 203 | ctx.LocalStorage().Del(c.getKey(userInfoKey)) 204 | 205 | // Remove cookies 206 | app.Window().Get("document").Set("cookie", "") 207 | } 208 | 209 | func (c *IdentityProvider) getKey(key string) string { 210 | // Get a prefixed key 211 | return fmt.Sprintf("%v.%v", c.StoragePrefix, key) 212 | } 213 | 214 | func (c *IdentityProvider) authorize(ctx app.Context) { 215 | // Read state from storage 216 | oauth2Token, idToken, userInfo, err := c.rehydrate(ctx) 217 | if err != nil { 218 | c.panic(err) 219 | 220 | return 221 | } 222 | 223 | // Create the OIDC provider 224 | provider, err := oidc.NewProvider(context.Background(), c.Issuer) 225 | if err != nil { 226 | c.panic(err) 227 | 228 | return 229 | } 230 | 231 | // Create the OAuth2 config 232 | config := &oauth2.Config{ 233 | ClientID: c.ClientID, 234 | RedirectURL: c.RedirectURL, 235 | Endpoint: provider.Endpoint(), 236 | Scopes: append([]string{oidc.ScopeOpenID}, c.Scopes...), 237 | } 238 | 239 | // Log in 240 | if oauth2Token.AccessToken == "" || userInfo.Email == "" { 241 | // Logged out state, info neither in storage nor in URL: Redirect to login 242 | if app.Window().URL().Query().Get(StateQueryParameter) == "" { 243 | ctx.Navigate(config.AuthCodeURL(c.RedirectURL, oauth2.AccessTypeOffline)) 244 | 245 | return 246 | } 247 | 248 | // Intermediate state, info is in URL: Parse OAuth2 token 249 | oauth2Token, err := config.Exchange(context.Background(), app.Window().URL().Query().Get(CodeQueryParameter)) 250 | if err != nil { 251 | c.panic(err) 252 | 253 | return 254 | } 255 | 256 | // Parse ID token 257 | idToken, ok := oauth2Token.Extra(idTokenExtraKey).(string) 258 | if !ok { 259 | c.panic(err) 260 | 261 | return 262 | } 263 | 264 | // Parse user info 265 | userInfo, err := provider.UserInfo(context.Background(), oauth2.StaticTokenSource(oauth2Token)) 266 | if err != nil { 267 | c.panic(err) 268 | 269 | return 270 | } 271 | 272 | // Persist state in storage 273 | if err := c.persist(*oauth2Token, idToken, *userInfo, ctx); err != nil { 274 | c.panic(err) 275 | 276 | return 277 | } 278 | 279 | // Test validity of storage 280 | if _, _, _, err = c.rehydrate(ctx); err != nil { 281 | c.panic(err) 282 | 283 | return 284 | } 285 | 286 | // Update and navigate to home URL 287 | c.Update() 288 | ctx.Navigate(c.HomeURL) 289 | 290 | return 291 | } 292 | 293 | // Validation state 294 | 295 | // Create the OIDC config 296 | oidcConfig := &oidc.Config{ 297 | ClientID: c.ClientID, 298 | } 299 | 300 | // Create the OIDC verifier and validate the token (i.e. check for it's expiry date) 301 | verifier := provider.Verifier(oidcConfig) 302 | if _, err := verifier.Verify(context.Background(), idToken); err != nil { 303 | // Invalid token; clear and re-authorize 304 | c.clear(ctx) 305 | c.authorize(ctx) 306 | 307 | return 308 | } 309 | 310 | // Logged in state 311 | 312 | // Set the login state 313 | c.oauth2Token = oauth2Token 314 | c.idToken = idToken 315 | c.userInfo = userInfo 316 | 317 | // Watch and renew token once expired 318 | go c.watch() 319 | 320 | return 321 | } 322 | -------------------------------------------------------------------------------- /pkg/providers/setup_provider.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | 8 | "github.com/maxence-charriere/go-app/v8/pkg/app" 9 | ) 10 | 11 | type SetupProviderChildrenProps struct { 12 | BackendURL string 13 | OIDCIssuer string 14 | OIDCClientID string 15 | OIDCRedirectURL string 16 | Ready bool 17 | 18 | SetBackendURL, 19 | SetOIDCIssuer, 20 | SetOIDCClientID, 21 | SetOIDCRedirectURL func(string) 22 | ApplyConfig func() 23 | 24 | Error error 25 | } 26 | 27 | type ConfigurationProvider struct { 28 | app.Compo 29 | 30 | StoragePrefix string 31 | StateQueryParameter string 32 | CodeQueryParameter string 33 | Children func(SetupProviderChildrenProps) app.UI 34 | 35 | backendURL string 36 | oidcIssuer string 37 | oidcClientID string 38 | oidcRedirectURL string 39 | ready bool 40 | 41 | err error 42 | } 43 | 44 | const ( 45 | backendURLKey = "backendURL" 46 | oidcIssuerKey = "oidcIssuer" 47 | oidcClientIDKey = "oidcClientID" 48 | oidcRedirectURLKey = "oidcRedirectURL" 49 | ) 50 | 51 | func (c *ConfigurationProvider) Render() app.UI { 52 | return c.Children(SetupProviderChildrenProps{ 53 | BackendURL: c.backendURL, 54 | OIDCIssuer: c.oidcIssuer, 55 | OIDCClientID: c.oidcClientID, 56 | OIDCRedirectURL: c.oidcRedirectURL, 57 | Ready: c.ready, 58 | 59 | SetBackendURL: func(s string) { 60 | c.dispatch(func(ctx app.Context) { 61 | c.ready = false 62 | c.backendURL = s 63 | }) 64 | }, 65 | SetOIDCIssuer: func(s string) { 66 | c.dispatch(func(ctx app.Context) { 67 | c.ready = false 68 | c.oidcIssuer = s 69 | }) 70 | }, 71 | SetOIDCClientID: func(s string) { 72 | c.dispatch(func(ctx app.Context) { 73 | c.ready = false 74 | c.oidcClientID = s 75 | }) 76 | }, 77 | SetOIDCRedirectURL: func(s string) { 78 | c.dispatch(func(ctx app.Context) { 79 | c.ready = false 80 | c.oidcRedirectURL = s 81 | }) 82 | }, 83 | ApplyConfig: func() { 84 | c.validate() 85 | }, 86 | 87 | Error: c.err, 88 | }) 89 | } 90 | 91 | func (c *ConfigurationProvider) invalidate(err error) { 92 | // Set the error state 93 | c.err = err 94 | c.ready = false 95 | 96 | c.Update() 97 | } 98 | 99 | func (c *ConfigurationProvider) dispatch(action func(ctx app.Context)) { 100 | c.Defer(func(ctx app.Context) { 101 | action(ctx) 102 | 103 | c.Update() 104 | }) 105 | } 106 | 107 | func (c *ConfigurationProvider) validate() { 108 | // Validate fields 109 | if c.oidcClientID == "" { 110 | c.invalidate(errors.New("invalid OIDC client ID")) 111 | 112 | return 113 | } 114 | 115 | if _, err := url.ParseRequestURI(c.oidcIssuer); err != nil { 116 | c.invalidate(fmt.Errorf("invalid OIDC issuer: %v", err)) 117 | 118 | return 119 | } 120 | 121 | if _, err := url.ParseRequestURI(c.backendURL); err != nil { 122 | c.invalidate(fmt.Errorf("invalid backend URL: %v", err)) 123 | 124 | return 125 | } 126 | 127 | if _, err := url.ParseRequestURI(c.oidcRedirectURL); err != nil { 128 | c.invalidate(fmt.Errorf("invalid OIDC redirect URL: %v", err)) 129 | 130 | return 131 | } 132 | 133 | c.dispatch(func(ctx app.Context) { 134 | // Persist state 135 | if err := c.persist(ctx); err != nil { 136 | c.invalidate(err) 137 | 138 | return 139 | } 140 | 141 | // If all are valid, set ready state 142 | c.err = nil 143 | c.ready = true 144 | }) 145 | } 146 | 147 | func (c *ConfigurationProvider) persist(ctx app.Context) error { 148 | // Write state to storage 149 | if err := ctx.LocalStorage().Set(c.getKey(backendURLKey), c.backendURL); err != nil { 150 | return err 151 | } 152 | if err := ctx.LocalStorage().Set(c.getKey(oidcIssuerKey), c.oidcIssuer); err != nil { 153 | return err 154 | } 155 | if err := ctx.LocalStorage().Set(c.getKey(oidcClientIDKey), c.oidcClientID); err != nil { 156 | return err 157 | } 158 | 159 | return ctx.LocalStorage().Set(c.getKey(oidcRedirectURLKey), c.oidcRedirectURL) 160 | } 161 | 162 | func (c *ConfigurationProvider) rehydrateFromURL() bool { 163 | // Read state from URL 164 | query := app.Window().URL().Query() 165 | 166 | backendURL := query.Get(backendURLKey) 167 | oidcIssuer := query.Get(oidcIssuerKey) 168 | oidcClientID := query.Get(oidcClientIDKey) 169 | oidcRedirectURL := query.Get(oidcRedirectURLKey) 170 | 171 | // If all values are set, set them in the data provider 172 | if backendURL != "" && oidcIssuer != "" && oidcClientID != "" && oidcRedirectURL != "" { 173 | c.dispatch(func(ctx app.Context) { 174 | c.backendURL = backendURL 175 | c.oidcIssuer = oidcIssuer 176 | c.oidcClientID = oidcClientID 177 | c.oidcRedirectURL = oidcRedirectURL 178 | }) 179 | 180 | return true 181 | } 182 | 183 | return false 184 | } 185 | 186 | func (c *ConfigurationProvider) rehydrateFromStorage(ctx app.Context) bool { 187 | // Read state from storage 188 | backendURL := "" 189 | oidcIssuer := "" 190 | oidcClientID := "" 191 | oidcRedirectURL := "" 192 | 193 | if err := ctx.LocalStorage().Get(c.getKey(backendURLKey), &backendURL); err != nil { 194 | c.invalidate(err) 195 | 196 | return false 197 | } 198 | if err := ctx.LocalStorage().Get(c.getKey(oidcIssuerKey), &oidcIssuer); err != nil { 199 | c.invalidate(err) 200 | 201 | return false 202 | } 203 | if err := ctx.LocalStorage().Get(c.getKey(oidcClientIDKey), &oidcClientID); err != nil { 204 | c.invalidate(err) 205 | 206 | return false 207 | } 208 | if err := ctx.LocalStorage().Get(c.getKey(oidcRedirectURLKey), &oidcRedirectURL); err != nil { 209 | c.invalidate(err) 210 | 211 | return false 212 | } 213 | 214 | // If all values are set, set them in the data provider 215 | if backendURL != "" && oidcIssuer != "" && oidcClientID != "" && oidcRedirectURL != "" { 216 | c.dispatch(func(ctx app.Context) { 217 | c.backendURL = backendURL 218 | c.oidcIssuer = oidcIssuer 219 | c.oidcClientID = oidcClientID 220 | c.oidcRedirectURL = oidcRedirectURL 221 | }) 222 | 223 | return true 224 | } 225 | 226 | return false 227 | } 228 | 229 | func (c *ConfigurationProvider) rehydrateAuthenticationFromURL() bool { 230 | // Read state from URL 231 | query := app.Window().URL().Query() 232 | 233 | state := query.Get(c.StateQueryParameter) 234 | code := query.Get(c.CodeQueryParameter) 235 | 236 | // If all values are set, set them in the data provider 237 | if state != "" && code != "" { 238 | return true 239 | } 240 | 241 | return false 242 | } 243 | 244 | func (c *ConfigurationProvider) getKey(key string) string { 245 | // Get a prefixed key 246 | return fmt.Sprintf("%v.%v", c.StoragePrefix, key) 247 | } 248 | 249 | func (c *ConfigurationProvider) OnMount(context app.Context) { 250 | // Initialize state 251 | c.backendURL = "" 252 | c.oidcIssuer = "" 253 | c.oidcClientID = "" 254 | c.oidcRedirectURL = "" 255 | c.ready = false 256 | 257 | // If rehydrated from URL, validate & apply 258 | if c.rehydrateFromURL() { 259 | // Auto-apply if configured 260 | // Disabled until a flow for handling wrong input details has been implemented 261 | // c.validate() 262 | } 263 | 264 | // If rehydrated from storage, validate & apply 265 | c.dispatch(func(ctx app.Context) { 266 | if c.rehydrateFromStorage(ctx) { 267 | // Auto-apply if configured 268 | // Disabled until a flow for handling wrong input details has been implemented 269 | // c.validate() 270 | } 271 | }) 272 | 273 | // If rehydrated authentication from URL, continue 274 | if c.rehydrateAuthenticationFromURL() { 275 | // Auto-apply if configured; set ready state 276 | c.dispatch(func(ctx app.Context) { 277 | c.err = nil 278 | c.ready = true 279 | }) 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /pkg/scanners/node.go: -------------------------------------------------------------------------------- 1 | package scanners 2 | 3 | // Based on https://github.com/google/gopacket/blob/master/examples/arpscan/arpscan.go 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "encoding/binary" 9 | "net" 10 | 11 | "github.com/google/gopacket" 12 | "github.com/google/gopacket/layers" 13 | "github.com/google/gopacket/pcap" 14 | "github.com/phayes/freeport" 15 | ) 16 | 17 | type DiscoveredNode struct { 18 | IPAddress net.IP 19 | MACAddress net.HardwareAddr 20 | } 21 | 22 | type NodeScanner struct { 23 | deviceName string 24 | handle *pcap.Handle 25 | ipv4addresses []*net.IPNet 26 | iface *net.Interface 27 | discoveredNodeChan chan *DiscoveredNode 28 | } 29 | 30 | func NewNodeScanner(device string) *NodeScanner { 31 | return &NodeScanner{device, nil, nil, nil, make(chan *DiscoveredNode)} 32 | } 33 | 34 | func (s *NodeScanner) Open() ([]*net.IPNet, error) { 35 | iface, err := net.InterfaceByName(s.deviceName) 36 | if err != nil { 37 | return nil, err 38 | } 39 | s.iface = iface 40 | 41 | addresses, err := iface.Addrs() 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | for _, ipv4address := range addresses { 47 | ip := ipv4address.(*net.IPNet).IP.To4() 48 | 49 | if ip != nil { 50 | s.ipv4addresses = append(s.ipv4addresses, &net.IPNet{ 51 | IP: ip, 52 | Mask: ipv4address.(*net.IPNet).Mask[len(ipv4address.(*net.IPNet).Mask)-4:], 53 | }) 54 | } 55 | } 56 | 57 | port, err := freeport.GetFreePort() 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | handle, err := pcap.OpenLive(iface.Name, int32(port), true, pcap.BlockForever) 63 | if err != nil { 64 | return nil, err 65 | } 66 | s.handle = handle 67 | 68 | return s.ipv4addresses, nil 69 | } 70 | 71 | func (s *NodeScanner) Receive(ctx context.Context) error { 72 | in := gopacket.NewPacketSource(s.handle, layers.LayerTypeEthernet).Packets() 73 | 74 | for { 75 | select { 76 | case <-ctx.Done(): 77 | s.discoveredNodeChan <- nil 78 | close(s.discoveredNodeChan) 79 | 80 | return nil 81 | case packet := <-in: 82 | layer := packet.Layer(layers.LayerTypeARP) 83 | 84 | // Not an arp packet 85 | if layer == nil { 86 | continue 87 | } 88 | 89 | // Sent by us 90 | arp := layer.(*layers.ARP) 91 | if arp.Operation != layers.ARPReply || bytes.Equal([]byte(s.iface.HardwareAddr), arp.SourceHwAddress) { 92 | continue 93 | } 94 | 95 | s.discoveredNodeChan <- &DiscoveredNode{net.IP(arp.SourceProtAddress), net.HardwareAddr(arp.SourceHwAddress)} 96 | } 97 | } 98 | } 99 | 100 | func (s *NodeScanner) Transmit() error { 101 | eth := layers.Ethernet{ 102 | SrcMAC: s.iface.HardwareAddr, 103 | DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, // Broadcast 104 | EthernetType: layers.EthernetTypeARP, 105 | } 106 | 107 | for _, ipv4addr := range s.ipv4addresses { 108 | arp := layers.ARP{ 109 | AddrType: layers.LinkTypeEthernet, 110 | Protocol: layers.EthernetTypeIPv4, 111 | HwAddressSize: 6, 112 | ProtAddressSize: 4, 113 | Operation: layers.ARPRequest, 114 | SourceHwAddress: []byte(s.iface.HardwareAddr), 115 | SourceProtAddress: []byte(ipv4addr.IP), 116 | DstHwAddress: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, // Broadcast 117 | } 118 | 119 | buffer := gopacket.NewSerializeBuffer() 120 | for _, ip := range GetAddressesForNet(ipv4addr) { 121 | arp.DstProtAddress = []byte(ip) 122 | gopacket.SerializeLayers(buffer, gopacket.SerializeOptions{ 123 | FixLengths: true, 124 | ComputeChecksums: true, 125 | }, ð, &arp) 126 | 127 | if err := s.handle.WritePacketData(buffer.Bytes()); err != nil { 128 | return err 129 | } 130 | } 131 | } 132 | 133 | return nil 134 | } 135 | 136 | func (s *NodeScanner) Read() *DiscoveredNode { 137 | node := <-s.discoveredNodeChan 138 | 139 | return node 140 | } 141 | 142 | func GetAddressesForNet(n *net.IPNet) (out []net.IP) { 143 | num := binary.BigEndian.Uint32([]byte(n.IP)) 144 | mask := binary.BigEndian.Uint32([]byte(n.Mask)) 145 | num &= mask 146 | for mask < 0xffffffff { 147 | var buf [4]byte 148 | binary.BigEndian.PutUint32(buf[:], num) 149 | out = append(out, net.IP(buf[:])) 150 | mask++ 151 | num++ 152 | } 153 | return 154 | } 155 | -------------------------------------------------------------------------------- /pkg/scanners/ports.go: -------------------------------------------------------------------------------- 1 | package scanners 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/google/gopacket" 13 | "github.com/google/gopacket/layers" 14 | "golang.org/x/sync/semaphore" 15 | ) 16 | 17 | type ScannedPort struct { 18 | Target string 19 | Port int 20 | Protocol string 21 | Open bool 22 | } 23 | 24 | type PortScanner struct { 25 | target string 26 | startPort int 27 | endPort int 28 | timeout time.Duration 29 | protocols []string 30 | scannedPortChan chan *ScannedPort 31 | semaphore *semaphore.Weighted 32 | packetGetter func(port int) ([]byte, error) 33 | } 34 | 35 | func NewPortScanner(target string, startPort int, endPort int, timeout time.Duration, protocols []string, semaphore *semaphore.Weighted, packetGetter func(port int) ([]byte, error)) *PortScanner { 36 | return &PortScanner{target, startPort, endPort, timeout, protocols, make(chan *ScannedPort), semaphore, packetGetter} 37 | } 38 | 39 | func (s *PortScanner) Transmit() error { 40 | // Concurrency 41 | wg := sync.WaitGroup{} 42 | 43 | for port := s.startPort; port <= s.endPort; port++ { 44 | for _, protocol := range s.protocols { 45 | // Aquire lock 46 | wg.Add(1) 47 | if err := s.semaphore.Acquire(context.Background(), 1); err != nil { 48 | return err 49 | } 50 | 51 | go func(targetPort int, targetProtocol string, iwg *sync.WaitGroup) { 52 | // Release lock 53 | defer s.semaphore.Release(1) 54 | defer iwg.Done() 55 | 56 | // Start scan 57 | for { 58 | if targetProtocol == "tcp" { 59 | // Scan TCP 60 | open, err := ScanTCPPort(s.target, targetPort, s.timeout) 61 | if err != nil && !err.(net.Error).Timeout() { 62 | log.Println("could not scan TCP port", err) 63 | 64 | return 65 | } 66 | 67 | // Handle scan result 68 | if open { 69 | s.scannedPortChan <- &ScannedPort{s.target, targetPort, targetProtocol, true} 70 | } else { 71 | s.scannedPortChan <- &ScannedPort{s.target, targetPort, targetProtocol, false} 72 | } 73 | 74 | break 75 | } else if targetProtocol == "udp" { 76 | // Scan UDP 77 | open, err := ScanUDPPort(s.target, targetPort, s.timeout, func(port int) ([]byte, error) { 78 | return s.packetGetter(port) 79 | }) 80 | if err != nil && !err.(net.Error).Timeout() { 81 | log.Println("could not scan UDP port", err) 82 | 83 | return 84 | } 85 | 86 | // Handle scan result 87 | if open { 88 | s.scannedPortChan <- &ScannedPort{s.target, targetPort, targetProtocol, true} 89 | } else { 90 | s.scannedPortChan <- &ScannedPort{s.target, targetPort, targetProtocol, false} 91 | } 92 | 93 | break 94 | } 95 | } 96 | }(port, protocol, &wg) 97 | } 98 | } 99 | 100 | // Wait till all have finished 101 | wg.Wait() 102 | 103 | s.scannedPortChan <- nil 104 | 105 | return nil 106 | } 107 | 108 | func (s *PortScanner) Read() *ScannedPort { 109 | port := <-s.scannedPortChan 110 | 111 | return port 112 | } 113 | 114 | func ScanTCPPort(targetAddress string, targetPort int, timeout time.Duration) (bool, error) { 115 | // Get local socket 116 | raddr := net.ParseIP(targetAddress).To4() 117 | rport := layers.TCPPort(targetPort) 118 | 119 | // Create connection 120 | con, err := net.Dial("udp", net.JoinHostPort(targetAddress, strconv.Itoa(targetPort))) 121 | if err != nil { 122 | return false, err 123 | } 124 | 125 | // Get remote socket 126 | laddr := con.LocalAddr().(*net.UDPAddr) 127 | lport := layers.TCPPort(laddr.Port) 128 | 129 | // Create IP packet 130 | outIP := &layers.IPv4{ 131 | SrcIP: laddr.IP, 132 | DstIP: raddr, 133 | Protocol: layers.IPProtocolTCP, 134 | } 135 | 136 | // Create TCP segment 137 | outTCP := &layers.TCP{ 138 | SrcPort: lport, 139 | DstPort: rport, 140 | Seq: 1, 141 | SYN: true, 142 | Window: 14600, 143 | } 144 | 145 | outTCP.SetNetworkLayerForChecksum(outIP) 146 | 147 | // Serialize packet 148 | outPacket := gopacket.NewSerializeBuffer() 149 | if err := gopacket.SerializeLayers( 150 | outPacket, 151 | gopacket.SerializeOptions{ 152 | ComputeChecksums: true, 153 | FixLengths: true, 154 | }, 155 | outTCP, 156 | ); err != nil { 157 | return false, err 158 | } 159 | 160 | // Listen for incoming packets 161 | conn, err := net.ListenPacket("ip4:tcp", "0.0.0.0") 162 | if err != nil { 163 | return false, err 164 | } 165 | defer conn.Close() 166 | 167 | // Write packet 168 | if _, err := conn.WriteTo( 169 | outPacket.Bytes(), 170 | &net.IPAddr{ 171 | IP: raddr, 172 | }, 173 | ); err != nil { 174 | return false, err 175 | } 176 | 177 | // Set timeout 178 | if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil { 179 | return false, err 180 | } 181 | 182 | for { 183 | // Receive packet 184 | buf := make([]byte, 4096) 185 | n, addr, err := conn.ReadFrom(buf) 186 | if err != nil { 187 | return false, err 188 | } 189 | 190 | // If packet is not intended for our IP, skip it 191 | if addr.String() != raddr.String() { 192 | continue 193 | } 194 | 195 | // If packet is intended for our IP, process it 196 | inPacket := gopacket.NewPacket(buf[:n], layers.LayerTypeTCP, gopacket.Default) 197 | 198 | // Skip non-TCP packets 199 | if inTCPLayer := inPacket.Layer(layers.LayerTypeTCP); inTCPLayer != nil { 200 | inTCP := inTCPLayer.(*layers.TCP) 201 | 202 | // If segment is not intended for our port, skip it 203 | if inTCP.DstPort != lport { 204 | continue 205 | } 206 | 207 | // If SYN and ACK bits are set, the port is open 208 | if inTCP.SYN && inTCP.ACK { 209 | return true, nil 210 | } 211 | 212 | // Port is closed 213 | return false, nil 214 | } 215 | } 216 | } 217 | 218 | func ScanUDPPort(targetAddress string, targetPort int, timeout time.Duration, packetGetter func(port int) ([]byte, error)) (bool, error) { 219 | // Create connection 220 | con, err := net.Dial("udp", net.JoinHostPort(targetAddress, strconv.Itoa(targetPort))) 221 | if err != nil { 222 | return false, err 223 | } 224 | 225 | // Set timeout 226 | if err := con.SetDeadline(time.Now().Add(timeout)); err != nil { 227 | return false, err 228 | } 229 | 230 | // Get known packet for port 231 | packet, err := packetGetter(targetPort) 232 | if err != nil { 233 | if strings.Contains(err.Error(), "could not find packet for port") { 234 | packet = []byte("Hello from liwasc!\n") // Unknown packet for port, send a test string 235 | } else { 236 | return false, err 237 | } 238 | } 239 | 240 | // Write packet 241 | if _, err := con.Write(packet); err != nil { 242 | return false, err 243 | } 244 | 245 | // Count every response that is at least 1 byte long as a "open port" 246 | buffer := make([]byte, 1) 247 | if _, err := con.Read(buffer); err != nil { 248 | // Port is closed 249 | return false, nil 250 | } else { 251 | // Port is open 252 | return true, nil 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /pkg/scanners/wake.go: -------------------------------------------------------------------------------- 1 | package scanners 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "time" 7 | 8 | "github.com/j-keck/arping" 9 | ) 10 | 11 | type ScannedNode struct { 12 | MacAddress string 13 | Awake bool 14 | } 15 | 16 | type WakeScanner struct { 17 | macAddress string 18 | deviceName string 19 | raddr net.IP 20 | timeout time.Duration 21 | statusChan chan *ScannedNode 22 | getIPAddress func(string) (string, error) 23 | } 24 | 25 | func NewWakeScanner(macAddress string, deviceName string, timeout time.Duration, getIPAddress func(string) (string, error)) *WakeScanner { 26 | return &WakeScanner{ 27 | macAddress, 28 | deviceName, 29 | nil, 30 | timeout, 31 | make(chan *ScannedNode), 32 | getIPAddress, 33 | } 34 | } 35 | 36 | func (w *WakeScanner) Open() error { 37 | ip, err := w.getIPAddress(w.macAddress) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | raddr := net.ParseIP(ip) 43 | if raddr == nil { 44 | return errors.New("could not parse IP") 45 | } 46 | 47 | w.raddr = raddr 48 | 49 | arping.SetTimeout(w.timeout) 50 | 51 | return nil 52 | } 53 | 54 | func (w *WakeScanner) Transmit() error { 55 | macAddress, _, err := arping.PingOverIfaceByName(w.raddr, w.deviceName) 56 | if err != nil { 57 | if err == arping.ErrTimeout { 58 | w.statusChan <- &ScannedNode{macAddress.String(), false} 59 | w.statusChan <- nil 60 | 61 | return nil 62 | } 63 | 64 | w.statusChan <- nil 65 | 66 | return err 67 | } 68 | 69 | w.statusChan <- &ScannedNode{macAddress.String(), true} 70 | w.statusChan <- nil 71 | 72 | return nil 73 | } 74 | 75 | func (w *WakeScanner) Read() *ScannedNode { 76 | status := <-w.statusChan 77 | 78 | return status 79 | } 80 | -------------------------------------------------------------------------------- /pkg/servers/liwasc.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | 7 | "github.com/pojntfx/go-app-grpc-chat-backend/pkg/websocketproxy" 8 | api "github.com/pojntfx/liwasc/pkg/api/proto/v1" 9 | "github.com/pojntfx/liwasc/pkg/services" 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/reflection" 12 | ) 13 | 14 | type LiwascServer struct { 15 | listenAddress string 16 | webSocketListenAddress string 17 | 18 | nodeAndPortScanService *services.NodeAndPortScanPortService 19 | metadataService *services.MetadataService 20 | nodeWakeService *services.NodeWakeService 21 | } 22 | 23 | func NewLiwascServer( 24 | listenAddress string, 25 | webSocketListenAddress string, 26 | 27 | nodeAndPortScanService *services.NodeAndPortScanPortService, 28 | metadataService *services.MetadataService, 29 | nodeWakeService *services.NodeWakeService, 30 | ) *LiwascServer { 31 | return &LiwascServer{ 32 | listenAddress: listenAddress, 33 | webSocketListenAddress: webSocketListenAddress, 34 | 35 | nodeAndPortScanService: nodeAndPortScanService, 36 | metadataService: metadataService, 37 | nodeWakeService: nodeWakeService, 38 | } 39 | } 40 | 41 | func (s *LiwascServer) ListenAndServe() error { 42 | listener, err := net.Listen("tcp", s.listenAddress) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | proxy := websocketproxy.NewWebSocketProxyServer(s.webSocketListenAddress) 48 | webSocketListener, err := proxy.Listen() 49 | if err != nil { 50 | return err 51 | } 52 | 53 | server := grpc.NewServer() 54 | 55 | reflection.Register(server) 56 | api.RegisterNodeAndPortScanServiceServer(server, s.nodeAndPortScanService) 57 | api.RegisterMetadataServiceServer(server, s.metadataService) 58 | api.RegisterNodeWakeServiceServer(server, s.nodeWakeService) 59 | 60 | doneChan := make(chan struct{}) 61 | errChan := make(chan error) 62 | 63 | var wg sync.WaitGroup 64 | wg.Add(2) 65 | 66 | go func() { 67 | wg.Wait() 68 | 69 | close(doneChan) 70 | }() 71 | 72 | go func() { 73 | if err := server.Serve(listener); err != nil { 74 | errChan <- err 75 | } 76 | 77 | wg.Done() 78 | }() 79 | 80 | go func() { 81 | if err := server.Serve(webSocketListener); err != nil { 82 | errChan <- err 83 | } 84 | 85 | wg.Done() 86 | }() 87 | 88 | select { 89 | case <-doneChan: 90 | return nil 91 | case err := <-errChan: 92 | return err 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pkg/services/metadata.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | //go:generate sh -c "mkdir -p ../api/proto/v1 && protoc --go_out=paths=source_relative,plugins=grpc:../api/proto/v1 -I=../../api/proto/v1 ../../api/proto/v1/*.proto" 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "log" 9 | "strconv" 10 | 11 | "github.com/golang/protobuf/ptypes/empty" 12 | api "github.com/pojntfx/liwasc/pkg/api/proto/v1" 13 | "github.com/pojntfx/liwasc/pkg/networking" 14 | "github.com/pojntfx/liwasc/pkg/persisters" 15 | "github.com/pojntfx/liwasc/pkg/validators" 16 | "google.golang.org/grpc/codes" 17 | "google.golang.org/grpc/status" 18 | ) 19 | 20 | const ( 21 | AUTHORIZATION_METADATA_KEY = "X-Liwasc-Authorization" 22 | ) 23 | 24 | type MetadataService struct { 25 | api.UnimplementedMetadataServiceServer 26 | 27 | subnets []string 28 | device string 29 | 30 | interfaceInspector *networking.InterfaceInspector 31 | 32 | mac2vendorPersister *persisters.MAC2VendorPersister 33 | serviceNamesPortNumbersPersister *persisters.ServiceNamesPortNumbersPersister 34 | 35 | contextValidator *validators.ContextValidator 36 | } 37 | 38 | func NewMetadataService( 39 | interfaceInspector *networking.InterfaceInspector, 40 | 41 | mac2vendorPersister *persisters.MAC2VendorPersister, 42 | serviceNamesPortNumbersPersister *persisters.ServiceNamesPortNumbersPersister, 43 | 44 | contextValidator *validators.ContextValidator, 45 | ) *MetadataService { 46 | return &MetadataService{ 47 | interfaceInspector: interfaceInspector, 48 | 49 | mac2vendorPersister: mac2vendorPersister, 50 | serviceNamesPortNumbersPersister: serviceNamesPortNumbersPersister, 51 | 52 | contextValidator: contextValidator, 53 | } 54 | } 55 | 56 | func (s *MetadataService) Open() error { 57 | subnets, err := s.interfaceInspector.GetIPv4Subnets() 58 | if err != nil { 59 | return err 60 | } 61 | 62 | s.subnets = subnets 63 | s.device = s.interfaceInspector.GetDevice() 64 | 65 | return nil 66 | } 67 | 68 | func (s *MetadataService) GetMetadataForScanner(ctx context.Context, _ *empty.Empty) (*api.ScannerMetadataMessage, error) { 69 | // Authorize 70 | valid, err := s.contextValidator.Validate(ctx) 71 | if err != nil || !valid { 72 | return nil, status.Errorf(codes.Unauthenticated, "could not authorize: %v", err) 73 | } 74 | 75 | protoScannerMetadataMessage := &api.ScannerMetadataMessage{ 76 | Subnets: s.subnets, 77 | Device: s.device, 78 | } 79 | 80 | return protoScannerMetadataMessage, nil 81 | } 82 | 83 | func (s *MetadataService) GetMetadataForNode(ctx context.Context, nodeMetadataReferenceMessage *api.NodeMetadataReferenceMessage) (*api.NodeMetadataMessage, error) { 84 | // Authorize 85 | valid, err := s.contextValidator.Validate(ctx) 86 | if err != nil || !valid { 87 | return nil, status.Errorf(codes.Unauthenticated, "could not authorize: %v", err) 88 | } 89 | 90 | dbNodeMetadata, err := s.mac2vendorPersister.GetVendor(nodeMetadataReferenceMessage.GetMACAddress()) 91 | if err != nil { 92 | log.Printf("could not find node %v in DB: %v\n", nodeMetadataReferenceMessage.GetMACAddress(), err) 93 | 94 | return nil, status.Errorf(codes.NotFound, "could not find node in DB") 95 | } 96 | 97 | protoNodeMetadataMessage := &api.NodeMetadataMessage{ 98 | Address: dbNodeMetadata.Address.String, 99 | MACAddress: nodeMetadataReferenceMessage.GetMACAddress(), 100 | Organization: dbNodeMetadata.Organization.String, 101 | Registry: dbNodeMetadata.Registry, 102 | Vendor: dbNodeMetadata.Vendor.String, 103 | Visible: func() bool { 104 | if dbNodeMetadata.Visibility == 1 { 105 | return true 106 | } 107 | 108 | return false 109 | }(), 110 | } 111 | 112 | return protoNodeMetadataMessage, nil 113 | } 114 | 115 | func (s *MetadataService) GetMetadataForPort(ctx context.Context, portMetadataReferenceMessage *api.PortMetadataReferenceMessage) (*api.PortMetadataMessage, error) { 116 | // Authorize 117 | valid, err := s.contextValidator.Validate(ctx) 118 | if err != nil || !valid { 119 | return nil, status.Errorf(codes.Unauthenticated, "could not authorize: %v", err) 120 | } 121 | 122 | dbPortMetadata, err := s.serviceNamesPortNumbersPersister.GetService(int(portMetadataReferenceMessage.GetPortNumber()), portMetadataReferenceMessage.GetTransportProtocol()) 123 | if err != nil || (dbPortMetadata != nil && len(dbPortMetadata) == 0) { 124 | log.Printf("could not find port %v in DB: %v\n", fmt.Sprintf("%v/%v", portMetadataReferenceMessage.GetPortNumber(), portMetadataReferenceMessage.GetTransportProtocol()), err) 125 | 126 | return nil, status.Errorf(codes.NotFound, "could not find port in DB") 127 | } 128 | 129 | portNumber, err := strconv.Atoi(dbPortMetadata[0].PortNumber) 130 | if err != nil { 131 | log.Printf("could not find valid port number for port %v in DB: %v\n", fmt.Sprintf("%v/%v", portMetadataReferenceMessage.GetPortNumber(), portMetadataReferenceMessage.GetTransportProtocol()), err) 132 | 133 | return nil, status.Errorf(codes.Unknown, "could not find valid port number in DB") 134 | } 135 | 136 | protoPortMetadataMessage := &api.PortMetadataMessage{ 137 | Assignee: dbPortMetadata[0].Assignee, 138 | AssignmentNotes: dbPortMetadata[0].AssignmentNotes, 139 | Contact: dbPortMetadata[0].Contact, 140 | Description: dbPortMetadata[0].Description, 141 | ModificationDate: dbPortMetadata[0].ModificationDate, 142 | PortNumber: int64(portNumber), 143 | Reference: dbPortMetadata[0].Reference, 144 | RegistrationDate: dbPortMetadata[0].RegistrationDate, 145 | ServiceCode: dbPortMetadata[0].ServiceCode, 146 | ServiceName: dbPortMetadata[0].ServiceName, 147 | TransportProtocol: dbPortMetadata[0].TransportProtocol, 148 | UnauthorizedUseReported: dbPortMetadata[0].UnauthorizedUseReported, 149 | } 150 | 151 | return protoPortMetadataMessage, nil 152 | } 153 | -------------------------------------------------------------------------------- /pkg/services/node_wake.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/golang/protobuf/ptypes/empty" 11 | api "github.com/pojntfx/liwasc/pkg/api/proto/v1" 12 | models "github.com/pojntfx/liwasc/pkg/db/sqlite/models/node_wake" 13 | "github.com/pojntfx/liwasc/pkg/persisters" 14 | "github.com/pojntfx/liwasc/pkg/scanners" 15 | "github.com/pojntfx/liwasc/pkg/validators" 16 | "github.com/pojntfx/liwasc/pkg/wakers" 17 | "github.com/ugjka/messenger" 18 | "google.golang.org/grpc/codes" 19 | "google.golang.org/grpc/status" 20 | ) 21 | 22 | type NodeWakeService struct { 23 | api.UnimplementedNodeWakeServiceServer 24 | 25 | device string 26 | wakeOnLANWaker *wakers.WakeOnLANWaker 27 | 28 | nodeWakePersister *persisters.NodeWakePersister 29 | getIPAddress func(macAddress string) (ipAddress string, err error) 30 | 31 | nodeWakeMessenger *messenger.Messenger 32 | 33 | contextValidator *validators.ContextValidator 34 | } 35 | 36 | func NewNodeWakeService( 37 | device string, 38 | wakeOnLANWaker *wakers.WakeOnLANWaker, 39 | 40 | nodeWakePersister *persisters.NodeWakePersister, 41 | getIPAddress func(macAddress string) (ipAddress string, err error), 42 | 43 | contextValidator *validators.ContextValidator, 44 | ) *NodeWakeService { 45 | return &NodeWakeService{ 46 | device: device, 47 | wakeOnLANWaker: wakeOnLANWaker, 48 | 49 | nodeWakePersister: nodeWakePersister, 50 | getIPAddress: getIPAddress, 51 | 52 | nodeWakeMessenger: messenger.New(0, true), 53 | 54 | contextValidator: contextValidator, 55 | } 56 | } 57 | 58 | func (s *NodeWakeService) StartNodeWake(ctx context.Context, nodeWakeStartMessage *api.NodeWakeStartMessage) (*api.NodeWakeMessage, error) { 59 | // Authorize 60 | valid, err := s.contextValidator.Validate(ctx) 61 | if err != nil || !valid { 62 | return nil, status.Errorf(codes.Unauthenticated, "could not authorize: %v", err) 63 | } 64 | 65 | // Validate 66 | if nodeWakeStartMessage.GetNodeWakeTimeout() < 1 { 67 | return nil, status.Error(codes.InvalidArgument, "node wake timeout can't be lower than 1") 68 | } 69 | 70 | // Create and broadcast node wake in DB 71 | dbNodeWake := &models.NodeWake{ 72 | Done: 0, 73 | PoweredOn: 0, 74 | MacAddress: nodeWakeStartMessage.GetMACAddress(), 75 | } 76 | if err := s.nodeWakePersister.CreateNodeWake(dbNodeWake); err != nil { 77 | log.Printf("could not create node wake in DB: %v\n", err) 78 | 79 | return nil, status.Errorf(codes.Unknown, "could not create node wake in DB") 80 | } 81 | s.nodeWakeMessenger.Broadcast(dbNodeWake) 82 | 83 | // Wake the node 84 | if err := s.wakeOnLANWaker.Write(dbNodeWake.MacAddress); err != nil { 85 | log.Printf("could not wake node: %v\n", err) 86 | 87 | return nil, status.Errorf(codes.Unknown, "could not wake node") 88 | } 89 | 90 | successfulFirstOpen := make(chan error) 91 | 92 | // Transmit and receive node wakes 93 | go func() { 94 | for i := 0; i < 5; i++ { 95 | timeout := time.Millisecond * time.Duration(nodeWakeStartMessage.GetNodeWakeTimeout()/5) 96 | 97 | // Create and open wake scanner 98 | wakeScanner := scanners.NewWakeScanner( 99 | dbNodeWake.MacAddress, 100 | s.device, 101 | timeout, 102 | s.getIPAddress, 103 | ) 104 | if err := wakeScanner.Open(); err != nil { 105 | log.Printf("could not open wake scanner: %v\n", err) 106 | 107 | // Send first error message to client 108 | if i == 0 { 109 | successfulFirstOpen <- err 110 | } 111 | 112 | return 113 | } 114 | 115 | // Send first error message to client 116 | if i == 0 { 117 | successfulFirstOpen <- nil 118 | } 119 | 120 | go func() { 121 | if err := wakeScanner.Transmit(); err != nil { 122 | log.Printf("could not transmit from wake scanner: %v\n", err) 123 | 124 | return 125 | } 126 | }() 127 | 128 | for { 129 | node := wakeScanner.Read() 130 | 131 | // Update and broadcast node wake in DB 132 | if node != nil && node.Awake { 133 | dbNodeWake.PoweredOn = 1 134 | dbNodeWake.Done = 1 135 | } else { 136 | dbNodeWake.PoweredOn = 0 137 | 138 | // Wake scan is done 139 | if node == nil { 140 | dbNodeWake.Done = 1 141 | } 142 | } 143 | if err := s.nodeWakePersister.UpdateNodeWake(dbNodeWake); err != nil { 144 | log.Printf("could not update node wake in DB: %v\n", err) 145 | 146 | return 147 | } 148 | s.nodeWakeMessenger.Broadcast(dbNodeWake) 149 | 150 | if dbNodeWake.Done == 1 { 151 | break 152 | } 153 | } 154 | 155 | if dbNodeWake.Done == 1 { 156 | break 157 | } 158 | } 159 | }() 160 | 161 | err = <-successfulFirstOpen 162 | if err != nil { 163 | if strings.Contains(err.Error(), "sql: no rows in result set") { 164 | return nil, status.Errorf(codes.NotFound, "could not find node to wake. Did you run a network scan yet?") 165 | } 166 | 167 | return nil, status.Errorf(codes.Unknown, "could not wake node") 168 | } 169 | 170 | protoNodeWake := &api.NodeWakeMessage{ 171 | CreatedAt: dbNodeWake.CreatedAt.Format(time.RFC3339), 172 | Done: func() bool { 173 | if dbNodeWake.Done == 1 { 174 | return true 175 | } 176 | 177 | return false 178 | }(), 179 | ID: dbNodeWake.ID, 180 | MACAddress: dbNodeWake.MacAddress, 181 | PoweredOn: func() bool { 182 | if dbNodeWake.PoweredOn == 1 { 183 | return true 184 | } 185 | 186 | return false 187 | }(), 188 | } 189 | 190 | return protoNodeWake, nil 191 | } 192 | 193 | func (s *NodeWakeService) SubscribeToNodeWakes(_ *empty.Empty, stream api.NodeWakeService_SubscribeToNodeWakesServer) error { 194 | // Authorize 195 | valid, err := s.contextValidator.Validate(stream.Context()) 196 | if err != nil || !valid { 197 | return status.Errorf(codes.Unauthenticated, "could not authorize: %v", err) 198 | } 199 | 200 | var wg sync.WaitGroup 201 | 202 | wg.Add(2) 203 | 204 | // Get node wakes from messenger (priority 2) 205 | go func() { 206 | dbNodeWakes, err := s.nodeWakeMessenger.Sub() 207 | if err != nil { 208 | log.Printf("could not get node wakes from messenger: %v\n", err) 209 | 210 | return 211 | } 212 | defer s.nodeWakeMessenger.Unsub(dbNodeWakes) 213 | 214 | for dbNodeWake := range dbNodeWakes { 215 | protoNodeWake := &api.NodeWakeMessage{ 216 | CreatedAt: dbNodeWake.(*models.NodeWake).CreatedAt.Format(time.RFC3339), 217 | Done: func() bool { 218 | if dbNodeWake.(*models.NodeWake).Done == 1 { 219 | return true 220 | } 221 | 222 | return false 223 | }(), 224 | ID: dbNodeWake.(*models.NodeWake).ID, 225 | MACAddress: dbNodeWake.(*models.NodeWake).MacAddress, 226 | PoweredOn: func() bool { 227 | if dbNodeWake.(*models.NodeWake).PoweredOn == 1 { 228 | return true 229 | } 230 | 231 | return false 232 | }(), 233 | Priority: 2, 234 | } 235 | 236 | if err := stream.Send(protoNodeWake); err != nil { 237 | log.Printf("could send node wake %v to client: %v\n", protoNodeWake.ID, err) 238 | 239 | return 240 | } 241 | } 242 | 243 | wg.Done() 244 | }() 245 | 246 | // Get lookback node wakes from persister (priority 1) 247 | go func() { 248 | dbNodeWakes, err := s.nodeWakePersister.GetNodeWakes() 249 | if err != nil { 250 | log.Printf("could not get node wakes from DB: %v\n", err) 251 | 252 | return 253 | } 254 | 255 | for _, dbNodeWake := range dbNodeWakes { 256 | protoNodeWake := &api.NodeWakeMessage{ 257 | CreatedAt: dbNodeWake.CreatedAt.Format(time.RFC3339), 258 | Done: func() bool { 259 | if dbNodeWake.Done == 1 { 260 | return true 261 | } 262 | 263 | return false 264 | }(), 265 | ID: dbNodeWake.ID, 266 | MACAddress: dbNodeWake.MacAddress, 267 | PoweredOn: func() bool { 268 | if dbNodeWake.PoweredOn == 1 { 269 | return true 270 | } 271 | 272 | return false 273 | }(), 274 | Priority: 1, 275 | } 276 | 277 | if err := stream.Send(protoNodeWake); err != nil { 278 | log.Printf("could send node wake %v to client: %v\n", protoNodeWake.ID, err) 279 | 280 | return 281 | } 282 | } 283 | 284 | wg.Done() 285 | }() 286 | 287 | wg.Wait() 288 | 289 | return nil 290 | } 291 | -------------------------------------------------------------------------------- /pkg/validators/context.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "google.golang.org/grpc/metadata" 9 | ) 10 | 11 | type ContextValidator struct { 12 | metadataKey string 13 | oidcValidator *OIDCValidator 14 | } 15 | 16 | func NewContextValidator(metadataKey string, oidcValidator *OIDCValidator) *ContextValidator { 17 | return &ContextValidator{ 18 | metadataKey: metadataKey, 19 | oidcValidator: oidcValidator, 20 | } 21 | } 22 | 23 | func (v *ContextValidator) Validate(ctx context.Context) (bool, error) { 24 | md, ok := metadata.FromIncomingContext(ctx) 25 | if !ok { 26 | return false, errors.New("could not parse metadata") 27 | } 28 | 29 | token := md.Get(v.metadataKey) 30 | if len(token) <= 0 { 31 | return false, errors.New("could not parse metadata") 32 | } 33 | 34 | idToken, err := v.oidcValidator.Validate(token[0]) 35 | if err != nil || idToken == nil { 36 | return false, fmt.Errorf("invalid token: %v", err) 37 | } 38 | 39 | return true, nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/validators/oidc.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/coreos/go-oidc/v3/oidc" 7 | ) 8 | 9 | type OIDCValidator struct { 10 | Issuer string 11 | ClientID string 12 | 13 | verifier *oidc.IDTokenVerifier 14 | } 15 | 16 | func NewOIDCValidator(issuer string, clientID string) *OIDCValidator { 17 | return &OIDCValidator{ClientID: clientID, Issuer: issuer} 18 | } 19 | 20 | func (v *OIDCValidator) Open() error { 21 | provider, err := oidc.NewProvider(context.Background(), v.Issuer) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | v.verifier = provider.Verifier(&oidc.Config{ClientID: v.ClientID}) 27 | 28 | return nil 29 | } 30 | 31 | func (v *OIDCValidator) Validate(token string) (*oidc.IDToken, error) { 32 | idToken, err := v.verifier.Verify(context.Background(), token) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return idToken, nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/wakers/wake_on_lan.go: -------------------------------------------------------------------------------- 1 | package wakers 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/mdlayher/wol" 7 | ) 8 | 9 | type WakeOnLANWaker struct { 10 | deviceName string 11 | wolClient *wol.RawClient 12 | } 13 | 14 | func NewWakeOnLANWaker(deviceName string) *WakeOnLANWaker { 15 | return &WakeOnLANWaker{deviceName, nil} 16 | } 17 | 18 | func (w *WakeOnLANWaker) Open() error { 19 | iface, err := net.InterfaceByName(w.deviceName) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | wolClient, err := wol.NewRawClient(iface) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | w.wolClient = wolClient 30 | 31 | return nil 32 | } 33 | 34 | func (w *WakeOnLANWaker) Write(targetMACAddress string) error { 35 | target, err := net.ParseMAC(targetMACAddress) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | return w.wolClient.Wake(target) 41 | } 42 | -------------------------------------------------------------------------------- /web/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pojntfx/liwasc/74da4132791f9ff392439530a7a42e07100271d9/web/icon.png -------------------------------------------------------------------------------- /web/index.css: -------------------------------------------------------------------------------- 1 | .pf-x-c-brand--main { 2 | max-width: 12.5rem; 3 | } 4 | 5 | .pf-x-c-brand--nav { 6 | height: 50px; 7 | } 8 | 9 | .pf-x-ws-router { 10 | height: 100vh; 11 | } 12 | 13 | .pf-x-c-tooltip { 14 | opacity: 0; 15 | width: 10rem; 16 | bottom: 50%; 17 | transform: translateY(50%); 18 | left: calc(100% + 2rem); 19 | position: absolute; 20 | z-index: 1; 21 | pointer-events: none; 22 | } 23 | 24 | .pf-x-c-tooltip--bottom { 25 | transform: translateY(calc(100% + 2rem)); 26 | left: -50%; 27 | } 28 | 29 | .pf-x-c-tooltip-wrapper:hover > .pf-x-c-tooltip { 30 | opacity: 1; 31 | } 32 | 33 | /* Prevent unnecessary horizontal scrolling in nested drawers */ 34 | .pf-x-m-overflow-x-hidden { 35 | overflow-x: hidden; 36 | } 37 | 38 | .pf-x-m-overflow-x-auto { 39 | overflow-x: auto; 40 | } 41 | 42 | /* Remove top border from table without toolbar */ 43 | .pf-x-u-border-t-0 { 44 | border-top: 0 !important; 45 | } 46 | 47 | .pf-x-c-power-switch { 48 | display: flex; 49 | justify-content: center; 50 | align-items: center; 51 | } 52 | --------------------------------------------------------------------------------