├── .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(``),
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 |
--------------------------------------------------------------------------------