├── .github
└── workflows
│ ├── docker.yaml
│ └── hydrun.yaml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── Hydrunfile
├── LICENSE
├── Makefile
├── README.md
├── api
└── proto
│ └── v1
│ ├── events.proto
│ └── metadata.proto
├── cmd
├── bofied-backend
│ └── main.go
└── bofied-frontend
│ └── main.go
├── docs
├── demo.mp4
├── icon.svg
├── logo.svg
├── screenshot-about-modal.png
├── screenshot-file-operations-2.png
├── screenshot-file-operations-3.png
├── screenshot-gnome-files-webdav-listing.png
├── screenshot-gnome-files-webdav-mounting.png
├── screenshot-initial.png
├── screenshot-monitoring.png
├── screenshot-mount-directory.png
├── screenshot-setup.png
├── screenshot-sharing.png
├── screenshot-syntax-validation.png
└── screenshot-text-editor.png
├── examples
└── bofied-backend-config.yaml
├── go.mod
├── go.sum
├── pkg
├── api
│ └── proto
│ │ └── v1
│ │ ├── events.pb.go
│ │ ├── events_grpc.pb.go
│ │ ├── metadata.pb.go
│ │ └── metadata_grpc.pb.go
├── authorization
│ └── oidc_over_basic_auth.go
├── components
│ ├── about_modal.go
│ ├── autofocused.go
│ ├── breadcrumbs.go
│ ├── copyable_input.go
│ ├── data_shell.go
│ ├── empty_state.go
│ ├── expandable_section.go
│ ├── file_explorer.go
│ ├── file_grid.go
│ ├── form_group.go
│ ├── home.go
│ ├── modal.go
│ ├── navbar.go
│ ├── notification_drawer.go
│ ├── path_picker_toolbar.go
│ ├── setup_form.go
│ ├── setup_shell.go
│ ├── status.go
│ ├── switch.go
│ ├── text_editor.go
│ ├── text_editor_wrapper.go
│ └── update_notification.go
├── config
│ ├── architecture_types.go
│ └── yaegi.go
├── constants
│ ├── authorization.go
│ └── config.go
├── eventing
│ ├── event.go
│ └── http_logging.go
├── providers
│ ├── data_provider.go
│ ├── identity_provider.go
│ └── setup_provider.go
├── servers
│ ├── dhcp.go
│ ├── extended_http.go
│ ├── file_server.go
│ ├── grpc.go
│ ├── proxy_dhcp.go
│ ├── tftp.go
│ └── udp_server.go
├── services
│ ├── events.go
│ └── metadata.go
├── transcoding
│ ├── dhcp.go
│ ├── proxy_dhcp.go
│ └── pxe_class_identifier.go
├── utils
│ └── ip.go
├── validators
│ ├── check_syntax.go
│ ├── context.go
│ ├── format.go
│ └── oidc.go
└── websocketproxy
│ ├── client.go
│ └── server.go
└── web
├── icon.png
├── index.css
├── logo-dark.png
└── logo-light.png
/.github/workflows/docker.yaml:
--------------------------------------------------------------------------------
1 | name: Docker CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: "0 0 * * 0"
8 |
9 | jobs:
10 | build-oci-images:
11 | runs-on: ${{ matrix.target.runner }}
12 | permissions:
13 | contents: read
14 | packages: write
15 | id-token: write
16 | strategy:
17 | matrix:
18 | target:
19 | - id: bofied-backend-linux-amd64
20 | src: .
21 | file: Dockerfile
22 | image: ghcr.io/pojntfx/bofied-backend
23 | arch: "linux/amd64,linux/arm/v7,linux/386,linux/s390x" # linux/mips64le,linux/ppc64le,linux/arm/v5
24 | runner: ubuntu-latest
25 | - id: bofied-backend-linux-arm64-v8
26 | src: .
27 | file: Dockerfile
28 | image: ghcr.io/pojntfx/bofied-backend
29 | arch: "linux/arm64/v8"
30 | runner: ubicloud-standard-4-arm
31 |
32 | steps:
33 | - name: Checkout
34 | uses: actions/checkout@v4
35 | - name: Set up QEMU
36 | uses: docker/setup-qemu-action@v3
37 | - name: Set up Docker Buildx
38 | uses: docker/setup-buildx-action@v3
39 | - name: Login to registry
40 | uses: docker/login-action@v3
41 | with:
42 | registry: ghcr.io
43 | username: ${{ github.actor }}
44 | password: ${{ secrets.GITHUB_TOKEN }}
45 | - name: Set up metadata
46 | id: meta
47 | uses: docker/metadata-action@v5
48 | with:
49 | images: ${{ matrix.target.image }}
50 | - name: Build and push image by digest to registry
51 | id: build
52 | uses: docker/build-push-action@v5
53 | with:
54 | context: ${{ matrix.target.src }}
55 | file: ${{ matrix.target.src }}/${{ matrix.target.file }}
56 | platforms: ${{ matrix.target.arch }}
57 | labels: ${{ steps.meta.outputs.labels }}
58 | outputs: type=image,name=${{ matrix.target.image }},push-by-digest=true,name-canonical=true,push=true
59 | cache-from: type=gha
60 | cache-to: type=gha,mode=max
61 | - name: Export digest
62 | run: |
63 | mkdir -p "/tmp/digests"
64 | export DIGEST="${{ steps.build.outputs.digest }}"
65 | touch "/tmp/digests/${DIGEST#sha256:}"
66 | - name: Upload digest
67 | uses: actions/upload-artifact@v4
68 | with:
69 | name: digests-${{ matrix.target.id }}
70 | path: /tmp/digests/*
71 | if-no-files-found: error
72 | retention-days: 1
73 |
74 | merge-oci-images:
75 | runs-on: ubuntu-latest
76 | permissions:
77 | contents: read
78 | packages: write
79 | id-token: write
80 | needs: build-oci-images
81 | strategy:
82 | matrix:
83 | target:
84 | - idprefix: bofied-backend-linux-
85 | image: ghcr.io/pojntfx/bofied-backend
86 |
87 | steps:
88 | - name: Checkout
89 | uses: actions/checkout@v4
90 | - name: Set up QEMU
91 | uses: docker/setup-qemu-action@v3
92 | - name: Set up Docker Buildx
93 | uses: docker/setup-buildx-action@v3
94 | - name: Login to registry
95 | uses: docker/login-action@v3
96 | with:
97 | registry: ghcr.io
98 | username: ${{ github.actor }}
99 | password: ${{ secrets.GITHUB_TOKEN }}
100 | - name: Set up metadata
101 | id: meta
102 | uses: docker/metadata-action@v5
103 | with:
104 | images: ${{ matrix.target.image }}
105 | tags: type=semver,pattern={{version}}
106 | - name: Download digests
107 | uses: actions/download-artifact@v4
108 | with:
109 | path: /tmp/digests
110 | pattern: digests-${{ matrix.target.idprefix }}*
111 | merge-multiple: true
112 | - name: Create pre-release manifest list and push to registry
113 | working-directory: /tmp/digests
114 | run: |
115 | docker buildx imagetools create --tag "${{ matrix.target.image }}:${{ github.ref_name }}" $(printf '${{ matrix.target.image }}@sha256:%s ' *)
116 | - name: Create release manifest list and push to registry
117 | if: startsWith(github.ref, 'refs/tags/v')
118 | working-directory: /tmp/digests
119 | run: |
120 | TAGS=$(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ' ')
121 | for TAG in $TAGS; do
122 | docker buildx imagetools create --tag "$TAG" $(printf '${{ matrix.target.image }}@sha256:%s ' *);
123 | done
124 |
--------------------------------------------------------------------------------
/.github/workflows/hydrun.yaml:
--------------------------------------------------------------------------------
1 | name: hydrun CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: "0 0 * * 0"
8 |
9 | jobs:
10 | build-linux:
11 | runs-on: ${{ matrix.target.runner }}
12 | permissions:
13 | contents: read
14 | strategy:
15 | matrix:
16 | target:
17 | # Tests
18 | - id: test-cli
19 | src: .
20 | os: golang:bookworm
21 | flags: -e '-v /tmp/ccache:/root/.cache/go-build'
22 | cmd: GOFLAGS="-short" ./Hydrunfile test/cli
23 | dst: out/nonexistent
24 | runner: ubuntu-latest
25 | - id: test-pwa
26 | src: .
27 | os: golang:bookworm
28 | flags: -e '-v /tmp/ccache:/root/.cache/go-build'
29 | cmd: GOFLAGS="-short" ./Hydrunfile test/pwa
30 | dst: out/nonexistent
31 | runner: ubuntu-latest
32 |
33 | # Binaries
34 | - id: go.bofied-backend
35 | src: .
36 | os: golang:bookworm
37 | flags: -e '-v /tmp/ccache:/root/.cache/go-build'
38 | cmd: ./Hydrunfile build/cli bofied-backend
39 | dst: out/*
40 | runner: ubuntu-latest
41 |
42 | # PWAs
43 | - id: pwa.bofied
44 | src: .
45 | os: golang:bookworm
46 | flags: -e '-v /tmp/ccache:/root/.cache/go-build'
47 | cmd: ./Hydrunfile build/pwa
48 | dst: out/*
49 | runner: ubuntu-latest
50 | - id: pwa.bofied-github-pages
51 | src: .
52 | os: golang:bookworm
53 | flags: -e '-v /tmp/ccache:/root/.cache/go-build'
54 | cmd: ./Hydrunfile build/pwa-github-pages && mv out/frontend.tar.gz out/frontend-github-pages.tar.gz
55 | dst: out/*
56 | runner: ubuntu-latest
57 |
58 | steps:
59 | - name: Checkout
60 | uses: actions/checkout@v4
61 | - name: Restore ccache
62 | uses: actions/cache/restore@v4
63 | with:
64 | path: |
65 | /tmp/ccache
66 | key: cache-ccache-${{ matrix.target.id }}
67 | - name: Set up QEMU
68 | uses: docker/setup-qemu-action@v3
69 | - name: Set up Docker Buildx
70 | uses: docker/setup-buildx-action@v3
71 | - name: Set up hydrun
72 | run: |
73 | curl -L -o /tmp/hydrun "https://github.com/pojntfx/hydrun/releases/latest/download/hydrun.linux-$(uname -m)"
74 | sudo install /tmp/hydrun /usr/local/bin
75 | - name: Build with hydrun
76 | working-directory: ${{ matrix.target.src }}
77 | run: hydrun -o ${{ matrix.target.os }} ${{ matrix.target.flags }} "${{ matrix.target.cmd }}"
78 | - name: Fix permissions for output
79 | run: sudo chown -R $USER .
80 | - name: Save ccache
81 | uses: actions/cache/save@v4
82 | with:
83 | path: |
84 | /tmp/ccache
85 | key: cache-ccache-${{ matrix.target.id }}
86 | - name: Upload output
87 | uses: actions/upload-artifact@v4
88 | with:
89 | name: ${{ matrix.target.id }}
90 | path: ${{ matrix.target.dst }}
91 |
92 | publish-linux:
93 | runs-on: ubuntu-latest
94 | permissions:
95 | contents: write
96 | pages: write
97 | id-token: write
98 | needs: build-linux
99 | environment:
100 | name: github-pages
101 | url: ${{ steps.publish.outputs.page_url }}
102 |
103 | steps:
104 | - name: Checkout
105 | uses: actions/checkout@v4
106 | - name: Download output
107 | uses: actions/download-artifact@v4
108 | with:
109 | path: /tmp/out
110 | - name: Isolate the frontend for GitHub pages
111 | run: |
112 | mkdir -p /tmp/github-pages
113 | tar -xzvf /tmp/out/pwa.bofied-github-pages/frontend-github-pages.tar.gz -C /tmp/github-pages
114 | touch /tmp/github-pages/.nojekyll
115 | - name: Extract branch name
116 | id: extract_branch
117 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
118 | - name: Publish pre-release to GitHub releases
119 | if: ${{ github.ref == 'refs/heads/main' }}
120 | uses: softprops/action-gh-release@v2
121 | with:
122 | tag_name: release-${{ steps.extract_branch.outputs.branch }}
123 | prerelease: true
124 | files: |
125 | /tmp/out/*/*
126 | - name: Publish release to GitHub releases
127 | if: startsWith(github.ref, 'refs/tags/v')
128 | uses: softprops/action-gh-release@v2
129 | with:
130 | prerelease: false
131 | files: |
132 | /tmp/out/*/*
133 | - name: Setup GitHub Pages
134 | uses: actions/configure-pages@v5
135 | - name: Upload GitHub Pages artifact
136 | uses: actions/upload-pages-artifact@v3
137 | with:
138 | path: /tmp/github-pages/
139 | - name: Publish to GitHub pages
140 | # if: startsWith(github.ref, 'refs/tags/v')
141 | if: ${{ github.ref == 'refs/heads/main' }}
142 | id: publish
143 | uses: actions/deploy-pages@v4
144 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | web/*.wasm
2 | out
3 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | felicitas@pojtinger.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build container
2 | FROM golang:bookworm AS build
3 |
4 | # Setup environment
5 | RUN mkdir -p /data
6 | WORKDIR /data
7 |
8 | # Install native dependencies
9 | RUN apt update
10 | RUN apt install -y protobuf-compiler
11 |
12 | # Build the release
13 | COPY . .
14 | RUN make depend
15 | RUN make build/cli
16 |
17 | # Extract the release
18 | RUN mkdir -p /out
19 | RUN cp out/bofied-backend /out/bofied-backend
20 |
21 | # Release container
22 | FROM debian:bookworm
23 |
24 | # Add certificates
25 | RUN apt update
26 | RUN apt install -y ca-certificates
27 |
28 | # Add the release
29 | COPY --from=build /out/bofied-backend /usr/local/bin/bofied-backend
30 |
31 | CMD /usr/local/bin/bofied-backend
32 |
--------------------------------------------------------------------------------
/Hydrunfile:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | # Test CLI
6 | if [ "$1" = "test/cli" ]; then
7 | # Install native dependencies
8 | apt update
9 | apt install -y make git protobuf-compiler
10 |
11 | # Configure Git
12 | git config --global --add safe.directory '*'
13 |
14 | # Generate dependencies
15 | make depend/cli
16 |
17 | # Run tests
18 | make test/cli
19 |
20 | exit 0
21 | fi
22 |
23 | # Test PWA
24 | if [ "$1" = "test/pwa" ]; then
25 | # Install native dependencies
26 | apt update
27 | apt install -y make git protobuf-compiler
28 |
29 | # Configure Git
30 | git config --global --add safe.directory '*'
31 |
32 | # Generate dependencies
33 | make depend/pwa
34 |
35 | # Run tests
36 | make test/pwa
37 |
38 | exit 0
39 | fi
40 |
41 | # Build CLI
42 | if [ "$1" = "build/cli" ]; then
43 | # Install native dependencies
44 | apt update
45 | apt install -y curl make git protobuf-compiler
46 |
47 | # Install bagop
48 | curl -L -o /tmp/bagop "https://github.com/pojntfx/bagop/releases/latest/download/bagop.linux-$(uname -m)"
49 | install /tmp/bagop /usr/local/bin
50 |
51 | # Configure Git
52 | git config --global --add safe.directory '*'
53 |
54 | # Generate dependencies
55 | make depend/cli
56 |
57 | # Build
58 | CGO_ENABLED=0 bagop -j "$(nproc)" -b "$2" -x '(android/*|ios/*|plan9/*|aix/*|linux/loong64|freebsd/riscv64|wasip1/wasm)' -p "make build/cli/$2 DST=\$DST" -d out
59 |
60 | exit 0
61 | fi
62 |
63 | # Build PWA
64 | if [ "$1" = "build/pwa" ]; then
65 | # Install native dependencies
66 | apt update
67 | apt install -y make git protobuf-compiler
68 |
69 | # Configure Git
70 | git config --global --add safe.directory '*'
71 |
72 | # Generate dependencies
73 | make depend/pwa
74 |
75 | # Build
76 | make build/pwa
77 |
78 | exit 0
79 | fi
80 |
81 | # Build PWA (GitHub pages)
82 | if [ "$1" = "build/pwa-github-pages" ]; then
83 | # Install native dependencies
84 | apt update
85 | apt install -y make git protobuf-compiler
86 |
87 | # Configure Git
88 | git config --global --add safe.directory '*'
89 |
90 | # Generate dependencies
91 | make depend/pwa
92 |
93 | # Build
94 | make build/pwa-github-pages
95 |
96 | exit 0
97 | fi
98 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Public variables
2 | DESTDIR ?=
3 | PREFIX ?= /usr/local
4 | OUTPUT_DIR ?= out
5 | DST ?=
6 |
7 | WWWROOT ?= /var/www/html
8 | WWWPREFIX ?= /bofied
9 |
10 | # Private variables
11 | clis = bofied-backend
12 | pwas = frontend
13 | all: build
14 |
15 | # Build
16 | build: build/cli build/pwa
17 |
18 | build/cli: $(addprefix build/cli/,$(clis))
19 | $(addprefix build/cli/,$(clis)):
20 | ifdef DST
21 | go build -o $(DST) ./cmd/$(subst build/cli/,,$@)
22 | else
23 | go build -o $(OUTPUT_DIR)/$(subst build/cli/,,$@) ./cmd/$(subst build/cli/,,$@)
24 | endif
25 |
26 | build/pwa:
27 | GOOS=js GOARCH=wasm go build -o web/app.wasm cmd/bofied-frontend/main.go
28 | rm -rf $(OUTPUT_DIR)/bofied-frontend/
29 | mkdir -p $(OUTPUT_DIR)/bofied-frontend/web
30 | go run ./cmd/bofied-frontend/main.go --build
31 | cp -r web/* $(OUTPUT_DIR)/bofied-frontend/web
32 | tar -cvzf $(OUTPUT_DIR)/frontend.tar.gz -C $(OUTPUT_DIR)/bofied-frontend .
33 |
34 | # Special target for GitHub pages builds
35 | build/pwa-github-pages:
36 | GOOS=js GOARCH=wasm go build -o web/app.wasm cmd/bofied-frontend/main.go
37 | rm -rf $(OUTPUT_DIR)/bofied-frontend/
38 | mkdir -p $(OUTPUT_DIR)/bofied-frontend/web
39 | go run ./cmd/bofied-frontend/main.go --build --path bofied
40 | cp -r web/* $(OUTPUT_DIR)/bofied-frontend/web
41 | tar -cvzf $(OUTPUT_DIR)/frontend.tar.gz -C $(OUTPUT_DIR)/bofied-frontend .
42 |
43 | # Install
44 | install: install/cli install/pwa
45 |
46 | install/cli: $(addprefix install/cli/,$(clis))
47 | $(addprefix install/cli/,$(clis)):
48 | install -D -m 0755 $(OUTPUT_DIR)/$(subst install/cli/,,$@) $(DESTDIR)$(PREFIX)/bin/$(subst install/cli/,,$@)
49 |
50 | install/pwa:
51 | mkdir -p $(DESTDIR)$(WWWROOT)$(WWWPREFIX)
52 | tar -xvf out/frontend.tar.gz -C $(DESTDIR)$(WWWROOT)$(WWWPREFIX)
53 |
54 | # Uninstall
55 | uninstall: uninstall/cli uninstall/pwa
56 |
57 | uninstall/cli: $(addprefix uninstall/cli/,$(clis))
58 | $(addprefix uninstall/cli/,$(clis)):
59 | rm $(DESTDIR)$(PREFIX)/bin/$(subst uninstall/cli/,,$@)
60 |
61 | uninstall/pwa:
62 | rm -rf $(DESTDIR)$(WWWROOT)$(WWWPREFIX)
63 |
64 | # Run
65 | run: run/cli run/pwa
66 |
67 | run/cli: $(addprefix run/cli/,$(clis))
68 | $(addprefix run/cli/,$(clis)):
69 | $(subst run/cli/,,$@) $(ARGS)
70 |
71 | run/pwa:
72 | go run ./cmd/bofied-frontend/ --serve
73 |
74 | # Dev
75 | dev: dev/cli dev/pwa
76 |
77 | dev/cli: $(addprefix dev/cli/,$(clis))
78 | $(addprefix dev/cli/,$(clis)): $(addprefix build/cli/,$(clis))
79 | sudo setcap cap_net_bind_service+ep $(OUTPUT_DIR)/$(subst dev/cli/,,$@)
80 | $(OUTPUT_DIR)/$(subst dev/cli/,,$@) $(ARGS)
81 |
82 | dev/pwa: build/pwa
83 | go run ./cmd/bofied-frontend/ --serve
84 |
85 | # Test
86 | test: test/cli test/pwa
87 |
88 | test/cli:
89 | go test -timeout 3600s -parallel $(shell nproc) ./...
90 |
91 | test/pwa:
92 | true
93 |
94 | # Benchmark
95 | benchmark: benchmark/cli benchmark/pwa
96 |
97 | benchmark/cli:
98 | go test -timeout 3600s -bench=./... ./...
99 |
100 | benchmark/pwa:
101 | true
102 |
103 | # Clean
104 | clean: clean/cli clean/pwa
105 |
106 | clean/cli:
107 | rm -rf out pkg/models pkg/api/proto/v1 rm -rf ~/.local/share/bofied
108 |
109 | clean/pwa:
110 | rm -rf out web/app.wasm
111 |
112 | # Dependencies
113 | depend: depend/cli depend/pwa
114 |
115 | depend/cli:
116 | GO111MODULE=on go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
117 | GO111MODULE=on go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
118 | GO111MODULE=on go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
119 |
120 | go generate ./...
121 |
122 | depend/pwa:
123 | true
124 |
--------------------------------------------------------------------------------
/api/proto/v1/events.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.pojtinger.felicitas.bofied;
4 |
5 | option go_package = "github.com/pojntfx/bofied/pkg/api/proto/v1";
6 |
7 | import "metadata.proto";
8 |
9 | service EventsService {
10 | rpc SubscribeToEvents(Empty) returns (stream EventMessage);
11 | }
12 |
13 | message EventMessage {
14 | string CreatedAt = 1;
15 | string Message = 2;
16 | }
17 |
--------------------------------------------------------------------------------
/api/proto/v1/metadata.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.pojtinger.felicitas.bofied;
4 |
5 | option go_package = "github.com/pojntfx/bofied/pkg/api/proto/v1";
6 |
7 | message Empty {}
8 |
9 | service MetadataService { rpc GetMetadata(Empty) returns (MetadataMessage); }
10 |
11 | message MetadataMessage {
12 | string AdvertisedIP = 1;
13 | int32 TFTPPort = 2;
14 | int32 HTTPPort = 3;
15 | }
16 |
--------------------------------------------------------------------------------
/cmd/bofied-backend/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net"
6 | "os"
7 | "path/filepath"
8 | "strconv"
9 |
10 | "github.com/pojntfx/bofied/pkg/config"
11 | "github.com/pojntfx/bofied/pkg/constants"
12 | "github.com/pojntfx/bofied/pkg/eventing"
13 | "github.com/pojntfx/bofied/pkg/servers"
14 | "github.com/pojntfx/bofied/pkg/services"
15 | "github.com/pojntfx/bofied/pkg/validators"
16 | "github.com/spf13/cobra"
17 | "github.com/spf13/viper"
18 | )
19 |
20 | const (
21 | configFileKey = "configFile"
22 | workingDirKey = "workingDir"
23 | advertisedIPKey = "advertisedIP"
24 | dhcpListenAddressKey = "dhcpListenAddress"
25 | proxyDHCPListenAddressKey = "proxyDHCPListenAddress"
26 | tftpListenAddressKey = "tftpListenAddress"
27 | webDAVAndHTTPListenAddressKey = "extendedHTTPListenAddress"
28 | oidcIssuerKey = "oidcIssuer"
29 | oidcClientIDKey = "oidcClientID"
30 | grpcListenAddressKey = "grpcListenAddress"
31 | pureConfigKey = "pureConfig"
32 | starterURLKey = "starterURL"
33 | skipStarterDownloadKey = "skipStarterDownload"
34 | )
35 |
36 | func main() {
37 | // Create command
38 | cmd := &cobra.Command{
39 | Use: "bofied-backend",
40 | Short: "Modern network boot server.",
41 | Long: `bofied is a network boot server. It provides everything you need to PXE boot a node, from a (proxy)DHCP server for PXE service to a TFTP and HTTP server to serve boot files.
42 |
43 | For more information, please visit https://github.com/pojntfx/bofied.`,
44 | RunE: func(cmd *cobra.Command, args []string) error {
45 | // Bind config file
46 | if !(viper.GetString(configFileKey) == "") {
47 | viper.SetConfigFile(viper.GetString(configFileKey))
48 |
49 | if err := viper.ReadInConfig(); err != nil {
50 | return err
51 | }
52 | }
53 |
54 | // Initialize the working directory
55 | configFilePath := filepath.Join(viper.GetString(workingDirKey), constants.BootConfigFileName)
56 | if viper.GetBool(skipStarterDownloadKey) {
57 | // Initialize with just a config file
58 | if err := config.CreateConfigIfNotExists(configFilePath); err != nil {
59 | log.Fatal(err)
60 | }
61 | } else {
62 | // Initialize with a starter
63 | if err := config.GetStarterIfNotExists(configFilePath, viper.GetString(starterURLKey), viper.GetString(workingDirKey)); err != nil {
64 | log.Fatal(err)
65 | }
66 | }
67 |
68 | // Parse flags
69 | _, tftpPortRaw, err := net.SplitHostPort(viper.GetString(tftpListenAddressKey))
70 | if err != nil {
71 | return err
72 | }
73 |
74 | tftpPort, err := strconv.Atoi(tftpPortRaw)
75 | if err != nil {
76 | return err
77 | }
78 |
79 | _, httpPortRaw, err := net.SplitHostPort(viper.GetString(webDAVAndHTTPListenAddressKey))
80 | if err != nil {
81 | return err
82 | }
83 |
84 | httpPort, err := strconv.Atoi(httpPortRaw)
85 | if err != nil {
86 | return err
87 | }
88 |
89 | // Create eventing utilities
90 | eventsHandler := eventing.NewEventHandler()
91 |
92 | // Create auth utilities
93 | oidcValidator := validators.NewOIDCValidator(viper.GetString(oidcIssuerKey), viper.GetString(oidcClientIDKey))
94 | if err := oidcValidator.Open(); err != nil {
95 | log.Fatal(err)
96 | }
97 | contextValidator := validators.NewContextValidator(services.AuthorizationMetadataKey, oidcValidator)
98 |
99 | // Create services
100 | eventsService := services.NewEventsService(eventsHandler, contextValidator)
101 | metadataService := services.NewMetadataService(
102 | viper.GetString(advertisedIPKey),
103 | int32(tftpPort),
104 | int32(httpPort),
105 | contextValidator,
106 | )
107 |
108 | // Create servers
109 | dhcpServer := servers.NewDHCPServer(
110 | viper.GetString(dhcpListenAddressKey),
111 | viper.GetString(advertisedIPKey),
112 | eventsHandler,
113 | )
114 | proxyDHCPServer := servers.NewProxyDHCPServer(
115 | viper.GetString(proxyDHCPListenAddressKey),
116 | viper.GetString(advertisedIPKey),
117 | filepath.Join(viper.GetString(workingDirKey), constants.BootConfigFileName),
118 | eventsHandler,
119 | viper.GetBool(pureConfigKey),
120 | )
121 | tftpServer := servers.NewTFTPServer(
122 | viper.GetString(workingDirKey),
123 | viper.GetString(tftpListenAddressKey),
124 | eventsHandler,
125 | )
126 | grpcServer, grpcServerHandler := servers.NewGRPCServer(viper.GetString(grpcListenAddressKey), eventsService, metadataService)
127 | extendedHTTPServer := servers.NewExtendedHTTPServer(viper.GetString(workingDirKey), viper.GetString(webDAVAndHTTPListenAddressKey), oidcValidator, grpcServerHandler, eventsHandler)
128 |
129 | // Start servers
130 | log.Printf(
131 | "bofied backend listening on %v (DHCP), %v (proxyDHCP), %v (TFTP), %v (WebDAV on %v, HTTP on %v and gRPC-Web on %v) and %v (gRPC), advertising IP %v to DHCP clients\n",
132 | viper.GetString(dhcpListenAddressKey),
133 | viper.GetString(proxyDHCPListenAddressKey),
134 | viper.GetString(tftpListenAddressKey),
135 | viper.GetString(webDAVAndHTTPListenAddressKey),
136 | servers.WebDAVPrefix,
137 | servers.HTTPPrefix,
138 | servers.GRPCPrefix,
139 | viper.GetString(grpcListenAddressKey),
140 | viper.GetString(advertisedIPKey),
141 | )
142 |
143 | go func() {
144 | log.Fatal(dhcpServer.ListenAndServe())
145 | }()
146 |
147 | go func() {
148 | log.Fatal(proxyDHCPServer.ListenAndServe())
149 | }()
150 |
151 | go func() {
152 | log.Fatal(tftpServer.ListenAndServe())
153 | }()
154 |
155 | go func() {
156 | log.Fatal(grpcServer.ListenAndServe())
157 | }()
158 |
159 | return extendedHTTPServer.ListenAndServe()
160 | },
161 | }
162 |
163 | // Get default working dir
164 | home, err := os.UserHomeDir()
165 | if err != nil {
166 | log.Fatal("could not get home directory", err)
167 | }
168 | workingDirDefault := filepath.Join(home, ".local", "share", "bofied", "var", "lib", "bofied")
169 |
170 | // Bind flags
171 | cmd.PersistentFlags().StringP(configFileKey, "c", "", "Config file to use")
172 | cmd.PersistentFlags().StringP(workingDirKey, "d", workingDirDefault, "Working directory")
173 | cmd.PersistentFlags().String(advertisedIPKey, "100.64.154.246", "IP to advertise for DHCP clients")
174 |
175 | cmd.PersistentFlags().String(dhcpListenAddressKey, ":67", "Listen address for DHCP server")
176 | cmd.PersistentFlags().String(proxyDHCPListenAddressKey, ":4011", "Listen address for proxyDHCP server")
177 | cmd.PersistentFlags().String(tftpListenAddressKey, ":69", "Listen address for TFTP server")
178 | cmd.PersistentFlags().String(webDAVAndHTTPListenAddressKey, ":15256", "Listen address for WebDAV, HTTP and gRPC-Web server")
179 | cmd.PersistentFlags().String(grpcListenAddressKey, ":15257", "Listen address for gRPC server")
180 |
181 | cmd.PersistentFlags().StringP(oidcIssuerKey, "i", "https://pojntfx.eu.auth0.com/", "OIDC issuer")
182 | cmd.PersistentFlags().StringP(oidcClientIDKey, "t", "myoidcclientid", "OIDC client ID")
183 |
184 | cmd.PersistentFlags().BoolP(pureConfigKey, "p", false, "Prevent usage of stdlib in configuration file, even if enabled in `Configuration` function")
185 |
186 | cmd.PersistentFlags().String(starterURLKey, "https://github.com/pojntfx/ipxe-binaries/releases/latest/download/ipxe.tar.gz", "Download URL to a starter .tar.gz archive; the default chainloads https://netboot.xyz/")
187 | cmd.PersistentFlags().BoolP(skipStarterDownloadKey, "s", false, "Don't initialize by downloading the starter on the first run")
188 |
189 | // Bind env variables
190 | if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil {
191 | log.Fatal(err)
192 | }
193 | viper.SetEnvPrefix("bofied_backend")
194 | viper.AutomaticEnv()
195 |
196 | // Run command
197 | if err := cmd.Execute(); err != nil {
198 | log.Fatal(err)
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/cmd/bofied-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/v10/pkg/app"
10 | "github.com/pojntfx/bofied/pkg/components"
11 | )
12 |
13 | func main() {
14 | // Client-side code
15 | {
16 | // Define the routes
17 | app.Route("/", func() app.Composer {
18 | return &components.Home{}
19 | })
20 |
21 | // Start the app
22 | app.RunWhenOnBrowser()
23 | }
24 |
25 | // Server-/build-side code
26 | {
27 | // Parse the flags
28 | build := flag.Bool("build", false, "Create static build")
29 | out := flag.String("out", "out/bofied-frontend", "Out directory for static build")
30 | path := flag.String("path", "", "Base path for static build")
31 | serve := flag.Bool("serve", false, "Build and serve the frontend")
32 | laddr := flag.String("laddr", "localhost:15255", "Address to serve the frontend on")
33 |
34 | flag.Parse()
35 |
36 | // Define the handler
37 | h := &app.Handler{
38 | Author: "Felicitas Pojtinger",
39 | BackgroundColor: "#151515",
40 | Description: "Modern network boot server.",
41 | Icon: app.Icon{
42 | Default: "/web/icon.png",
43 | },
44 | Keywords: []string{
45 | "pxe-boot",
46 | "ipxe",
47 | "netboot",
48 | "network-boot",
49 | "http-server",
50 | "dhcp-server",
51 | "pxe",
52 | "webdav-server",
53 | "tftp-server",
54 | "proxy-dhcp",
55 | },
56 | LoadingLabel: "Modern network boot server.",
57 | Name: "bofied",
58 | RawHeaders: []string{
59 | ``,
60 | ``,
61 | ``,
62 | ``,
63 | },
64 | Styles: []string{
65 | `https://unpkg.com/@patternfly/patternfly@6.0.0/patternfly.css`,
66 | `https://unpkg.com/@patternfly/patternfly@6.0.0/patternfly-addons.css`,
67 | `/web/index.css`,
68 | },
69 | ThemeColor: "#151515",
70 | Title: "bofied",
71 | }
72 |
73 | // Create static build if specified
74 | if *build {
75 | // Deploy under a path
76 | if *path != "" {
77 | h.Resources = app.GitHubPages(*path)
78 | }
79 |
80 | if err := app.GenerateStaticWebsite(*out, h); err != nil {
81 | log.Fatalf("could not build: %v\n", err)
82 | }
83 | }
84 |
85 | // Serve if specified
86 | if *serve {
87 | log.Printf("bofied frontend listening on %v\n", *laddr)
88 |
89 | if err := http.ListenAndServe(*laddr, compress.Handler(h)); err != nil {
90 | log.Fatalf("could not open bofied frontend: %v\n", err)
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/docs/demo.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/docs/demo.mp4
--------------------------------------------------------------------------------
/docs/screenshot-about-modal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/docs/screenshot-about-modal.png
--------------------------------------------------------------------------------
/docs/screenshot-file-operations-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/docs/screenshot-file-operations-2.png
--------------------------------------------------------------------------------
/docs/screenshot-file-operations-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/docs/screenshot-file-operations-3.png
--------------------------------------------------------------------------------
/docs/screenshot-gnome-files-webdav-listing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/docs/screenshot-gnome-files-webdav-listing.png
--------------------------------------------------------------------------------
/docs/screenshot-gnome-files-webdav-mounting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/docs/screenshot-gnome-files-webdav-mounting.png
--------------------------------------------------------------------------------
/docs/screenshot-initial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/docs/screenshot-initial.png
--------------------------------------------------------------------------------
/docs/screenshot-monitoring.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/docs/screenshot-monitoring.png
--------------------------------------------------------------------------------
/docs/screenshot-mount-directory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/docs/screenshot-mount-directory.png
--------------------------------------------------------------------------------
/docs/screenshot-setup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/docs/screenshot-setup.png
--------------------------------------------------------------------------------
/docs/screenshot-sharing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/docs/screenshot-sharing.png
--------------------------------------------------------------------------------
/docs/screenshot-syntax-validation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/docs/screenshot-syntax-validation.png
--------------------------------------------------------------------------------
/docs/screenshot-text-editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/docs/screenshot-text-editor.png
--------------------------------------------------------------------------------
/examples/bofied-backend-config.yaml:
--------------------------------------------------------------------------------
1 | advertisedIP: "100.64.154.246"
2 | dhcpListenAddress: ":67"
3 | extendedHTTPListenAddress: ":15256"
4 | grpcListenAddress: ":15257"
5 | oidcClientID: myoidcclientid
6 | oidcIssuer: https://pojntfx.eu.auth0.com/
7 | proxyDHCPListenAddress: ":4011"
8 | pureConfig: false
9 | tftpListenAddress: ":69"
10 | workingDir: /home/pojntfx/.local/share/bofied/var/lib/bofied
11 | starterURL: https://github.com/pojntfx/ipxe-binaries/releases/download/latest/ipxe.tar.gz
12 | skipStarterDownload: false
13 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/pojntfx/bofied
2 |
3 | go 1.22.7
4 |
5 | require (
6 | github.com/codeclysm/extract/v3 v3.1.1
7 | github.com/coreos/go-oidc/v3 v3.11.0
8 | github.com/google/gopacket v1.1.19
9 | github.com/kataras/compress v0.0.6
10 | github.com/maxence-charriere/go-app/v10 v10.0.8
11 | github.com/pin/tftp/v3 v3.1.0
12 | github.com/rs/cors v1.11.1
13 | github.com/spf13/cobra v1.8.1
14 | github.com/spf13/viper v1.19.0
15 | github.com/studio-b12/gowebdav v0.9.0
16 | github.com/traefik/yaegi v0.16.1
17 | github.com/ugjka/messenger v1.1.3
18 | golang.org/x/net v0.30.0
19 | golang.org/x/oauth2 v0.23.0
20 | google.golang.org/grpc v1.67.1
21 | google.golang.org/protobuf v1.35.1
22 | nhooyr.io/websocket v1.8.17
23 | )
24 |
25 | require (
26 | github.com/andybalholm/brotli v1.1.1 // indirect
27 | github.com/fsnotify/fsnotify v1.7.0 // indirect
28 | github.com/go-jose/go-jose/v4 v4.0.4 // indirect
29 | github.com/google/uuid v1.6.0 // indirect
30 | github.com/h2non/filetype v1.1.3 // indirect
31 | github.com/hashicorp/hcl v1.0.0 // indirect
32 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
33 | github.com/juju/errors v1.0.0 // indirect
34 | github.com/klauspost/compress v1.17.11 // indirect
35 | github.com/magiconair/properties v1.8.7 // indirect
36 | github.com/mitchellh/mapstructure v1.5.0 // indirect
37 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect
38 | github.com/sagikazarmark/locafero v0.6.0 // indirect
39 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect
40 | github.com/sourcegraph/conc v0.3.0 // indirect
41 | github.com/spf13/afero v1.11.0 // indirect
42 | github.com/spf13/cast v1.7.0 // indirect
43 | github.com/spf13/pflag v1.0.5 // indirect
44 | github.com/subosito/gotenv v1.6.0 // indirect
45 | github.com/ulikunitz/xz v0.5.12 // indirect
46 | go.uber.org/multierr v1.11.0 // indirect
47 | golang.org/x/crypto v0.28.0 // indirect
48 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
49 | golang.org/x/sys v0.26.0 // indirect
50 | golang.org/x/text v0.19.0 // indirect
51 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
52 | gopkg.in/ini.v1 v1.67.0 // indirect
53 | gopkg.in/yaml.v3 v3.0.1 // indirect
54 | )
55 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
2 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
3 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
4 | github.com/arduino/go-paths-helper v1.2.0 h1:qDW93PR5IZUN/jzO4rCtexiwF8P4OIcOmcSgAYLZfY4=
5 | github.com/arduino/go-paths-helper v1.2.0/go.mod h1:HpxtKph+g238EJHq4geEPv9p+gl3v5YYu35Yb+w31Ck=
6 | github.com/codeclysm/extract/v3 v3.1.1 h1:iHZtdEAwSTqPrd+1n4jfhr1qBhUWtHlMTjT90+fJVXg=
7 | github.com/codeclysm/extract/v3 v3.1.1/go.mod h1:ZJi80UG2JtfHqJI+lgJSCACttZi++dHxfWuPaMhlOfQ=
8 | github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
9 | github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
10 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
13 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
15 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
16 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
17 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
18 | github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
19 | github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
20 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
21 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
22 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
23 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
24 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
25 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
26 | github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
27 | github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
28 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
29 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
30 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
31 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
32 | github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM=
33 | github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8=
34 | github.com/kataras/compress v0.0.6 h1:EMFR0GuyrLaTp3BmqKciVcyyb6By+dVJgY4deP1sz+A=
35 | github.com/kataras/compress v0.0.6/go.mod h1:xru59oerl89gl/p3nzbmGR12C9+XMdlZ8jNF43XyEPA=
36 | github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
37 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
38 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
39 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
40 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
41 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
42 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
43 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
44 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
45 | github.com/maxence-charriere/go-app/v10 v10.0.8 h1:ZbHTaIN1nMTzMvWmx5/wAMioDxnftNmkYfcX3O4lleE=
46 | github.com/maxence-charriere/go-app/v10 v10.0.8/go.mod h1:VyjGLeTiK6hfAQ/Q5ZVcLbuJ9vOZhKcewSC/ZwI+6WM=
47 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
48 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
49 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
50 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
51 | github.com/pin/tftp/v3 v3.1.0 h1:rQaxd4pGwcAJnpId8zC+O2NX3B2/NscjDZQaqEjuE7c=
52 | github.com/pin/tftp/v3 v3.1.0/go.mod h1:xwQaN4viYL019tM4i8iecm++5cGxSqen6AJEOEyEI0w=
53 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
54 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
55 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
56 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
57 | github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
58 | github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
59 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
60 | github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
61 | github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
62 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
63 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
64 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
65 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
66 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
67 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
68 | github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
69 | github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
70 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
71 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
72 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
73 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
74 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
75 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
76 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
77 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
78 | github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
79 | github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
80 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
81 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
82 | github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
83 | github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
84 | github.com/ugjka/messenger v1.1.3 h1:8Eum8yF2CeGEj0KpldA/cmDCw4JH1mQK40817wxAP7w=
85 | github.com/ugjka/messenger v1.1.3/go.mod h1:yjM7cZkSgCqFGKvQ7z1egQHzweas2pstCAdP1chzeCQ=
86 | github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
87 | github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
88 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
89 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
90 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
91 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
92 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
93 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
94 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
95 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
96 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
97 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
98 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
99 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
100 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
101 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
102 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
103 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
104 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
105 | golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
106 | golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
107 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
108 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
109 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
110 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
111 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
112 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
113 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
114 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
115 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
116 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
117 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI=
118 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
119 | google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
120 | google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
121 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
122 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
123 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
124 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
125 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
126 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
127 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
128 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
129 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
130 | nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
131 | nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
132 |
--------------------------------------------------------------------------------
/pkg/api/proto/v1/events.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // versions:
3 | // protoc-gen-go v1.35.2
4 | // protoc v3.19.6
5 | // source: events.proto
6 |
7 | package v1
8 |
9 | import (
10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect"
11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl"
12 | reflect "reflect"
13 | sync "sync"
14 | )
15 |
16 | const (
17 | // Verify that this generated code is sufficiently up-to-date.
18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
19 | // Verify that runtime/protoimpl is sufficiently up-to-date.
20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
21 | )
22 |
23 | type EventMessage struct {
24 | state protoimpl.MessageState
25 | sizeCache protoimpl.SizeCache
26 | unknownFields protoimpl.UnknownFields
27 |
28 | CreatedAt string `protobuf:"bytes,1,opt,name=CreatedAt,proto3" json:"CreatedAt,omitempty"`
29 | Message string `protobuf:"bytes,2,opt,name=Message,proto3" json:"Message,omitempty"`
30 | }
31 |
32 | func (x *EventMessage) Reset() {
33 | *x = EventMessage{}
34 | mi := &file_events_proto_msgTypes[0]
35 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
36 | ms.StoreMessageInfo(mi)
37 | }
38 |
39 | func (x *EventMessage) String() string {
40 | return protoimpl.X.MessageStringOf(x)
41 | }
42 |
43 | func (*EventMessage) ProtoMessage() {}
44 |
45 | func (x *EventMessage) ProtoReflect() protoreflect.Message {
46 | mi := &file_events_proto_msgTypes[0]
47 | if x != nil {
48 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
49 | if ms.LoadMessageInfo() == nil {
50 | ms.StoreMessageInfo(mi)
51 | }
52 | return ms
53 | }
54 | return mi.MessageOf(x)
55 | }
56 |
57 | // Deprecated: Use EventMessage.ProtoReflect.Descriptor instead.
58 | func (*EventMessage) Descriptor() ([]byte, []int) {
59 | return file_events_proto_rawDescGZIP(), []int{0}
60 | }
61 |
62 | func (x *EventMessage) GetCreatedAt() string {
63 | if x != nil {
64 | return x.CreatedAt
65 | }
66 | return ""
67 | }
68 |
69 | func (x *EventMessage) GetMessage() string {
70 | if x != nil {
71 | return x.Message
72 | }
73 | return ""
74 | }
75 |
76 | var File_events_proto protoreflect.FileDescriptor
77 |
78 | var file_events_proto_rawDesc = []byte{
79 | 0x0a, 0x0c, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1e,
80 | 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6a, 0x74, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x66, 0x65,
81 | 0x6c, 0x69, 0x63, 0x69, 0x74, 0x61, 0x73, 0x2e, 0x62, 0x6f, 0x66, 0x69, 0x65, 0x64, 0x1a, 0x0e,
82 | 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x46,
83 | 0x0a, 0x0c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1c,
84 | 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28,
85 | 0x09, 0x52, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x18, 0x0a, 0x07,
86 | 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4d,
87 | 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x7b, 0x0a, 0x0d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73,
88 | 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6a, 0x0a, 0x11, 0x53, 0x75, 0x62, 0x73, 0x63,
89 | 0x72, 0x69, 0x62, 0x65, 0x54, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x25, 0x2e, 0x63,
90 | 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6a, 0x74, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x66, 0x65, 0x6c,
91 | 0x69, 0x63, 0x69, 0x74, 0x61, 0x73, 0x2e, 0x62, 0x6f, 0x66, 0x69, 0x65, 0x64, 0x2e, 0x45, 0x6d,
92 | 0x70, 0x74, 0x79, 0x1a, 0x2c, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6a, 0x74, 0x69, 0x6e,
93 | 0x67, 0x65, 0x72, 0x2e, 0x66, 0x65, 0x6c, 0x69, 0x63, 0x69, 0x74, 0x61, 0x73, 0x2e, 0x62, 0x6f,
94 | 0x66, 0x69, 0x65, 0x64, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
95 | 0x65, 0x30, 0x01, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
96 | 0x6d, 0x2f, 0x70, 0x6f, 0x6a, 0x6e, 0x74, 0x66, 0x78, 0x2f, 0x62, 0x6f, 0x66, 0x69, 0x65, 0x64,
97 | 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76,
98 | 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
99 | }
100 |
101 | var (
102 | file_events_proto_rawDescOnce sync.Once
103 | file_events_proto_rawDescData = file_events_proto_rawDesc
104 | )
105 |
106 | func file_events_proto_rawDescGZIP() []byte {
107 | file_events_proto_rawDescOnce.Do(func() {
108 | file_events_proto_rawDescData = protoimpl.X.CompressGZIP(file_events_proto_rawDescData)
109 | })
110 | return file_events_proto_rawDescData
111 | }
112 |
113 | var file_events_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
114 | var file_events_proto_goTypes = []any{
115 | (*EventMessage)(nil), // 0: com.pojtinger.felicitas.bofied.EventMessage
116 | (*Empty)(nil), // 1: com.pojtinger.felicitas.bofied.Empty
117 | }
118 | var file_events_proto_depIdxs = []int32{
119 | 1, // 0: com.pojtinger.felicitas.bofied.EventsService.SubscribeToEvents:input_type -> com.pojtinger.felicitas.bofied.Empty
120 | 0, // 1: com.pojtinger.felicitas.bofied.EventsService.SubscribeToEvents:output_type -> com.pojtinger.felicitas.bofied.EventMessage
121 | 1, // [1:2] is the sub-list for method output_type
122 | 0, // [0:1] is the sub-list for method input_type
123 | 0, // [0:0] is the sub-list for extension type_name
124 | 0, // [0:0] is the sub-list for extension extendee
125 | 0, // [0:0] is the sub-list for field type_name
126 | }
127 |
128 | func init() { file_events_proto_init() }
129 | func file_events_proto_init() {
130 | if File_events_proto != nil {
131 | return
132 | }
133 | file_metadata_proto_init()
134 | type x struct{}
135 | out := protoimpl.TypeBuilder{
136 | File: protoimpl.DescBuilder{
137 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
138 | RawDescriptor: file_events_proto_rawDesc,
139 | NumEnums: 0,
140 | NumMessages: 1,
141 | NumExtensions: 0,
142 | NumServices: 1,
143 | },
144 | GoTypes: file_events_proto_goTypes,
145 | DependencyIndexes: file_events_proto_depIdxs,
146 | MessageInfos: file_events_proto_msgTypes,
147 | }.Build()
148 | File_events_proto = out.File
149 | file_events_proto_rawDesc = nil
150 | file_events_proto_goTypes = nil
151 | file_events_proto_depIdxs = nil
152 | }
153 |
--------------------------------------------------------------------------------
/pkg/api/proto/v1/events_grpc.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
2 | // versions:
3 | // - protoc-gen-go-grpc v1.5.1
4 | // - protoc v3.19.6
5 | // source: events.proto
6 |
7 | package v1
8 |
9 | import (
10 | context "context"
11 | grpc "google.golang.org/grpc"
12 | codes "google.golang.org/grpc/codes"
13 | status "google.golang.org/grpc/status"
14 | )
15 |
16 | // This is a compile-time assertion to ensure that this generated file
17 | // is compatible with the grpc package it is being compiled against.
18 | // Requires gRPC-Go v1.64.0 or later.
19 | const _ = grpc.SupportPackageIsVersion9
20 |
21 | const (
22 | EventsService_SubscribeToEvents_FullMethodName = "/com.pojtinger.felicitas.bofied.EventsService/SubscribeToEvents"
23 | )
24 |
25 | // EventsServiceClient is the client API for EventsService service.
26 | //
27 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
28 | type EventsServiceClient interface {
29 | SubscribeToEvents(ctx context.Context, in *Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[EventMessage], error)
30 | }
31 |
32 | type eventsServiceClient struct {
33 | cc grpc.ClientConnInterface
34 | }
35 |
36 | func NewEventsServiceClient(cc grpc.ClientConnInterface) EventsServiceClient {
37 | return &eventsServiceClient{cc}
38 | }
39 |
40 | func (c *eventsServiceClient) SubscribeToEvents(ctx context.Context, in *Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[EventMessage], error) {
41 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
42 | stream, err := c.cc.NewStream(ctx, &EventsService_ServiceDesc.Streams[0], EventsService_SubscribeToEvents_FullMethodName, cOpts...)
43 | if err != nil {
44 | return nil, err
45 | }
46 | x := &grpc.GenericClientStream[Empty, EventMessage]{ClientStream: stream}
47 | if err := x.ClientStream.SendMsg(in); err != nil {
48 | return nil, err
49 | }
50 | if err := x.ClientStream.CloseSend(); err != nil {
51 | return nil, err
52 | }
53 | return x, nil
54 | }
55 |
56 | // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
57 | type EventsService_SubscribeToEventsClient = grpc.ServerStreamingClient[EventMessage]
58 |
59 | // EventsServiceServer is the server API for EventsService service.
60 | // All implementations must embed UnimplementedEventsServiceServer
61 | // for forward compatibility.
62 | type EventsServiceServer interface {
63 | SubscribeToEvents(*Empty, grpc.ServerStreamingServer[EventMessage]) error
64 | mustEmbedUnimplementedEventsServiceServer()
65 | }
66 |
67 | // UnimplementedEventsServiceServer must be embedded to have
68 | // forward compatible implementations.
69 | //
70 | // NOTE: this should be embedded by value instead of pointer to avoid a nil
71 | // pointer dereference when methods are called.
72 | type UnimplementedEventsServiceServer struct{}
73 |
74 | func (UnimplementedEventsServiceServer) SubscribeToEvents(*Empty, grpc.ServerStreamingServer[EventMessage]) error {
75 | return status.Errorf(codes.Unimplemented, "method SubscribeToEvents not implemented")
76 | }
77 | func (UnimplementedEventsServiceServer) mustEmbedUnimplementedEventsServiceServer() {}
78 | func (UnimplementedEventsServiceServer) testEmbeddedByValue() {}
79 |
80 | // UnsafeEventsServiceServer may be embedded to opt out of forward compatibility for this service.
81 | // Use of this interface is not recommended, as added methods to EventsServiceServer will
82 | // result in compilation errors.
83 | type UnsafeEventsServiceServer interface {
84 | mustEmbedUnimplementedEventsServiceServer()
85 | }
86 |
87 | func RegisterEventsServiceServer(s grpc.ServiceRegistrar, srv EventsServiceServer) {
88 | // If the following call pancis, it indicates UnimplementedEventsServiceServer was
89 | // embedded by pointer and is nil. This will cause panics if an
90 | // unimplemented method is ever invoked, so we test this at initialization
91 | // time to prevent it from happening at runtime later due to I/O.
92 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
93 | t.testEmbeddedByValue()
94 | }
95 | s.RegisterService(&EventsService_ServiceDesc, srv)
96 | }
97 |
98 | func _EventsService_SubscribeToEvents_Handler(srv interface{}, stream grpc.ServerStream) error {
99 | m := new(Empty)
100 | if err := stream.RecvMsg(m); err != nil {
101 | return err
102 | }
103 | return srv.(EventsServiceServer).SubscribeToEvents(m, &grpc.GenericServerStream[Empty, EventMessage]{ServerStream: stream})
104 | }
105 |
106 | // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
107 | type EventsService_SubscribeToEventsServer = grpc.ServerStreamingServer[EventMessage]
108 |
109 | // EventsService_ServiceDesc is the grpc.ServiceDesc for EventsService service.
110 | // It's only intended for direct use with grpc.RegisterService,
111 | // and not to be introspected or modified (even as a copy)
112 | var EventsService_ServiceDesc = grpc.ServiceDesc{
113 | ServiceName: "com.pojtinger.felicitas.bofied.EventsService",
114 | HandlerType: (*EventsServiceServer)(nil),
115 | Methods: []grpc.MethodDesc{},
116 | Streams: []grpc.StreamDesc{
117 | {
118 | StreamName: "SubscribeToEvents",
119 | Handler: _EventsService_SubscribeToEvents_Handler,
120 | ServerStreams: true,
121 | },
122 | },
123 | Metadata: "events.proto",
124 | }
125 |
--------------------------------------------------------------------------------
/pkg/api/proto/v1/metadata.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // versions:
3 | // protoc-gen-go v1.35.2
4 | // protoc v3.19.6
5 | // source: metadata.proto
6 |
7 | package v1
8 |
9 | import (
10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect"
11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl"
12 | reflect "reflect"
13 | sync "sync"
14 | )
15 |
16 | const (
17 | // Verify that this generated code is sufficiently up-to-date.
18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
19 | // Verify that runtime/protoimpl is sufficiently up-to-date.
20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
21 | )
22 |
23 | type Empty struct {
24 | state protoimpl.MessageState
25 | sizeCache protoimpl.SizeCache
26 | unknownFields protoimpl.UnknownFields
27 | }
28 |
29 | func (x *Empty) Reset() {
30 | *x = Empty{}
31 | mi := &file_metadata_proto_msgTypes[0]
32 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
33 | ms.StoreMessageInfo(mi)
34 | }
35 |
36 | func (x *Empty) String() string {
37 | return protoimpl.X.MessageStringOf(x)
38 | }
39 |
40 | func (*Empty) ProtoMessage() {}
41 |
42 | func (x *Empty) ProtoReflect() protoreflect.Message {
43 | mi := &file_metadata_proto_msgTypes[0]
44 | if x != nil {
45 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
46 | if ms.LoadMessageInfo() == nil {
47 | ms.StoreMessageInfo(mi)
48 | }
49 | return ms
50 | }
51 | return mi.MessageOf(x)
52 | }
53 |
54 | // Deprecated: Use Empty.ProtoReflect.Descriptor instead.
55 | func (*Empty) Descriptor() ([]byte, []int) {
56 | return file_metadata_proto_rawDescGZIP(), []int{0}
57 | }
58 |
59 | type MetadataMessage struct {
60 | state protoimpl.MessageState
61 | sizeCache protoimpl.SizeCache
62 | unknownFields protoimpl.UnknownFields
63 |
64 | AdvertisedIP string `protobuf:"bytes,1,opt,name=AdvertisedIP,proto3" json:"AdvertisedIP,omitempty"`
65 | TFTPPort int32 `protobuf:"varint,2,opt,name=TFTPPort,proto3" json:"TFTPPort,omitempty"`
66 | HTTPPort int32 `protobuf:"varint,3,opt,name=HTTPPort,proto3" json:"HTTPPort,omitempty"`
67 | }
68 |
69 | func (x *MetadataMessage) Reset() {
70 | *x = MetadataMessage{}
71 | mi := &file_metadata_proto_msgTypes[1]
72 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
73 | ms.StoreMessageInfo(mi)
74 | }
75 |
76 | func (x *MetadataMessage) String() string {
77 | return protoimpl.X.MessageStringOf(x)
78 | }
79 |
80 | func (*MetadataMessage) ProtoMessage() {}
81 |
82 | func (x *MetadataMessage) ProtoReflect() protoreflect.Message {
83 | mi := &file_metadata_proto_msgTypes[1]
84 | if x != nil {
85 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
86 | if ms.LoadMessageInfo() == nil {
87 | ms.StoreMessageInfo(mi)
88 | }
89 | return ms
90 | }
91 | return mi.MessageOf(x)
92 | }
93 |
94 | // Deprecated: Use MetadataMessage.ProtoReflect.Descriptor instead.
95 | func (*MetadataMessage) Descriptor() ([]byte, []int) {
96 | return file_metadata_proto_rawDescGZIP(), []int{1}
97 | }
98 |
99 | func (x *MetadataMessage) GetAdvertisedIP() string {
100 | if x != nil {
101 | return x.AdvertisedIP
102 | }
103 | return ""
104 | }
105 |
106 | func (x *MetadataMessage) GetTFTPPort() int32 {
107 | if x != nil {
108 | return x.TFTPPort
109 | }
110 | return 0
111 | }
112 |
113 | func (x *MetadataMessage) GetHTTPPort() int32 {
114 | if x != nil {
115 | return x.HTTPPort
116 | }
117 | return 0
118 | }
119 |
120 | var File_metadata_proto protoreflect.FileDescriptor
121 |
122 | var file_metadata_proto_rawDesc = []byte{
123 | 0x0a, 0x0e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
124 | 0x12, 0x1e, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6a, 0x74, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x2e,
125 | 0x66, 0x65, 0x6c, 0x69, 0x63, 0x69, 0x74, 0x61, 0x73, 0x2e, 0x62, 0x6f, 0x66, 0x69, 0x65, 0x64,
126 | 0x22, 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x6d, 0x0a, 0x0f, 0x4d, 0x65, 0x74,
127 | 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x22, 0x0a, 0x0c,
128 | 0x41, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x64, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01,
129 | 0x28, 0x09, 0x52, 0x0c, 0x41, 0x64, 0x76, 0x65, 0x72, 0x74, 0x69, 0x73, 0x65, 0x64, 0x49, 0x50,
130 | 0x12, 0x1a, 0x0a, 0x08, 0x54, 0x46, 0x54, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01,
131 | 0x28, 0x05, 0x52, 0x08, 0x54, 0x46, 0x54, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08,
132 | 0x48, 0x54, 0x54, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08,
133 | 0x48, 0x54, 0x54, 0x50, 0x50, 0x6f, 0x72, 0x74, 0x32, 0x78, 0x0a, 0x0f, 0x4d, 0x65, 0x74, 0x61,
134 | 0x64, 0x61, 0x74, 0x61, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x65, 0x0a, 0x0b, 0x47,
135 | 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x25, 0x2e, 0x63, 0x6f, 0x6d,
136 | 0x2e, 0x70, 0x6f, 0x6a, 0x74, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x2e, 0x66, 0x65, 0x6c, 0x69, 0x63,
137 | 0x69, 0x74, 0x61, 0x73, 0x2e, 0x62, 0x6f, 0x66, 0x69, 0x65, 0x64, 0x2e, 0x45, 0x6d, 0x70, 0x74,
138 | 0x79, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x70, 0x6f, 0x6a, 0x74, 0x69, 0x6e, 0x67, 0x65,
139 | 0x72, 0x2e, 0x66, 0x65, 0x6c, 0x69, 0x63, 0x69, 0x74, 0x61, 0x73, 0x2e, 0x62, 0x6f, 0x66, 0x69,
140 | 0x65, 0x64, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4d, 0x65, 0x73, 0x73, 0x61,
141 | 0x67, 0x65, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
142 | 0x2f, 0x70, 0x6f, 0x6a, 0x6e, 0x74, 0x66, 0x78, 0x2f, 0x62, 0x6f, 0x66, 0x69, 0x65, 0x64, 0x2f,
143 | 0x70, 0x6b, 0x67, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31,
144 | 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
145 | }
146 |
147 | var (
148 | file_metadata_proto_rawDescOnce sync.Once
149 | file_metadata_proto_rawDescData = file_metadata_proto_rawDesc
150 | )
151 |
152 | func file_metadata_proto_rawDescGZIP() []byte {
153 | file_metadata_proto_rawDescOnce.Do(func() {
154 | file_metadata_proto_rawDescData = protoimpl.X.CompressGZIP(file_metadata_proto_rawDescData)
155 | })
156 | return file_metadata_proto_rawDescData
157 | }
158 |
159 | var file_metadata_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
160 | var file_metadata_proto_goTypes = []any{
161 | (*Empty)(nil), // 0: com.pojtinger.felicitas.bofied.Empty
162 | (*MetadataMessage)(nil), // 1: com.pojtinger.felicitas.bofied.MetadataMessage
163 | }
164 | var file_metadata_proto_depIdxs = []int32{
165 | 0, // 0: com.pojtinger.felicitas.bofied.MetadataService.GetMetadata:input_type -> com.pojtinger.felicitas.bofied.Empty
166 | 1, // 1: com.pojtinger.felicitas.bofied.MetadataService.GetMetadata:output_type -> com.pojtinger.felicitas.bofied.MetadataMessage
167 | 1, // [1:2] is the sub-list for method output_type
168 | 0, // [0:1] is the sub-list for method input_type
169 | 0, // [0:0] is the sub-list for extension type_name
170 | 0, // [0:0] is the sub-list for extension extendee
171 | 0, // [0:0] is the sub-list for field type_name
172 | }
173 |
174 | func init() { file_metadata_proto_init() }
175 | func file_metadata_proto_init() {
176 | if File_metadata_proto != nil {
177 | return
178 | }
179 | type x struct{}
180 | out := protoimpl.TypeBuilder{
181 | File: protoimpl.DescBuilder{
182 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
183 | RawDescriptor: file_metadata_proto_rawDesc,
184 | NumEnums: 0,
185 | NumMessages: 2,
186 | NumExtensions: 0,
187 | NumServices: 1,
188 | },
189 | GoTypes: file_metadata_proto_goTypes,
190 | DependencyIndexes: file_metadata_proto_depIdxs,
191 | MessageInfos: file_metadata_proto_msgTypes,
192 | }.Build()
193 | File_metadata_proto = out.File
194 | file_metadata_proto_rawDesc = nil
195 | file_metadata_proto_goTypes = nil
196 | file_metadata_proto_depIdxs = nil
197 | }
198 |
--------------------------------------------------------------------------------
/pkg/api/proto/v1/metadata_grpc.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
2 | // versions:
3 | // - protoc-gen-go-grpc v1.5.1
4 | // - protoc v3.19.6
5 | // source: metadata.proto
6 |
7 | package v1
8 |
9 | import (
10 | context "context"
11 | grpc "google.golang.org/grpc"
12 | codes "google.golang.org/grpc/codes"
13 | status "google.golang.org/grpc/status"
14 | )
15 |
16 | // This is a compile-time assertion to ensure that this generated file
17 | // is compatible with the grpc package it is being compiled against.
18 | // Requires gRPC-Go v1.64.0 or later.
19 | const _ = grpc.SupportPackageIsVersion9
20 |
21 | const (
22 | MetadataService_GetMetadata_FullMethodName = "/com.pojtinger.felicitas.bofied.MetadataService/GetMetadata"
23 | )
24 |
25 | // MetadataServiceClient is the client API for MetadataService service.
26 | //
27 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
28 | type MetadataServiceClient interface {
29 | GetMetadata(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*MetadataMessage, error)
30 | }
31 |
32 | type metadataServiceClient struct {
33 | cc grpc.ClientConnInterface
34 | }
35 |
36 | func NewMetadataServiceClient(cc grpc.ClientConnInterface) MetadataServiceClient {
37 | return &metadataServiceClient{cc}
38 | }
39 |
40 | func (c *metadataServiceClient) GetMetadata(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*MetadataMessage, error) {
41 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
42 | out := new(MetadataMessage)
43 | err := c.cc.Invoke(ctx, MetadataService_GetMetadata_FullMethodName, in, out, cOpts...)
44 | if err != nil {
45 | return nil, err
46 | }
47 | return out, nil
48 | }
49 |
50 | // MetadataServiceServer is the server API for MetadataService service.
51 | // All implementations must embed UnimplementedMetadataServiceServer
52 | // for forward compatibility.
53 | type MetadataServiceServer interface {
54 | GetMetadata(context.Context, *Empty) (*MetadataMessage, error)
55 | mustEmbedUnimplementedMetadataServiceServer()
56 | }
57 |
58 | // UnimplementedMetadataServiceServer must be embedded to have
59 | // forward compatible implementations.
60 | //
61 | // NOTE: this should be embedded by value instead of pointer to avoid a nil
62 | // pointer dereference when methods are called.
63 | type UnimplementedMetadataServiceServer struct{}
64 |
65 | func (UnimplementedMetadataServiceServer) GetMetadata(context.Context, *Empty) (*MetadataMessage, error) {
66 | return nil, status.Errorf(codes.Unimplemented, "method GetMetadata not implemented")
67 | }
68 | func (UnimplementedMetadataServiceServer) mustEmbedUnimplementedMetadataServiceServer() {}
69 | func (UnimplementedMetadataServiceServer) testEmbeddedByValue() {}
70 |
71 | // UnsafeMetadataServiceServer may be embedded to opt out of forward compatibility for this service.
72 | // Use of this interface is not recommended, as added methods to MetadataServiceServer will
73 | // result in compilation errors.
74 | type UnsafeMetadataServiceServer interface {
75 | mustEmbedUnimplementedMetadataServiceServer()
76 | }
77 |
78 | func RegisterMetadataServiceServer(s grpc.ServiceRegistrar, srv MetadataServiceServer) {
79 | // If the following call pancis, it indicates UnimplementedMetadataServiceServer was
80 | // embedded by pointer and is nil. This will cause panics if an
81 | // unimplemented method is ever invoked, so we test this at initialization
82 | // time to prevent it from happening at runtime later due to I/O.
83 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
84 | t.testEmbeddedByValue()
85 | }
86 | s.RegisterService(&MetadataService_ServiceDesc, srv)
87 | }
88 |
89 | func _MetadataService_GetMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
90 | in := new(Empty)
91 | if err := dec(in); err != nil {
92 | return nil, err
93 | }
94 | if interceptor == nil {
95 | return srv.(MetadataServiceServer).GetMetadata(ctx, in)
96 | }
97 | info := &grpc.UnaryServerInfo{
98 | Server: srv,
99 | FullMethod: MetadataService_GetMetadata_FullMethodName,
100 | }
101 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
102 | return srv.(MetadataServiceServer).GetMetadata(ctx, req.(*Empty))
103 | }
104 | return interceptor(ctx, in, info, handler)
105 | }
106 |
107 | // MetadataService_ServiceDesc is the grpc.ServiceDesc for MetadataService service.
108 | // It's only intended for direct use with grpc.RegisterService,
109 | // and not to be introspected or modified (even as a copy)
110 | var MetadataService_ServiceDesc = grpc.ServiceDesc{
111 | ServiceName: "com.pojtinger.felicitas.bofied.MetadataService",
112 | HandlerType: (*MetadataServiceServer)(nil),
113 | Methods: []grpc.MethodDesc{
114 | {
115 | MethodName: "GetMetadata",
116 | Handler: _MetadataService_GetMetadata_Handler,
117 | },
118 | },
119 | Streams: []grpc.StreamDesc{},
120 | Metadata: "metadata.proto",
121 | }
122 |
--------------------------------------------------------------------------------
/pkg/authorization/oidc_over_basic_auth.go:
--------------------------------------------------------------------------------
1 | package authorization
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 | "log"
7 | "net/http"
8 |
9 | "github.com/pojntfx/bofied/pkg/validators"
10 | )
11 |
12 | func OIDCOverBasicAuth(next http.Handler, username string, oidcValidator *validators.OIDCValidator, description string) http.Handler {
13 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
14 | // Validate the OIDC token which is passed as a HTTP basic auth password due to client limitations
15 | user, pass, ok := r.BasicAuth()
16 | if _, err := oidcValidator.Validate(pass); err != nil || !ok || user != username {
17 | // Unauthorized, log and redirect
18 | log.Println("could not authorize user, redirecting")
19 |
20 | rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%v"`, description))
21 | rw.WriteHeader(401)
22 | rw.Write([]byte("could not authorize: " + err.Error()))
23 |
24 | return
25 | }
26 |
27 | // Authorized, continue
28 | next.ServeHTTP(rw, r)
29 | })
30 | }
31 |
32 | func GetOIDCOverBasicAuthHeader(username string, idToken string) (key string, value string) {
33 | key = "Authorization"
34 |
35 | value = "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+idToken))
36 |
37 | return
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/components/about_modal.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "github.com/maxence-charriere/go-app/v10/pkg/app"
5 | )
6 |
7 | type AboutModal struct {
8 | app.Compo
9 |
10 | Open bool
11 | Close func()
12 |
13 | ID string
14 |
15 | LogoDarkSrc string
16 | LogoDarkAlt string
17 |
18 | LogoLightSrc string
19 | LogoLightAlt string
20 |
21 | Title string
22 |
23 | Body app.UI
24 | Footer string
25 | }
26 |
27 | func (c *AboutModal) Render() app.UI {
28 | return app.Div().
29 | Class(func() string {
30 | classes := "pf-v6-c-backdrop"
31 |
32 | if !c.Open {
33 | classes += " pf-v6-u-display-none"
34 | }
35 |
36 | return classes
37 | }()).
38 | Body(
39 | app.Div().
40 | Class("pf-v6-l-bullseye").
41 | Body(
42 | app.Div().
43 | Class("pf-v6-c-modal-box pf-m-lg").
44 | Aria("role", "dialog").
45 | Aria("modal", true).
46 | Aria("labelledby", c.ID).
47 | Body(
48 | app.Div().
49 | Class("pf-v6-c-about-modal-box").
50 | Body(
51 | app.Div().
52 | Class("pf-v6-c-about-modal-box__brand").
53 | Body(
54 | app.Img().
55 | Class("pf-v6-c-about-modal-box__brand-image pf-v6-c-brand--dark").
56 | Src(c.LogoDarkSrc).
57 | Alt(c.LogoDarkAlt),
58 |
59 | app.Img().
60 | Class("pf-v6-c-about-modal-box__brand-image pf-v6-c-brand--light").
61 | Src(c.LogoLightSrc).
62 | Alt(c.LogoLightAlt),
63 | ),
64 | app.Div().
65 | Class("pf-v6-c-about-modal-box__close").
66 | Body(
67 | app.Button().
68 | Class("pf-v6-c-button pf-m-plain").
69 | Type("button").
70 | Aria("label", "Close dialog").
71 | OnClick(func(ctx app.Context, e app.Event) {
72 | c.Close()
73 | }).
74 | Body(
75 | app.Span().
76 | Class("pf-v6-c-button__icon").
77 | Body(
78 | app.I().
79 | Class("fas fa-times").
80 | Aria("hidden", true),
81 | ),
82 | ),
83 | ),
84 | app.Div().
85 | Class("pf-v6-c-about-modal-box__header").
86 | Body(
87 | app.H1().
88 | Class("pf-v6-c-title pf-m-4xl").
89 | ID(c.ID).
90 | Text(c.Title),
91 | ),
92 | app.Div().Class("pf-v6-c-about-modal-box__hero"),
93 | app.Div().
94 | Class("pf-v6-c-about-modal-box__content").
95 | Body(
96 | app.Div().
97 | Class("pf-v6-c-content").
98 | Body(
99 | c.Body,
100 | ),
101 | app.P().
102 | Class("pf-v6-c-about-modal-box__strapline").
103 | Text(c.Footer),
104 | ),
105 | ),
106 | ),
107 | ),
108 | )
109 | }
110 |
111 | func (c *AboutModal) OnMount(ctx app.Context) {
112 | app.Window().Call("addEventListener", "keyup", app.FuncOf(func(this app.Value, args []app.Value) any {
113 | ctx.Async(func() {
114 | if len(args) > 0 && args[0].Get("key").String() == "Escape" {
115 | c.Close()
116 |
117 | ctx.Update()
118 | }
119 | })
120 |
121 | return nil
122 | }))
123 | }
124 |
--------------------------------------------------------------------------------
/pkg/components/autofocused.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "github.com/maxence-charriere/go-app/v10/pkg/app"
5 | )
6 |
7 | type Autofocused struct {
8 | app.Compo
9 |
10 | Component app.UI
11 | }
12 |
13 | func (c *Autofocused) Render() app.UI {
14 | return c.Component
15 | }
16 |
17 | func (c *Autofocused) OnUpdate(ctx app.Context) {
18 | ctx.Defer(func(_ app.Context) {
19 | c.JSValue().Call("focus")
20 | })
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/components/breadcrumbs.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "path"
5 |
6 | "github.com/maxence-charriere/go-app/v10/pkg/app"
7 | )
8 |
9 | type Breadcrumbs struct {
10 | app.Compo
11 |
12 | PathComponents []string
13 |
14 | CurrentPath string
15 | SetCurrentPath func(string)
16 |
17 | SelectedPath string
18 | SetSelectedPath func(string)
19 |
20 | ItemClass string
21 | }
22 |
23 | func (c *Breadcrumbs) Render() app.UI {
24 | return app.Nav().
25 | Class("pf-v6-c-breadcrumb").
26 | Aria("label", "Current path").
27 | Body(
28 | app.Ol().
29 | Class("pf-v6-c-breadcrumb__list pf-v6-u-font-weight-bold").
30 | Body(
31 | app.Li().
32 | Class("pf-v6-c-breadcrumb__item", c.ItemClass).
33 | Body(
34 | app.Span().
35 | Class("pf-v6-c-breadcrumb__item-divider").
36 | Body(
37 | app.I().
38 | Class("fas fa-angle-right").
39 | Aria("hidden", true),
40 | ),
41 | app.Button().
42 | Type("button").
43 | Class("pf-v6-c-breadcrumb__link").
44 | TabIndex(0).
45 | OnClick(func(ctx app.Context, e app.Event) {
46 | c.SetCurrentPath("/")
47 |
48 | c.SetSelectedPath("")
49 | }).
50 | Text("Files"),
51 | ),
52 | app.Range(c.PathComponents).Slice(func(i int) app.UI {
53 | link := path.Join(append([]string{"/"}, c.PathComponents[:i+1]...)...)
54 |
55 | // The last path part shouldn't be marked as a link
56 | classes := "pf-v6-c-breadcrumb__link"
57 | if i == len(c.PathComponents)-1 {
58 | classes += " pf-m-current"
59 | }
60 |
61 | return app.Li().
62 | Class("pf-v6-c-breadcrumb__item", c.ItemClass).
63 | Body(
64 | app.Span().
65 | Class("pf-v6-c-breadcrumb__item-divider").
66 | Body(
67 | app.I().
68 | Class("fas fa-angle-right").
69 | Aria("hidden", true),
70 | ),
71 | app.If(
72 | // The last path part shouldn't be an action
73 | i == len(c.PathComponents)-1,
74 | func() app.UI {
75 | return app.A().
76 | Class(classes).
77 | Text(c.PathComponents[i])
78 | },
79 | ).Else(
80 | func() app.UI {
81 | return app.Button().
82 | Type("button").
83 | Class(classes).
84 | OnClick(func(ctx app.Context, e app.Event) {
85 | c.SetCurrentPath(link)
86 |
87 | c.SetSelectedPath("")
88 | }).
89 | Text(c.PathComponents[i])
90 | },
91 | ),
92 | )
93 | }),
94 | ),
95 | )
96 | }
97 |
--------------------------------------------------------------------------------
/pkg/components/copyable_input.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import "github.com/maxence-charriere/go-app/v10/pkg/app"
4 |
5 | type CopyableInput struct {
6 | app.Compo
7 |
8 | Component app.UI
9 | ID string
10 | }
11 |
12 | func (c *CopyableInput) Render() app.UI {
13 | return app.Div().
14 | Class("pf-v6-c-clipboard-copy").
15 | Body(
16 | app.Div().
17 | Class("pf-v6-c-clipboard-copy__group").
18 | Body(
19 | app.Span().
20 | Class("pf-v6-c-form-control").
21 | Body(
22 | c.Component,
23 | ),
24 | app.Button().
25 | Class("pf-v6-c-button pf-m-control").
26 | Type("button").
27 | Aria("label", "Copy to clipboard").
28 | Aria("labelledby", c.ID).
29 | OnClick(func(ctx app.Context, e app.Event) {
30 | app.Window().JSValue().Get("document").Call("getElementById", c.ID).Call("select")
31 |
32 | app.Window().JSValue().Get("document").Call("execCommand", "copy")
33 | }).
34 | Body(
35 | app.Span().
36 | Class("pf-v6-c-button__icon").
37 | Body(
38 | app.I().
39 | Class("fas fa-copy").
40 | Aria("hidden", true),
41 | ),
42 | ),
43 | ),
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/components/data_shell.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "net/url"
5 | "os"
6 |
7 | "github.com/coreos/go-oidc/v3/oidc"
8 | "github.com/maxence-charriere/go-app/v10/pkg/app"
9 | "github.com/pojntfx/bofied/pkg/providers"
10 | "github.com/studio-b12/gowebdav"
11 | )
12 |
13 | type DataShell struct {
14 | app.Compo
15 |
16 | // Config file editor
17 | ConfigFile string
18 | SetConfigFile func(string)
19 |
20 | FormatConfigFile func()
21 | RefreshConfigFile func()
22 | SaveConfigFile func()
23 |
24 | ConfigFileError error
25 | IgnoreConfigFileError func()
26 |
27 | // File explorer
28 | CurrentPath string
29 | SetCurrentPath func(string)
30 |
31 | Index []os.FileInfo
32 | RefreshIndex func()
33 | WriteToPath func(string, []byte)
34 |
35 | HTTPShareLink url.URL
36 | TFTPShareLink url.URL
37 | SharePath func(string)
38 |
39 | CreatePath func(string)
40 | CreateEmptyFile func(string)
41 | DeletePath func(string)
42 | MovePath func(string, string)
43 | CopyPath func(string, string)
44 |
45 | EditPathContents string
46 | SetEditPathContents func(string)
47 | EditPath func(string)
48 |
49 | WebDAVAddress url.URL
50 | WebDAVUsername string
51 | WebDAVPassword string
52 |
53 | OperationIndex []os.FileInfo
54 |
55 | OperationCurrentPath string
56 | OperationSetCurrentPath func(string)
57 |
58 | FileExplorerError error
59 | RecoverFileExplorerError func(app.Context)
60 | IgnoreFileExplorerError func()
61 |
62 | Events []providers.Event
63 |
64 | EventsError error
65 | RecoverEventsError func(app.Context)
66 | IgnoreEventsError func()
67 |
68 | // Identity
69 | UserInfo oidc.UserInfo
70 | Logout func(app.Context)
71 |
72 | // Metadata
73 | UseAdvertisedIP bool
74 | SetUseAdvertisedIP func(bool)
75 |
76 | UseAdvertisedIPForWebDAV bool
77 | SetUseAdvertisedIPForWebDAV func(bool)
78 |
79 | SetUseHTTPS func(bool)
80 | SetUseDavs func(bool)
81 |
82 | // Internal state
83 | aboutDialogOpen bool
84 | notificationsDrawerOpen bool
85 | overflowMenuExpanded bool
86 | userMenuExpanded bool
87 | }
88 |
89 | func (c *DataShell) Render() app.UI {
90 | // Gather notifications
91 | notifications := []Notification{}
92 | for _, event := range c.Events {
93 | notifications = append(notifications, Notification{
94 | CreatedAt: event.CreatedAt.String(),
95 | Message: event.Message,
96 | })
97 | }
98 |
99 | // Reduce errors to global error
100 | globalError := c.FileExplorerError
101 | if c.EventsError != nil {
102 | globalError = c.EventsError
103 | }
104 |
105 | recoverGlobalError := c.RecoverFileExplorerError
106 | if c.EventsError != nil {
107 | recoverGlobalError = c.RecoverEventsError
108 | }
109 |
110 | ignoreGlobalError := c.IgnoreFileExplorerError
111 | if c.EventsError != nil {
112 | ignoreGlobalError = c.IgnoreEventsError
113 | }
114 |
115 | return app.Div().
116 | Class("pf-v6-u-h-100").
117 | Body(
118 | app.Div().
119 | Class("pf-v6-c-page").
120 | ID("page-layout-horizontal-nav").
121 | Body(
122 | app.A().
123 | Class("pf-v6-c-skip-to-content pf-v6-c-button pf-m-primary").
124 | Href("#main-content-page-layout-horizontal-nav").
125 | Text(
126 | "Skip to content",
127 | ),
128 | &Navbar{
129 | NotificationsDrawerOpen: c.notificationsDrawerOpen,
130 | ToggleNotificationsDrawerOpen: func() {
131 | c.notificationsDrawerOpen = !c.notificationsDrawerOpen
132 | c.overflowMenuExpanded = false
133 | },
134 |
135 | ToggleAbout: func() {
136 | c.aboutDialogOpen = true
137 | c.overflowMenuExpanded = false
138 | },
139 |
140 | OverflowMenuExpanded: c.overflowMenuExpanded,
141 | ToggleOverflowMenuExpanded: func() {
142 | c.overflowMenuExpanded = !c.overflowMenuExpanded
143 | c.userMenuExpanded = false
144 | },
145 |
146 | UserMenuExpanded: c.userMenuExpanded,
147 | ToggleUserMenuExpanded: func() {
148 | c.userMenuExpanded = !c.userMenuExpanded
149 | c.overflowMenuExpanded = false
150 | },
151 |
152 | UserEmail: c.UserInfo.Email,
153 | Logout: func(ctx app.Context) {
154 | c.Logout(ctx)
155 | },
156 | },
157 | app.Div().
158 | Class("pf-v6-c-page__drawer").
159 | Body(
160 | app.Div().
161 | Class(func() string {
162 | classes := "pf-v6-c-drawer"
163 |
164 | if c.notificationsDrawerOpen {
165 | classes += " pf-m-expanded"
166 | }
167 |
168 | return classes
169 | }()).
170 | Body(
171 | app.Div().
172 | Class("pf-v6-c-drawer__main").
173 | Body(
174 | app.Div().
175 | Class("pf-v6-c-drawer__content").
176 | Body(
177 | app.Div().
178 | Class("pf-v6-c-page__main-container").
179 | TabIndex(-1).
180 | Body(
181 | app.Div().Class("pf-v6-c-drawer__body").Body(
182 | app.Main().
183 | Class("pf-v6-c-page__main pf-v6-u-h-100").
184 | ID("main-content-page-layout-horizontal-nav").
185 | TabIndex(-1).
186 | Body(
187 | app.Section().
188 | Class("pf-v6-c-page__main-section").
189 | Body(
190 | app.Div().
191 | Class("pf-v6-l-grid pf-m-gutter pf-v6-u-h-100").
192 | Body(
193 | app.Div().
194 | Class("pf-v6-l-grid__item pf-m-12-col pf-m-12-col-on-md pf-m-5-col-on-xl").
195 | Body(
196 | &TextEditorWrapper{
197 | Title: "Config",
198 |
199 | HelpLink: "https://github.com/pojntfx/bofied#config-script",
200 |
201 | Error: c.ConfigFileError,
202 | ErrorDescription: "Syntax Error",
203 | Ignore: c.IgnoreConfigFileError,
204 |
205 | Children: &TextEditor{
206 | Content: c.ConfigFile,
207 | SetContent: c.SetConfigFile,
208 |
209 | Format: c.FormatConfigFile,
210 | Refresh: c.RefreshConfigFile,
211 | Save: c.SaveConfigFile,
212 |
213 | Language: "Go",
214 | },
215 | },
216 | ),
217 |
218 | app.Div().
219 | Class("pf-v6-l-grid__item pf-m-12-col pf-m-12-col-on-md pf-m-7-col-on-xl").
220 | Body(
221 | &FileExplorer{
222 | CurrentPath: c.CurrentPath,
223 | SetCurrentPath: c.SetCurrentPath,
224 |
225 | Index: c.Index,
226 | RefreshIndex: c.RefreshIndex,
227 | WriteToPath: c.WriteToPath,
228 |
229 | HTTPShareLink: c.HTTPShareLink,
230 | TFTPShareLink: c.TFTPShareLink,
231 | SharePath: c.SharePath,
232 |
233 | CreatePath: c.CreatePath,
234 | CreateEmptyFile: c.CreateEmptyFile,
235 | DeletePath: c.DeletePath,
236 | MovePath: c.MovePath,
237 | CopyPath: c.CopyPath,
238 |
239 | EditPathContents: c.EditPathContents,
240 | SetEditPathContents: c.SetEditPathContents,
241 | EditPath: c.EditPath,
242 |
243 | WebDAVAddress: c.WebDAVAddress,
244 | WebDAVUsername: c.WebDAVUsername,
245 | WebDAVPassword: c.WebDAVPassword,
246 |
247 | OperationIndex: c.OperationIndex,
248 |
249 | OperationCurrentPath: c.OperationCurrentPath,
250 | OperationSetCurrentPath: c.OperationSetCurrentPath,
251 |
252 | UseAdvertisedIP: c.UseAdvertisedIP,
253 | SetUseAdvertisedIP: c.SetUseAdvertisedIP,
254 |
255 | UseAdvertisedIPForWebDAV: c.UseAdvertisedIPForWebDAV,
256 | SetUseAdvertisedIPForWebDAV: c.SetUseAdvertisedIPForWebDAV,
257 |
258 | SetUseHTTPS: c.SetUseHTTPS,
259 | SetUseDavs: c.SetUseDavs,
260 |
261 | Nested: true,
262 |
263 | GetContentType: func(fi os.FileInfo) string {
264 | return fi.(gowebdav.File).ContentType()
265 | },
266 | },
267 | ),
268 | ),
269 | ),
270 | ),
271 | ),
272 | ),
273 | ),
274 | app.Div().
275 | Class("pf-v6-c-drawer__panel").
276 | Body(
277 | app.Div().
278 | Class("pf-v6-c-drawer__body pf-m-no-padding").
279 | Body(
280 | &NotificationDrawer{
281 | Notifications: notifications,
282 | EmptyState: app.Div().
283 | Class("pf-v6-c-empty-state").
284 | Body(
285 | app.Div().
286 | Class("pf-v6-c-empty-state__content").
287 | Body(
288 | app.Div().
289 | Class("pf-v6-c-empty-state__header").
290 | Body(
291 | app.I().
292 | Class("fas fa-inbox pf-v6-c-empty-state__icon").
293 | Aria("hidden", true),
294 | app.Div().
295 | Class("pf-v6-c-empty-state__title").Body(
296 | app.H2().
297 | Class("pf-v6-c-empty-state__title-text").
298 | Text("No events yet"),
299 | ),
300 | ),
301 | app.Div().
302 | Class("pf-v6-c-empty-state__body").
303 | Text("Network boot a node to see events here."),
304 | ),
305 | ),
306 | },
307 | ),
308 | ),
309 | ),
310 | ),
311 | ),
312 |
313 | app.Ul().
314 | Class("pf-v6-c-alert-group pf-m-toast").
315 | Body(
316 | &UpdateNotification{
317 | UpdateTitle: "An update for bofied is available",
318 |
319 | StartUpdateText: "Upgrade now",
320 | IgnoreUpdateText: "Maybe later",
321 | },
322 | app.If(
323 | globalError != nil,
324 | func() app.UI {
325 | return app.Li().
326 | Class("pf-v6-c-alert-group__item").
327 | Body(
328 | &Status{
329 | Error: globalError,
330 | ErrorText: "Fatal Error",
331 | Recover: recoverGlobalError,
332 | RecoverText: "Reconnect",
333 | Ignore: ignoreGlobalError,
334 | },
335 | )
336 | },
337 | ),
338 | ),
339 |
340 | &AboutModal{
341 | Open: c.aboutDialogOpen,
342 | Close: func() {
343 | c.aboutDialogOpen = false
344 | },
345 |
346 | ID: "about-modal-title",
347 |
348 | LogoDarkSrc: "/web/logo-dark.png",
349 | LogoDarkAlt: "bofied Logo (dark variant)",
350 |
351 | LogoLightSrc: "/web/logo-light.png",
352 | LogoLightAlt: "bofied Logo (light variant)",
353 |
354 | Title: "bofied",
355 |
356 | Body: app.Dl().
357 | Body(
358 | app.Dt().Text("Frontend version"),
359 | app.Dd().Text("main"),
360 | app.Dt().Text("Backend version"),
361 | app.Dd().Text("main"),
362 | ),
363 | Footer: "Copyright © 2024 Felicitas Pojtinger and contributors (SPDX-License-Identifier: AGPL-3.0)",
364 | },
365 | ),
366 | )
367 | }
368 |
--------------------------------------------------------------------------------
/pkg/components/empty_state.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import "github.com/maxence-charriere/go-app/v10/pkg/app"
4 |
5 | type EmptyState struct {
6 | app.Compo
7 |
8 | Action app.UI
9 | }
10 |
11 | func (c *EmptyState) Render() app.UI {
12 | return app.Div().
13 | Class("pf-v6-c-empty-state").
14 | Body(
15 | app.Div().
16 | Class("pf-v6-c-empty-state__content").
17 | Body(
18 | app.Div().
19 | Class("pf-v6-c-empty-state__header").
20 | Body(
21 | app.I().
22 | Class("fas fa-folder-open pf-v6-c-empty-state__icon").
23 | Aria("hidden", true),
24 | app.Div().
25 | Class("pf-v6-c-empty-state__title").Body(
26 | app.H2().
27 | Class("pf-v6-c-empty-state__title-text").
28 | Text("No files or directories here yet"),
29 | ),
30 | ),
31 | app.Div().
32 | Class("pf-v6-c-empty-state__body").
33 | Text("You can add a file or directory to make it available for nodes."),
34 | app.If(
35 | c.Action != nil,
36 | func() app.UI {
37 | return app.Div().
38 | Class("pf-v6-c-empty-state__footer").
39 | Body(
40 | app.Div().
41 | Class("pf-v6-c-empty-state__actions").
42 | Body(
43 | c.Action,
44 | ),
45 | )
46 | },
47 | ),
48 | ),
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/components/expandable_section.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import "github.com/maxence-charriere/go-app/v10/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-v6-c-expandable-section"
20 |
21 | if c.Open {
22 | classes += " pf-m-expanded"
23 | }
24 |
25 | return classes
26 | }()).
27 | Body(
28 | app.Div().
29 | Class("pf-v6-c-expandable-section__toggle").
30 | Body(
31 | app.Button().
32 | Type("button").
33 | Class("pf-v6-c-button pf-m-link").
34 | Aria("label", func() string {
35 | message := c.ClosedTitle
36 |
37 | if c.Open {
38 | message = c.OpenTitle
39 | }
40 |
41 | return message
42 | }()).
43 | Aria("expanded", c.Open).
44 | OnClick(func(ctx app.Context, e app.Event) {
45 | c.OnToggle()
46 | }).
47 | Body(
48 | app.Span().
49 | Class("pf-v6-c-button__icon pf-m-start").
50 | Body(
51 | app.I().
52 | Class("fas fa-angle-right").
53 | Aria("hidden", true),
54 | ),
55 | app.Span().
56 | Class("pf-v6-c-button__text").
57 | Text(c.Title),
58 | ),
59 | ),
60 | app.Div().
61 | Class("pf-v6-c-expandable-section__content").
62 | Hidden(!c.Open).
63 | Body(c.Body...),
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/components/file_grid.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "time"
7 |
8 | "github.com/maxence-charriere/go-app/v10/pkg/app"
9 | )
10 |
11 | type FileGrid struct {
12 | app.Compo
13 |
14 | Index []os.FileInfo
15 |
16 | SelectedPath string
17 | SetSelectedPath func(string)
18 |
19 | CurrentPath string
20 | SetCurrentPath func(string)
21 |
22 | Standalone bool
23 |
24 | hasInitiatedClick bool
25 | }
26 |
27 | func (c *FileGrid) Render() app.UI {
28 | return app.Div().
29 | Class(func() string {
30 | classes := "pf-v6-l-grid pf-m-gutter"
31 | if c.Standalone {
32 | classes += " pf-m-all-4-col-on-md pf-m-all-3-col-on-xl pf-v6-u-py-md"
33 | } else {
34 | classes += " pf-m-all-4-col-on-sm pf-m-all-4-col-on-md pf-m-all-3-col-on-lg pf-m-all-3-col-on-xl"
35 | }
36 |
37 | return classes
38 | }()).
39 | Body(
40 | app.Range(c.Index).Slice(func(i int) app.UI {
41 | selectCard := func() {
42 | newSelectedPath := filepath.Join(c.CurrentPath, c.Index[i].Name())
43 | if c.SelectedPath == newSelectedPath {
44 | // Handle double click
45 | if c.hasInitiatedClick && c.Index[i].IsDir() {
46 | c.SetCurrentPath(filepath.Join(c.CurrentPath, c.Index[i].Name()))
47 |
48 | c.SetSelectedPath("")
49 |
50 | return
51 | }
52 |
53 | newSelectedPath = ""
54 | }
55 |
56 | c.SetSelectedPath(newSelectedPath)
57 |
58 | // Prepare for double click
59 | c.hasInitiatedClick = true
60 | time.AfterFunc(time.Second, func() {
61 | c.hasInitiatedClick = false
62 | })
63 | }
64 |
65 | return app.Div().
66 | Class("pf-v6-l-grid__item pf-v6-u-text-align-center").
67 | Body(
68 | app.Div().
69 | Class(
70 | func() string {
71 | classes := "pf-v6-c-card pf-m-plain pf-m-selectable"
72 | if c.SelectedPath == filepath.Join(c.CurrentPath, c.Index[i].Name()) {
73 | classes += " pf-m-selected"
74 | }
75 |
76 | return classes
77 | }()).
78 | On("keyup", func(ctx app.Context, e app.Event) {
79 | if e.Get("key").String() == "Enter" || e.Get("key").String() == " " {
80 | selectCard()
81 | }
82 | }).
83 | OnClick(func(ctx app.Context, e app.Event) {
84 | selectCard()
85 | }).
86 | Aria("role", "button").
87 | TabIndex(0).
88 | Body(
89 | app.Div().
90 | Class("pf-v6-c-card__body").
91 | Body(
92 | app.I().
93 | Class(func() string {
94 | classes := "fas pf-v6-u-font-size-3xl"
95 | if c.Index[i].IsDir() {
96 | classes += " fa-folder"
97 | } else {
98 | classes += " fa-file-alt"
99 | }
100 |
101 | return classes
102 | }()).
103 | Aria("hidden", true),
104 | ),
105 | app.Div().
106 | Class("pf-v6-c-card__footer").
107 | Body(
108 | app.Text(c.Index[i].Name()),
109 | ),
110 | ),
111 | )
112 | }),
113 | )
114 | }
115 |
--------------------------------------------------------------------------------
/pkg/components/form_group.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import "github.com/maxence-charriere/go-app/v10/pkg/app"
4 |
5 | type FormGroup struct {
6 | app.Compo
7 |
8 | Required bool
9 | Label app.UI
10 | Input app.UI
11 | NoTopPadding bool
12 | NoControlWrapper bool
13 | }
14 |
15 | func (c *FormGroup) Render() app.UI {
16 | return app.Div().
17 | Class("pf-v6-c-form__group").
18 | Body(
19 | app.Div().
20 | Class(func() string {
21 | classes := "pf-v6-c-form__group-label"
22 | if c.NoTopPadding {
23 | classes += " pf-m-no-padding-top"
24 | }
25 |
26 | return classes
27 | }()).
28 | Body(
29 | c.Label,
30 | app.If(c.Required,
31 | func() app.UI {
32 | return app.Span().
33 | Class("pf-v6-c-form__label-required").
34 | Aria("hidden", true).
35 | Text("*")
36 | },
37 | ),
38 | ),
39 | app.Div().
40 | Class("pf-v6-c-form__group-control").
41 | Body(
42 | app.If(
43 | c.NoControlWrapper,
44 | func() app.UI {
45 | return c.Input
46 | },
47 | ).Else(
48 | func() app.UI {
49 | return app.
50 | Span().
51 | Class("pf-v6-c-form-control").
52 | Body(
53 | c.Input,
54 | )
55 | }),
56 | ),
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/components/home.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "github.com/maxence-charriere/go-app/v10/pkg/app"
5 | "github.com/pojntfx/bofied/pkg/providers"
6 | )
7 |
8 | type Home struct {
9 | app.Compo
10 |
11 | removeEventListener func()
12 | }
13 |
14 | func (c *Home) Render() app.UI {
15 | return &providers.SetupProvider{
16 | StoragePrefix: "bofied.configuration",
17 | StateQueryParameter: "state",
18 | CodeQueryParameter: "code",
19 | Children: func(cpcp providers.SetupProviderChildrenProps) app.UI {
20 | // This div is required so that there are no authorization loops
21 | return app.Div().
22 | Class("pf-v6-x-ws-router").
23 | Body(
24 | app.If(cpcp.Ready,
25 | func() app.UI {
26 | // Identity provider
27 | return &providers.IdentityProvider{
28 | Issuer: cpcp.OIDCIssuer,
29 | ClientID: cpcp.OIDCClientID,
30 | RedirectURL: cpcp.OIDCRedirectURL,
31 | HomeURL: "/",
32 | Scopes: []string{"profile", "email"},
33 | StoragePrefix: "bofied.identity",
34 | Children: func(ipcp providers.IdentityProviderChildrenProps) app.UI {
35 | // Configuration shell
36 | if ipcp.Error != nil {
37 | return &SetupShell{
38 | LogoDarkSrc: "/web/logo-dark.png",
39 | LogoDarkAlt: "bofied Logo (dark variant)",
40 |
41 | LogoLightSrc: "/web/logo-light.png",
42 | LogoLightAlt: "bofied Logo (light variant)",
43 |
44 | Title: "Log in to bofied",
45 | ShortDescription: "Modern network boot server.",
46 | LongDescription: `bofied is a network boot server. It provides everything you need to PXE boot a node, from a (proxy)DHCP server for PXE service to a TFTP and HTTP server to serve boot files.`,
47 | HelpLink: "https://github.com/pojntfx/bofied#tutorial",
48 | Links: map[string]string{
49 | "License": "https://github.com/pojntfx/bofied/blob/main/LICENSE",
50 | "Source Code": "https://github.com/pojntfx/bofied",
51 | "Documentation": "https://github.com/pojntfx/bofied#tutorial",
52 | },
53 |
54 | BackendURL: cpcp.BackendURL,
55 | OIDCIssuer: cpcp.OIDCIssuer,
56 | OIDCClientID: cpcp.OIDCClientID,
57 | OIDCRedirectURL: cpcp.OIDCRedirectURL,
58 |
59 | SetBackendURL: cpcp.SetBackendURL,
60 | SetOIDCIssuer: cpcp.SetOIDCIssuer,
61 | SetOIDCClientID: cpcp.SetOIDCClientID,
62 | SetOIDCRedirectURL: cpcp.SetOIDCRedirectURL,
63 | ApplyConfig: cpcp.ApplyConfig,
64 |
65 | Error: ipcp.Error,
66 | }
67 | }
68 |
69 | // Configuration placeholder
70 | if ipcp.IDToken == "" || ipcp.UserInfo.Email == "" {
71 | return app.P().Text("Authorizing ...")
72 | }
73 |
74 | // Data provider
75 | return &providers.DataProvider{
76 | BackendURL: cpcp.BackendURL,
77 | IDToken: ipcp.IDToken,
78 | Children: func(dpcp providers.DataProviderChildrenProps) app.UI {
79 | // Data shell
80 | return &DataShell{
81 | // Config file editor
82 | ConfigFile: dpcp.ConfigFile,
83 | SetConfigFile: dpcp.SetConfigFile,
84 |
85 | FormatConfigFile: dpcp.FormatConfigFile,
86 | RefreshConfigFile: dpcp.RefreshConfigFile,
87 | SaveConfigFile: dpcp.SaveConfigFile,
88 |
89 | ConfigFileError: dpcp.ConfigFileError,
90 | IgnoreConfigFileError: dpcp.IgnoreConfigFileError,
91 |
92 | // File explorer
93 | CurrentPath: dpcp.CurrentPath,
94 | SetCurrentPath: dpcp.SetCurrentPath,
95 |
96 | Index: dpcp.Index,
97 | RefreshIndex: dpcp.RefreshIndex,
98 | WriteToPath: dpcp.WriteToPath,
99 |
100 | HTTPShareLink: dpcp.HTTPShareLink,
101 | TFTPShareLink: dpcp.TFTPShareLink,
102 | SharePath: dpcp.SharePath,
103 |
104 | CreatePath: dpcp.CreatePath,
105 | CreateEmptyFile: dpcp.CreateEmptyFile,
106 | DeletePath: dpcp.DeletePath,
107 | MovePath: dpcp.MovePath,
108 | CopyPath: dpcp.CopyPath,
109 |
110 | EditPathContents: dpcp.EditPathContents,
111 | SetEditPathContents: dpcp.SetEditPathContents,
112 | EditPath: dpcp.EditPath,
113 |
114 | WebDAVAddress: dpcp.WebDAVAddress,
115 | WebDAVUsername: dpcp.WebDAVUsername,
116 | WebDAVPassword: dpcp.WebDAVPassword,
117 |
118 | OperationIndex: dpcp.OperationIndex,
119 |
120 | OperationCurrentPath: dpcp.OperationCurrentPath,
121 | OperationSetCurrentPath: dpcp.OperationSetCurrentPath,
122 |
123 | FileExplorerError: dpcp.FileExplorerError,
124 | RecoverFileExplorerError: dpcp.RecoverFileExplorerError,
125 | IgnoreFileExplorerError: dpcp.IgnoreFileExplorerError,
126 |
127 | Events: dpcp.Events,
128 |
129 | EventsError: dpcp.EventsError,
130 | RecoverEventsError: dpcp.RecoverEventsError,
131 | IgnoreEventsError: dpcp.IgnoreEventsError,
132 |
133 | UserInfo: ipcp.UserInfo,
134 | Logout: ipcp.Logout,
135 |
136 | // Metadata
137 | UseAdvertisedIP: dpcp.UseAdvertisedIP,
138 | SetUseAdvertisedIP: dpcp.SetUseAdvertisedIP,
139 |
140 | UseAdvertisedIPForWebDAV: dpcp.UseAdvertisedIPForWebDAV,
141 | SetUseAdvertisedIPForWebDAV: dpcp.SetUseAdvertisedIPForWebDAV,
142 |
143 | SetUseHTTPS: dpcp.SetUseHTTPS,
144 | SetUseDavs: dpcp.SetUseDavs,
145 | }
146 | },
147 | }
148 | },
149 | }
150 | },
151 | ).Else(
152 | func() app.UI {
153 | // Configuration shell
154 | return &SetupShell{
155 | LogoDarkSrc: "/web/logo-dark.png",
156 | LogoDarkAlt: "bofied Logo (dark variant)",
157 |
158 | LogoLightSrc: "/web/logo-light.png",
159 | LogoLightAlt: "bofied Logo (light variant)",
160 |
161 | Title: "Log in to bofied",
162 | ShortDescription: "Modern network boot server.",
163 | LongDescription: `bofied is a network boot server. It provides everything you need to PXE boot a node, from a (proxy)DHCP server for PXE service to a TFTP and HTTP server to serve boot files.`,
164 | HelpLink: "https://github.com/pojntfx/bofied#tutorial",
165 | Links: map[string]string{
166 | "License": "https://github.com/pojntfx/bofied/blob/main/LICENSE",
167 | "Source Code": "https://github.com/pojntfx/bofied",
168 | "Documentation": "https://github.com/pojntfx/bofied#tutorial",
169 | },
170 |
171 | BackendURL: cpcp.BackendURL,
172 | OIDCIssuer: cpcp.OIDCIssuer,
173 | OIDCClientID: cpcp.OIDCClientID,
174 | OIDCRedirectURL: cpcp.OIDCRedirectURL,
175 |
176 | SetBackendURL: cpcp.SetBackendURL,
177 | SetOIDCIssuer: cpcp.SetOIDCIssuer,
178 | SetOIDCClientID: cpcp.SetOIDCClientID,
179 | SetOIDCRedirectURL: cpcp.SetOIDCRedirectURL,
180 | ApplyConfig: cpcp.ApplyConfig,
181 |
182 | Error: cpcp.Error,
183 | }
184 | },
185 | ),
186 | )
187 | },
188 | }
189 | }
190 |
191 | func (c *Home) OnMount(ctx app.Context) {
192 | darkModeMediaQuery := app.Window().Call("matchMedia", "(prefers-color-scheme: dark)")
193 |
194 | updateTheme := func() {
195 | app.
196 | Window().
197 | Get("document").
198 | Get("documentElement").
199 | Get("classList").
200 | Call("toggle", "pf-v6-theme-dark", darkModeMediaQuery.Get("matches"))
201 | }
202 |
203 | updateThemeEventListener := app.FuncOf(func(this app.Value, args []app.Value) any {
204 | updateTheme()
205 |
206 | return nil
207 | })
208 |
209 | darkModeMediaQuery.Call(
210 | "addEventListener",
211 | "change",
212 | updateThemeEventListener,
213 | )
214 |
215 | c.removeEventListener = func() {
216 | darkModeMediaQuery.Call(
217 | "removeEventListener",
218 | "change",
219 | updateThemeEventListener,
220 | )
221 | }
222 |
223 | updateTheme()
224 | }
225 |
226 | func (c *Home) OnDismount() {
227 | if c.removeEventListener != nil {
228 | c.removeEventListener()
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/pkg/components/modal.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "github.com/maxence-charriere/go-app/v10/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 | Large bool
21 | PaddedBottom bool
22 | Overlay bool
23 |
24 | Nested bool
25 | }
26 |
27 | func (c *Modal) Render() app.UI {
28 | return app.Div().
29 | Class(func() string {
30 | classes := "pf-v6-c-backdrop"
31 |
32 | if c.Classes != "" {
33 | classes += " " + c.Classes
34 | }
35 |
36 | if !c.Open {
37 | classes += " pf-v6-u-display-none"
38 | }
39 |
40 | if c.Overlay {
41 | classes += " pf-v6-x-m-modal-overlay"
42 | }
43 |
44 | if c.Nested {
45 | classes += " pf-v6-x-c-backdrop--nested"
46 | }
47 |
48 | return classes
49 | }()).
50 | Body(
51 | app.Div().
52 | Class("pf-v6-l-bullseye").
53 | Body(
54 | app.Div().
55 | Class(func() string {
56 | classes := "pf-v6-c-modal-box"
57 | if c.Large {
58 | classes += " pf-m-lg"
59 | } else {
60 | classes += " pf-m-sm"
61 | }
62 |
63 | return classes
64 | }()).
65 | Aria("modal", true).
66 | Aria("labelledby", c.ID).
67 | Body(
68 | app.Div().
69 | Class("pf-v6-c-modal-box__close").
70 | Body(
71 | app.Button().
72 | Class("pf-v6-c-button pf-m-plain").
73 | Type("button").
74 | Aria("label", "Close dialog").
75 | OnClick(func(ctx app.Context, e app.Event) {
76 | c.Close()
77 | }).
78 | Body(
79 | app.Span().
80 | Class("pf-v6-c-button__icon").
81 | Body(
82 | app.I().
83 | Class("fas fa-times").
84 | Aria("hidden", true),
85 | ),
86 | ),
87 | ),
88 | app.Header().
89 | Class("pf-v6-c-modal-box__header").
90 | Body(
91 | app.H1().
92 | Class("pf-v6-c-modal-box__title").
93 | ID(c.ID).
94 | Text(c.Title),
95 | ),
96 | app.Div().
97 | Class(func() string {
98 | classes := "pf-v6-c-modal-box__body"
99 | if c.PaddedBottom {
100 | classes += " pf-v6-u-pb-md"
101 | }
102 |
103 | return classes
104 | }()).
105 | Body(c.Body...),
106 | app.If(
107 | c.Footer != nil,
108 | func() app.UI {
109 | return app.Footer().
110 | Class("pf-v6-c-modal-box__footer").
111 | Body(c.Footer...)
112 | },
113 | ),
114 | ),
115 | ),
116 | )
117 | }
118 |
119 | func (c *Modal) OnMount(ctx app.Context) {
120 | app.Window().Call("addEventListener", "keyup", app.FuncOf(func(this app.Value, args []app.Value) any {
121 | ctx.Async(func() {
122 | if len(args) > 0 && args[0].Get("key").String() == "Escape" {
123 | c.Close()
124 |
125 | ctx.Update()
126 | }
127 | })
128 |
129 | return nil
130 | }))
131 | }
132 |
--------------------------------------------------------------------------------
/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/v10/pkg/app"
9 | )
10 |
11 | type Navbar struct {
12 | app.Compo
13 |
14 | NotificationsDrawerOpen bool
15 | ToggleNotificationsDrawerOpen func()
16 |
17 | ToggleAbout func()
18 |
19 | OverflowMenuExpanded bool
20 | ToggleOverflowMenuExpanded func()
21 |
22 | UserMenuExpanded bool
23 | ToggleUserMenuExpanded func()
24 |
25 | UserEmail string
26 | Logout func(app.Context)
27 | }
28 |
29 | func (c *Navbar) Render() app.UI {
30 | // Get the MD5 hash for the user's gravatar
31 | avatarHash := md5.Sum([]byte(c.UserEmail))
32 |
33 | return app.Header().
34 | Class("pf-v6-c-masthead pf-m-display-inline").
35 | Body(
36 | app.Div().
37 | Class("pf-v6-c-masthead__main").
38 | Body(
39 | app.Div().
40 | Class("pf-v6-c-masthead__brand").
41 | Body(
42 | app.Img().
43 | Class("pf-v6-c-brand pf-v6-x-c-brand--nav pf-v6-c-brand--dark").
44 | Src("/web/logo-dark.png").
45 | Alt("bofied Logo (dark variant)"),
46 |
47 | app.Img().
48 | Class("pf-v6-c-brand pf-v6-x-c-brand--nav pf-v6-c-brand--light").
49 | Src("/web/logo-light.png").
50 | Alt("bofied Logo (light variant)"),
51 | ),
52 | ),
53 | app.Div().
54 | Class("pf-v6-c-masthead__content").
55 | Body(
56 | app.Div().
57 | Class("pf-v6-c-toolbar").
58 | Body(
59 | app.Div().
60 | Class("pf-v6-c-toolbar__content").
61 | Body(
62 | app.Div().
63 | Class("pf-v6-c-toolbar__content-section pf-v6-u-align-items-center").
64 | Body(
65 | app.Div().
66 | Class("pf-v6-c-toolbar__group pf-m-align-end pf-v6-u-align-items-center").
67 | Body(
68 | app.Div().
69 | Class(func() string {
70 | classes := "pf-v6-c-toolbar__item"
71 |
72 | if c.NotificationsDrawerOpen {
73 | classes += " pf-m-selected"
74 | }
75 |
76 | return classes
77 | }()).
78 | Body(
79 | app.Button().
80 | Class(
81 | func() string {
82 | classes := "pf-v6-c-button pf-m-plain"
83 |
84 | if c.NotificationsDrawerOpen {
85 | classes += " pf-m-read pf-m-stateful pf-m-clicked pf-m-expanded"
86 | }
87 |
88 | return classes
89 | }()).
90 | Type("button").
91 | Aria("label", "Unread notifications").
92 | Aria("expanded", c.NotificationsDrawerOpen).
93 | OnClick(func(ctx app.Context, e app.Event) {
94 | c.ToggleNotificationsDrawerOpen()
95 | }).
96 | Body(
97 | app.Span().
98 | Class("pf-v6-c-button__icon").
99 | Body(
100 | app.Span().
101 | Class("pf-v6-c-button__icon").
102 | Body(
103 | app.I().
104 | Class("fas fa-bell").
105 | Aria("hidden", true),
106 | ),
107 | ),
108 | ),
109 | ),
110 | app.Div().Class("pf-v6-c-toolbar__item").
111 | Body(
112 | app.Div().
113 | Class(func() string {
114 | classes := "pf-v6-c-dropdown"
115 |
116 | if c.OverflowMenuExpanded {
117 | classes += " pf-m-expanded"
118 | }
119 |
120 | return classes
121 | }()).
122 | Body(
123 | app.Button().
124 | Class("pf-v6-c-menu-toggle pf-m-plain").
125 | Type("button").
126 | Aria("expanded", c.OverflowMenuExpanded).
127 | Aria("label", "Actions").
128 | Body(
129 | app.Span().
130 | Class("pf-v6-c-menu-toggle__text pf-v6-u-display-flex pf-v6-u-display-block-on-md").
131 | Body(
132 | app.Img().
133 | Src(fmt.Sprintf("https://www.gravatar.com/avatar/%v?s=150", hex.EncodeToString(avatarHash[:]))).
134 | Alt("Avatar image of user with email "+c.UserEmail).
135 | Class("pf-v6-c-avatar pf-m-sm pf-v6-u-display-none-on-md"),
136 | app.I().
137 | Class("fas fa-ellipsis-v pf-v6-u-display-none pf-v6-u-display-inline-block-on-md pf-v6-u-display-none-on-lg").
138 | Aria("hidden", true),
139 | app.I().
140 | Class("fas fa-question-circle pf-v6-u-display-none pf-v6-u-display-inline-block-on-lg").
141 | Aria("hidden", true),
142 | ),
143 | ).
144 | OnClick(func(ctx app.Context, e app.Event) {
145 | c.ToggleOverflowMenuExpanded()
146 | }),
147 |
148 | app.Div().
149 | Class("pf-v6-c-menu pf-v6-x-u-position-absolute").
150 | Hidden(!c.OverflowMenuExpanded).
151 | Body(
152 | app.Div().
153 | Class("pf-v6-c-menu__content").
154 | Body(
155 | app.Ul().
156 | Role("menu").
157 | Class("pf-v6-c-menu__list").
158 | Body(
159 | app.Li().
160 | Class("pf-v6-c-menu__list-item").
161 | Role("none").
162 | Body(
163 | app.Button().
164 | Class("pf-v6-c-menu__item").
165 | Type("button").
166 | Aria("role", "menuitem").
167 | Body(
168 | app.Span().
169 | Class("pf-v6-c-menu__item-main").
170 | Body(
171 | app.Span().
172 | Class("pf-v6-c-menu__item-text").
173 | Text("About"),
174 | ),
175 | ).
176 | OnClick(func(ctx app.Context, e app.Event) {
177 | c.ToggleAbout()
178 | }),
179 | ),
180 | app.Li().
181 | Class("pf-v6-c-menu__list-item").
182 | Role("none").
183 | Body(
184 | app.A().
185 | Class("pf-v6-c-menu__item").
186 | Target("_blank").
187 | Href("https://github.com/pojntfx/bofied#tutorial").
188 | Aria("role", "menuitem").
189 | Body(
190 | app.Span().
191 | Class("pf-v6-c-menu__item-main").
192 | Body(
193 | app.Span().
194 | Class("pf-v6-c-menu__item-text").
195 | Text("Documentation"),
196 | ),
197 | ),
198 | ),
199 | app.Li().
200 | Class("pf-v6-c-divider pf-v6-u-display-inherit pf-v6-u-display-none-on-md").
201 | Role("separator"),
202 | app.Li().
203 | Class("pf-v6-c-menu__list-item pf-v6-u-display-inherit pf-v6-u-display-none-on-md").
204 | Role("none").
205 | Body(
206 | app.Button().
207 | Class("pf-v6-c-menu__item").
208 | Type("button").
209 | Aria("role", "menuitem").
210 | Body(
211 | app.Span().
212 | Class("pf-v6-c-menu__item-main").
213 | Body(
214 | app.Span().
215 | Class("pf-v6-c-menu__item-icon").
216 | Body(
217 | app.I().
218 | Class("fas fa-sign-out-alt").
219 | Aria("hidden", true),
220 | ),
221 | app.Span().
222 | Class("pf-v6-c-menu__item-text").
223 | Text("Logout"),
224 | ),
225 | ).
226 | OnClick(func(ctx app.Context, e app.Event) {
227 | c.Logout(ctx)
228 | }),
229 | ),
230 | ),
231 | ),
232 | ),
233 | ),
234 | ),
235 | app.Div().Class("pf-v6-c-toolbar__item pf-m-hidden pf-m-visible-on-md").
236 | Body(
237 | app.Div().
238 | Class(func() string {
239 | classes := "pf-v6-c-dropdown"
240 |
241 | if c.UserMenuExpanded {
242 | classes += " pf-m-expanded"
243 | }
244 |
245 | return classes
246 | }()).
247 | Body(
248 | app.Button().
249 | Class("pf-v6-c-menu-toggle pf-m-plain").
250 | Type("button").
251 | Aria("expanded", c.UserMenuExpanded).
252 | Aria("label", "User actions").
253 | Body(
254 | app.Span().
255 | Class("pf-v6-c-menu-toggle__icon pf-v6-u-display-flex").
256 | Body(
257 | app.Img().
258 | Src(fmt.Sprintf("https://www.gravatar.com/avatar/%v?s=150", hex.EncodeToString(avatarHash[:]))).
259 | Alt("Avatar image of user with email "+c.UserEmail).
260 | Class("pf-v6-c-avatar pf-m-sm"),
261 | ),
262 | app.Span().
263 | Class("pf-v6-c-menu-toggle__text").
264 | Text(c.UserEmail),
265 | app.Span().
266 | Class("pf-v6-c-menu-toggle__controls").
267 | Body(
268 | app.Span().
269 | Class("pf-v6-c-menu-toggle__toggle-icon").
270 | Body(
271 | app.I().
272 | Class("fas fa-caret-down").
273 | Aria("hidden", true),
274 | ),
275 | ),
276 | ).
277 | OnClick(func(ctx app.Context, e app.Event) {
278 | c.ToggleUserMenuExpanded()
279 | }),
280 |
281 | app.Div().
282 | Class("pf-v6-c-menu pf-v6-x-u-position-absolute").
283 | Hidden(!c.UserMenuExpanded).
284 | Body(
285 | app.Div().
286 | Class("pf-v6-c-menu__content").
287 | Body(
288 | app.Ul().
289 | Role("menu").
290 | Class("pf-v6-c-menu__list").
291 | Body(
292 | app.Li().
293 | Class("pf-v6-c-menu__list-item").
294 | Role("none").
295 | Body(
296 | app.Button().
297 | Class("pf-v6-c-menu__item").
298 | Type("button").
299 | Aria("role", "menuitem").
300 | Body(
301 | app.Span().
302 | Class("pf-v6-c-menu__item-main").
303 | Body(
304 | app.Span().
305 | Class("pf-v6-c-menu__item-icon").
306 | Body(
307 | app.I().
308 | Class("fas fa-sign-out-alt").
309 | Aria("hidden", true),
310 | ),
311 | app.Span().
312 | Class("pf-v6-c-menu__item-text").
313 | Text("Logout"),
314 | ),
315 | ).
316 | OnClick(func(ctx app.Context, e app.Event) {
317 | c.Logout(ctx)
318 | }),
319 | ),
320 | ),
321 | ),
322 | ),
323 | ),
324 | ),
325 | ),
326 | ),
327 | ),
328 | ),
329 | ),
330 | )
331 | }
332 |
--------------------------------------------------------------------------------
/pkg/components/notification_drawer.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import "github.com/maxence-charriere/go-app/v10/pkg/app"
4 |
5 | type Notification struct {
6 | Message string
7 | CreatedAt string
8 | }
9 |
10 | type NotificationDrawer struct {
11 | app.Compo
12 |
13 | Notifications []Notification
14 | EmptyState app.UI
15 | }
16 |
17 | func (c *NotificationDrawer) Render() app.UI {
18 | return app.Div().
19 | Class("pf-v6-c-notification-drawer").
20 | Body(
21 | app.Div().
22 | Class("pf-v6-c-notification-drawer__header").
23 | Body(
24 | app.H1().
25 | Class("pf-v6-c-notification-drawer__header-title").
26 | Text("Events"),
27 | ),
28 | app.Div().Class("pf-v6-c-notification-drawer__body").Body(
29 | app.If(
30 | len(c.Notifications) > 0,
31 | func() app.UI {
32 | return app.Ul().Class("pf-v6-c-notification-drawer__list").Body(
33 | app.Range(c.Notifications).Slice(func(i int) app.UI {
34 | return app.Li().Class("pf-v6-c-notification-drawer__list-item pf-m-read pf-m-info").Body(
35 | app.Div().Class("pf-v6-c-notification-drawer__list-item-description").Text(
36 | c.Notifications[len(c.Notifications)-1-i].Message,
37 | ),
38 | app.Div().Class("pf-v6-c-notification-drawer__list-item-timestamp").Text(
39 | c.Notifications[len(c.Notifications)-1-i].CreatedAt,
40 | ),
41 | )
42 | }),
43 | )
44 | },
45 | ).Else(func() app.UI {
46 | return c.EmptyState
47 | }),
48 | ),
49 | )
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/components/path_picker_toolbar.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/maxence-charriere/go-app/v10/pkg/app"
7 | )
8 |
9 | type PathPickerToolbar struct {
10 | app.Compo
11 |
12 | Index []os.FileInfo
13 | RefreshIndex func()
14 |
15 | PathComponents []string
16 |
17 | CurrentPath string
18 | SetCurrentPath func(string)
19 |
20 | SelectedPath string
21 | SetSelectedPath func(string)
22 |
23 | OpenCreateDirectoryModal func()
24 | }
25 |
26 | func (c *PathPickerToolbar) Render() app.UI {
27 | return app.Div().
28 | Class("pf-v6-c-toolbar pf-v6-u-py-0").
29 | Body(
30 | app.Div().
31 | Class("pf-v6-c-toolbar__content pf-v6-x-m-gap-md").
32 | Body(
33 | app.Div().
34 | Class("pf-v6-c-toolbar__content-section pf-v6-u-align-items-center").
35 | Body(
36 | app.Div().
37 | Class("pf-v6-c-toolbar__item pf-m-overflow-menu").
38 | Body(
39 | app.Div().
40 | Class("pf-v6-c-overflow-menu").
41 | Body(
42 | app.Div().
43 | Class("pf-v6-c-overflow-menu__content").
44 | Body(
45 | app.Div().
46 | Class("pf-v6-c-overflow-menu__group pf-m-button-group").
47 | Body(
48 | app.Div().
49 | Class("pf-v6-c-overflow-menu__item").
50 | Body(
51 | &Breadcrumbs{
52 | PathComponents: c.PathComponents,
53 |
54 | CurrentPath: c.CurrentPath,
55 | SetCurrentPath: c.SetCurrentPath,
56 |
57 | SelectedPath: c.SelectedPath,
58 | SetSelectedPath: func(s string) {
59 | c.SetSelectedPath(s)
60 | },
61 | },
62 | ),
63 | ),
64 | ),
65 | ),
66 | ),
67 | app.Div().
68 | Class("pf-v6-c-toolbar__item pf-m-pagination").
69 | Body(
70 | app.Div().
71 | Class("pf-v6-c-pagination pf-m-compact").
72 | Body(
73 | app.Div().
74 | Class("pf-v6-c-pagination pf-m-compact pf-m-compact").
75 | Body(
76 | app.Div().
77 | Class("pf-v6-c-overflow-menu").
78 | Body(
79 | app.Div().
80 | Class("pf-v6-c-overflow-menu__content").
81 | Body(
82 | app.Div().
83 | Class("pf-v6-c-overflow-menu__group pf-m-button-group").
84 | Body(
85 | app.Div().
86 | Class("pf-v6-c-overflow-menu__item").
87 | Body(
88 | app.Button().
89 | Type("button").
90 | Aria("label", "Create directory").
91 | Title("Create directory").
92 | Class("pf-v6-c-button pf-m-plain").
93 | OnClick(func(ctx app.Context, e app.Event) {
94 | c.OpenCreateDirectoryModal()
95 | }).
96 | Body(
97 | app.Span().
98 | Class("pf-v6-c-button__icon").
99 | Body(
100 | app.I().
101 | Class("fas fa-folder-plus").
102 | Aria("hidden", true),
103 | ),
104 | ),
105 | ),
106 | ),
107 | ),
108 | app.Div().
109 | Class("pf-v6-c-divider pf-m-vertical pf-m-inset-md").
110 | Aria("role", "separator"),
111 | app.Div().
112 | Class("pf-v6-c-overflow-menu__group pf-m-button-group").
113 | Body(
114 | app.Div().
115 | Class("pf-v6-c-overflow-menu__item").
116 | Body(
117 | app.Button().
118 | Type("button").
119 | Aria("label", "Refresh").
120 | Title("Refresh").
121 | Class("pf-v6-c-button pf-m-plain").
122 | OnClick(func(ctx app.Context, e app.Event) {
123 | c.RefreshIndex()
124 | }).
125 | Body(
126 | app.Span().
127 | Class("pf-v6-c-button__icon").
128 | Body(
129 | app.I().
130 | Class("fas fas fa-sync").
131 | Aria("hidden", true),
132 | ),
133 | ),
134 | ),
135 | ),
136 | ),
137 | ),
138 | ),
139 | ),
140 | ),
141 | ),
142 | )
143 | }
144 |
--------------------------------------------------------------------------------
/pkg/components/setup_form.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import "github.com/maxence-charriere/go-app/v10/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, app.Context)
13 |
14 | OIDCIssuer string
15 | SetOIDCIssuer func(string, app.Context)
16 |
17 | OIDCClientID string
18 | SetOIDCClientID func(string, app.Context)
19 |
20 | OIDCRedirectURL string
21 | SetOIDCRedirectURL func(string, app.Context)
22 |
23 | Submit func(app.Context)
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 = "http://localhost:15256"
35 | oidcIssuerPlaceholder = "https://pojntfx.eu.auth0.com/"
36 | oidcRedirectURLPlaceholder = "http://localhost:15255/"
37 | )
38 |
39 | func (c *SetupForm) Render() app.UI {
40 | return app.Form().
41 | Class("pf-v6-c-form").
42 | Body(
43 | // Error display
44 | app.If(c.Error != nil, func() app.UI {
45 | return app.P().
46 | Class("pf-v6-c-form__helper-text pf-m-error").
47 | Aria("live", "polite").
48 | Body(
49 | app.Span().
50 | Class("pf-v6-c-form__helper-text-icon").
51 | Body(
52 | app.I().
53 | Class("fas fa-exclamation-circle").
54 | Aria("hidden", true),
55 | ),
56 | app.Text(c.ErrorMessage),
57 | )
58 | },
59 | ),
60 | // Backend URL Input
61 | &FormGroup{
62 | Label: app.
63 | Label().
64 | For(backendURLName).
65 | Class("pf-v6-c-form__label").
66 | Body(
67 | app.
68 | Span().
69 | Class("pf-v6-c-form__label-text").
70 | Text("Backend URL"),
71 | ),
72 | Input: app.
73 | Input().
74 | Name(backendURLName).
75 | ID(backendURLName).
76 | Type("url").
77 | Required(true).
78 | Placeholder(backendURLPlaceholder).
79 | Class("pf-v6-c-form-control").
80 | Aria("invalid", c.Error != nil).
81 | Value(c.BackendURL).
82 | OnInput(func(ctx app.Context, e app.Event) {
83 | c.SetBackendURL(ctx.JSSrc().Get("value").String(), ctx)
84 | }),
85 | Required: true,
86 | },
87 | // OIDC Issuer Input
88 | &FormGroup{
89 | Label: app.
90 | Label().
91 | For(oidcIssuerName).
92 | Class("pf-v6-c-form__label").
93 | Body(
94 | app.
95 | Span().
96 | Class("pf-v6-c-form__label-text").
97 | Text("OIDC Issuer"),
98 | ),
99 | Input: app.
100 | Input().
101 | Name(oidcIssuerName).
102 | ID(oidcIssuerName).
103 | Type("url").
104 | Required(true).
105 | Placeholder(oidcIssuerPlaceholder).
106 | Class("pf-v6-c-form-control").
107 | Aria("invalid", c.Error != nil).
108 | Value(c.OIDCIssuer).
109 | OnInput(func(ctx app.Context, e app.Event) {
110 | c.SetOIDCIssuer(ctx.JSSrc().Get("value").String(), ctx)
111 | }),
112 | Required: true,
113 | },
114 | // OIDC Client ID
115 | &FormGroup{
116 | Label: app.
117 | Label().
118 | For(oidcClientIDName).
119 | Class("pf-v6-c-form__label").
120 | Body(
121 | app.
122 | Span().
123 | Class("pf-v6-c-form__label-text").
124 | Text("OIDC Client ID"),
125 | ),
126 | Input: app.
127 | Input().
128 | Name(oidcClientIDName).
129 | ID(oidcClientIDName).
130 | Type("text").
131 | Required(true).
132 | Class("pf-v6-c-form-control").
133 | Aria("invalid", c.Error != nil).
134 | Value(c.OIDCClientID).
135 | OnInput(func(ctx app.Context, e app.Event) {
136 | c.SetOIDCClientID(ctx.JSSrc().Get("value").String(), ctx)
137 | }),
138 | Required: true,
139 | },
140 | // OIDC Redirect URL
141 | &FormGroup{
142 | Label: app.
143 | Label().
144 | For(oidcRedirectURLName).
145 | Class("pf-v6-c-form__label").
146 | Body(
147 | app.
148 | Span().
149 | Class("pf-v6-c-form__label-text").
150 | Text("OIDC Redirect URL"),
151 | ),
152 | Input: app.
153 | Input().
154 | Name(oidcRedirectURLName).
155 | ID(oidcRedirectURLName).
156 | Type("url").
157 | Required(true).
158 | Placeholder(oidcRedirectURLPlaceholder).
159 | Class("pf-v6-c-form-control").
160 | Aria("invalid", c.Error != nil).
161 | Value(c.OIDCRedirectURL).
162 | OnInput(func(ctx app.Context, e app.Event) {
163 | c.SetOIDCRedirectURL(ctx.JSSrc().Get("value").String(), ctx)
164 | }),
165 | Required: true,
166 | },
167 | // Configuration Apply Trigger
168 | app.Div().
169 | Class("pf-v6-c-form__group pf-m-action").
170 | Body(
171 | app.
172 | Button().
173 | Type("submit").
174 | Class("pf-v6-c-button pf-m-primary pf-m-block").
175 | Text("Log in"),
176 | ),
177 | ).OnSubmit(func(ctx app.Context, e app.Event) {
178 | e.PreventDefault()
179 |
180 | c.Submit(ctx)
181 | })
182 | }
183 |
--------------------------------------------------------------------------------
/pkg/components/setup_shell.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "github.com/maxence-charriere/go-app/v10/pkg/app"
5 | )
6 |
7 | type SetupShell struct {
8 | app.Compo
9 |
10 | LogoDarkSrc string
11 | LogoDarkAlt string
12 |
13 | LogoLightSrc string
14 | LogoLightAlt string
15 |
16 | Title string
17 | ShortDescription string
18 | LongDescription string
19 | HelpLink string
20 | Links map[string]string
21 |
22 | BackendURL string
23 | OIDCIssuer string
24 | OIDCClientID string
25 | OIDCRedirectURL string
26 |
27 | SetBackendURL,
28 | SetOIDCIssuer,
29 | SetOIDCClientID,
30 | SetOIDCRedirectURL func(string, app.Context)
31 | ApplyConfig func(app.Context)
32 |
33 | Error error
34 | }
35 |
36 | func (c *SetupShell) Render() app.UI {
37 | // Display the error message if error != nil
38 | errorMessage := ""
39 | if c.Error != nil {
40 | errorMessage = c.Error.Error()
41 | }
42 |
43 | return app.Div().
44 | Class("pf-v6-u-h-100").
45 | Body(
46 | app.Div().
47 | Class("pf-v6-c-background-image").
48 | Body(
49 | app.Raw(``),
80 | ),
81 | app.Div().
82 | Class("pf-v6-c-login").
83 | Body(
84 | app.Div().
85 | Class("pf-v6-c-login__container").
86 | Body(
87 | app.Header().
88 | Class("pf-v6-c-login__header").
89 | Body(
90 | app.Img().
91 | Class("pf-v6-c-brand pf-v6-x-c-brand--main pf-v6-c-brand--dark").
92 | Src(c.LogoDarkSrc).
93 | Alt(c.LogoDarkAlt),
94 |
95 | app.Img().
96 | Class("pf-v6-c-brand pf-v6-x-c-brand--main pf-v6-c-brand--light").
97 | Src(c.LogoLightSrc).
98 | Alt(c.LogoLightAlt),
99 | ),
100 | app.Main().
101 | Class("pf-v6-c-login__main").
102 | Body(
103 | app.Header().
104 | Class("pf-v6-c-login__main-header").
105 | Body(
106 | app.H1().
107 | Class("pf-v6-c-title pf-m-3xl").
108 | Text(
109 | c.Title,
110 | ),
111 | app.P().
112 | Class("pf-v6-c-login__main-header-desc").
113 | Text(
114 | c.ShortDescription,
115 | ),
116 | ),
117 | app.Div().
118 | Class("pf-v6-c-login__main-body").
119 | Body(
120 | &SetupForm{
121 | Error: c.Error,
122 | ErrorMessage: errorMessage,
123 |
124 | BackendURL: c.BackendURL,
125 | SetBackendURL: c.SetBackendURL,
126 |
127 | OIDCIssuer: c.OIDCIssuer,
128 | SetOIDCIssuer: c.SetOIDCIssuer,
129 |
130 | OIDCClientID: c.OIDCClientID,
131 | SetOIDCClientID: c.SetOIDCClientID,
132 |
133 | OIDCRedirectURL: c.OIDCRedirectURL,
134 | SetOIDCRedirectURL: c.SetOIDCRedirectURL,
135 |
136 | Submit: c.ApplyConfig,
137 | },
138 | ),
139 | app.Footer().
140 | Class("pf-v6-c-login__main-footer").
141 | Body(
142 | app.Div().
143 | Class("pf-v6-c-login__main-footer-band").
144 | Body(
145 | app.P().
146 | Class("pf-v6-c-login__main-footer-band-item").
147 | Body(
148 | app.Text("Not sure what to do? "),
149 | app.A().
150 | Href(c.HelpLink).
151 | Target("_blank").
152 | Text("Get help."),
153 | ),
154 | ),
155 | ),
156 | ),
157 | app.Footer().
158 | Class("pf-v6-c-login__footer").
159 | Body(
160 | app.P().
161 | Text(c.LongDescription),
162 | app.Ul().
163 | Class("pf-v6-c-list pf-m-inline").
164 | Body(
165 | app.Range(c.Links).Map(func(s string) app.UI {
166 | return app.Li().Body(
167 | app.A().
168 | Target("_blank").
169 | Href(c.Links[s]).
170 | Text(s),
171 | )
172 | }),
173 | ),
174 | ),
175 | ),
176 | ),
177 | app.Ul().
178 | Class("pf-v6-c-alert-group pf-m-toast").
179 | Body(
180 | &UpdateNotification{
181 | UpdateTitle: "An update for bofied is available",
182 |
183 | StartUpdateText: "Upgrade now",
184 | IgnoreUpdateText: "Maybe later",
185 | },
186 | ),
187 | )
188 | }
189 |
--------------------------------------------------------------------------------
/pkg/components/status.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import "github.com/maxence-charriere/go-app/v10/pkg/app"
4 |
5 | type Status struct {
6 | app.Compo
7 |
8 | Error error
9 | ErrorText string
10 | Recover func(app.Context)
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, func() app.UI {
23 | return app.Div().
24 | Class("pf-v6-c-alert pf-m-danger").
25 | Aria("label", c.ErrorText).
26 | Body(
27 | app.Div().
28 | Class("pf-v6-c-alert__icon").
29 | Body(
30 | app.I().
31 | Class("fas fa-fw fa-exclamation-circle").
32 | Aria("hidden", true),
33 | ),
34 | app.P().
35 | Class("pf-v6-c-alert__title").
36 | Body(
37 | app.Strong().Body(
38 | app.Span().
39 | Class("pf-screen-reader").
40 | Text(c.ErrorText),
41 | ),
42 | app.Text(c.ErrorText),
43 | ),
44 | app.Div().
45 | Class("pf-v6-c-alert__action").
46 | Body(
47 | app.Button().
48 | Class("pf-v6-c-button pf-m-plain").
49 | Aria("label", "Ignore error").
50 | OnClick(func(ctx app.Context, e app.Event) {
51 | c.Ignore()
52 | }).
53 | Body(
54 | app.Span().
55 | Class("pf-v6-c-button__icon").
56 | Body(
57 | app.I().
58 | Class("fas fa-times").
59 | Aria("hidden", true),
60 | ),
61 | ),
62 | ),
63 | app.Div().
64 | Class("pf-v6-c-alert__description").
65 | Body(
66 | app.P().Body(
67 | app.Code().
68 | Text(errorMessage),
69 | ),
70 | ),
71 | app.If(c.Recover != nil,
72 | func() app.UI {
73 | return app.Div().
74 | Class("pf-v6-c-alert__action-group").
75 | Body(
76 | app.Button().
77 | Class("pf-v6-c-button pf-m-link pf-m-inline").
78 | Type("button").
79 | OnClick(func(ctx app.Context, e app.Event) {
80 | c.Recover(ctx)
81 | }).
82 | Text(c.RecoverText),
83 | )
84 | },
85 | ),
86 | )
87 | }).Else(func() app.UI {
88 | return app.Span()
89 | })
90 | }
91 |
--------------------------------------------------------------------------------
/pkg/components/switch.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "github.com/maxence-charriere/go-app/v10/pkg/app"
5 | )
6 |
7 | type Switch struct {
8 | app.Compo
9 |
10 | ID string
11 |
12 | Open bool
13 | ToggleOpen func()
14 |
15 | OnMessage string
16 | OffMessage string
17 | }
18 |
19 | func (c *Switch) Render() app.UI {
20 | return app.Label().
21 | Class("pf-v6-c-switch").
22 | For(c.ID).
23 | Body(
24 | app.Input().
25 | Class("pf-v6-c-switch__input").
26 | Type("checkbox").
27 | ID(c.ID).
28 | Aria("labelledby", c.ID+"-on").
29 | Name(c.ID).
30 | Checked(c.Open).
31 | OnInput(func(ctx app.Context, e app.Event) {
32 | c.ToggleOpen()
33 | }),
34 | app.Span().
35 | Class("pf-v6-c-switch__toggle"),
36 | app.If(
37 | c.OnMessage != "" && c.Open,
38 | func() app.UI {
39 | return app.Span().
40 | Class("pf-v6-c-switch__label pf-m-on").
41 | ID(c.ID+"-on").
42 | Aria("hidden", true).
43 | Text(c.OnMessage)
44 | },
45 | ),
46 | app.If(
47 | c.OffMessage != "" && !c.Open,
48 | func() app.UI {
49 | return app.Span().
50 | Class("pf-v6-c-switch__label pf-m-off").
51 | ID(c.ID+"-off").
52 | Aria("hidden", true).
53 | Text(c.OffMessage)
54 | },
55 | ),
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/pkg/components/text_editor.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/maxence-charriere/go-app/v10/pkg/app"
7 | )
8 |
9 | type TextEditor struct {
10 | app.Compo
11 |
12 | Content string
13 | SetContent func(string)
14 |
15 | Format func()
16 | Refresh func()
17 | Save func()
18 |
19 | Language string
20 | VariableHeight bool
21 | }
22 |
23 | func (c *TextEditor) Render() app.UI {
24 | return app.Div().
25 | Class(func() string {
26 | classes := "pf-v6-c-code-editor pf-v6-u-h-100 pf-v6-u-display-flex pf-v6-u-flex-direction-column"
27 | if c.SetContent == nil {
28 | classes += " pf-m-read-only"
29 | }
30 |
31 | return classes
32 | }()).
33 | Body(
34 | app.Div().
35 | Class("pf-v6-c-code-editor__header").
36 | Body(
37 | app.Div().
38 | Class("pf-v6-c-code-editor__header-content").
39 | Body(
40 | app.Div().
41 | Class("pf-v6-c-code-editor__controls").
42 | Body(
43 | app.If(
44 | c.Format != nil,
45 | func() app.UI {
46 | return app.Button().
47 | Class("pf-v6-c-button pf-m-plain").
48 | Type("button").
49 | Aria("label", "Format").
50 | Title("Format").
51 | OnClick(func(ctx app.Context, e app.Event) {
52 | c.Format()
53 | }).
54 | Body(
55 | app.Span().
56 | Class("pf-v6-c-button__icon").
57 | Body(
58 | app.I().
59 | Class("fas fa-align-left").
60 | Aria("hidden", true),
61 | ),
62 | )
63 | },
64 | ),
65 | app.If(
66 | c.Refresh != nil,
67 | func() app.UI {
68 | return app.Button().
69 | Class("pf-v6-c-button pf-m-plain").
70 | Type("button").
71 | Aria("label", "Refresh").
72 | Title("Refresh").
73 | OnClick(func(ctx app.Context, e app.Event) {
74 | c.Refresh()
75 | }).
76 | Body(
77 | app.Span().
78 | Class("pf-v6-c-button__icon").
79 | Body(
80 | app.I().
81 | Class("fas fas fa-sync").
82 | Aria("hidden", true),
83 | ),
84 | )
85 | },
86 | ),
87 | app.If(
88 | c.Save != nil,
89 | func() app.UI {
90 | return app.Button().
91 | Class("pf-v6-c-button pf-m-plain").
92 | Type("button").
93 | Aria("label", "Save").
94 | Title("Save").
95 | OnClick(func(ctx app.Context, e app.Event) {
96 | c.Save()
97 | }).
98 | Body(
99 | app.Span().
100 | Class("pf-v6-c-button__icon").
101 | Body(
102 | app.I().
103 | Class("fas fas fa-save").
104 | Aria("hidden", true),
105 | ),
106 | )
107 | },
108 | ),
109 | ),
110 | app.Div().
111 | Class("pf-v6-c-code-editor__header-main"),
112 | ),
113 | app.If(
114 | c.Language != "",
115 | func() app.UI {
116 | return app.Div().
117 | Class("pf-v6-c-code-editor__tab").
118 | Body(
119 | app.Span().
120 | Class("pf-v6-c-code-editor__tab-icon").
121 | Body(
122 | app.I().
123 | Class("fas fa-code").
124 | Aria("hidden", true),
125 | ),
126 | app.Span().
127 | Class("pf-v6-c-code-editor__tab-text").
128 | Text(c.Language),
129 | )
130 | },
131 | ),
132 | ),
133 | app.Textarea().
134 | Class(func() string {
135 | classes := "pf-v6-c-code-editor__main pf-v6-u-w-100 pf-v6-x-u-resize-none pf-v6-u-p-sm pf-v6-u-p-sm pf-v6-u-flex-fill"
136 | if c.VariableHeight {
137 | classes += " pf-v6-x-m-overflow-y-hidden"
138 | }
139 |
140 | return classes
141 | }()).
142 | Rows(func() int {
143 | if c.VariableHeight {
144 | return strings.Count(c.Content, "\n") + 1 // Trailing newline
145 | }
146 |
147 | return 25
148 | }()).
149 | ReadOnly(c.SetContent == nil).
150 | OnInput(func(ctx app.Context, e app.Event) {
151 | c.SetContent(ctx.JSSrc().Get("value").String())
152 | }).
153 | Text(c.Content),
154 | )
155 | }
156 |
--------------------------------------------------------------------------------
/pkg/components/text_editor_wrapper.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import "github.com/maxence-charriere/go-app/v10/pkg/app"
4 |
5 | type TextEditorWrapper struct {
6 | app.Compo
7 |
8 | Title string
9 | HelpLink string
10 |
11 | Children app.UI
12 |
13 | Error error
14 | ErrorDescription string
15 | Ignore func()
16 | }
17 |
18 | func (c *TextEditorWrapper) Render() app.UI {
19 | return app.Div().
20 | Class("pf-v6-c-card pf-m-plain pf-v6-u-h-100").
21 | Body(
22 | app.Div().
23 | Class("pf-v6-c-card__header").
24 | Body(
25 | app.If(
26 | c.HelpLink != "",
27 | func() app.UI {
28 | return app.Div().
29 | Class("pf-v6-c-card__actions").
30 | Body(
31 | app.A().
32 | Class("pf-v6-c-button pf-m-plain").
33 | Aria("label", "Help").
34 | Target("_blank").
35 | Href(c.HelpLink).
36 | Body(
37 | app.Span().
38 | Class("pf-v6-c-menu-toggle__text pf-v6-c-button__icon").
39 | Body(
40 | app.I().
41 | Class("fas fa-question-circle").
42 | Aria("hidden", true),
43 | ),
44 | ),
45 | )
46 | },
47 | ),
48 | app.Div().
49 | Class("pf-v6-c-card__header-main").
50 | Body(
51 | app.Div().
52 | Class("pf-v6-c-card__title").
53 | Body(
54 | app.H2().
55 | Class("pf-v6-c-card__title-text").
56 | Text(c.Title),
57 | ),
58 | ),
59 | ),
60 | app.Div().
61 | Class("pf-v6-c-card__body").
62 | Body(c.Children),
63 | app.If(
64 | c.Error != nil,
65 | func() app.UI {
66 | return app.Div().
67 | Class("pf-v6-c-card__footer").
68 | Body(
69 | app.Div().
70 | Class("pf-v6-c-alert pf-m-danger pf-m-inline").
71 | Aria("label", "Error alert").
72 | Body(
73 | app.Div().
74 | Class("pf-v6-c-alert__icon").
75 | Body(
76 | app.I().
77 | Class("fas fa-fw fa-exclamation-circle").
78 | Aria("hidden", true),
79 | ),
80 | app.P().
81 | Class("pf-v6-c-alert__title").
82 | Body(
83 | app.
84 | Strong().
85 | Body(
86 | app.Span().
87 | Class("pf-screen-reader").
88 | Text(c.ErrorDescription+":"),
89 | app.Text(c.ErrorDescription),
90 | ),
91 | ),
92 | app.Div().
93 | Class("pf-v6-c-alert__action").
94 | Body(
95 | app.Button().
96 | Class("pf-v6-c-button pf-m-plain").
97 | Type("button").
98 | Aria("label", "Button to ignore the error").
99 | OnClick(func(ctx app.Context, e app.Event) {
100 | c.Ignore()
101 | }).
102 | Body(
103 | app.Span().
104 | Class("pf-v6-c-button__icon").
105 | Body(
106 | app.I().
107 | Class("fas fa-times").
108 | Aria("hidden", true),
109 | ),
110 | ),
111 | ),
112 | app.Div().
113 | Class("pf-v6-c-alert__description").
114 | Body(
115 | app.Code().Text(c.Error),
116 | ),
117 | ),
118 | )
119 | },
120 | ),
121 | )
122 | }
123 |
--------------------------------------------------------------------------------
/pkg/components/update_notification.go:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import "github.com/maxence-charriere/go-app/v10/pkg/app"
4 |
5 | type UpdateNotification struct {
6 | app.Compo
7 |
8 | UpdateTitle string
9 | UpdateDescription string
10 |
11 | StartUpdateText string
12 | IgnoreUpdateText string
13 |
14 | updateAvailable bool
15 | updateIgnored bool
16 | }
17 |
18 | func (c *UpdateNotification) Render() app.UI {
19 | return app.If(
20 | c.updateAvailable && !c.updateIgnored,
21 | func() app.UI {
22 | return app.Li().
23 | Class("pf-v6-c-alert-group__item").
24 | Body(
25 | app.Div().
26 | Class("pf-v6-c-alert pf-m-info").
27 | Aria("label", c.UpdateTitle).
28 | Body(
29 | app.Div().
30 | Class("pf-v6-c-alert__icon").
31 | Body(
32 | app.I().
33 | Class("fas fa-fw fa-bell").
34 | Aria("hidden", true),
35 | ),
36 | app.P().
37 | Class("pf-v6-c-alert__title").
38 | Body(
39 | app.Strong().Body(
40 | app.Span().
41 | Class("pf-screen-reader").
42 | Text(c.UpdateTitle),
43 | ),
44 | app.Text(c.UpdateTitle),
45 | ),
46 | app.Div().
47 | Class("pf-v6-c-alert__action").
48 | Body(
49 | app.Button().
50 | Class("pf-v6-c-button pf-m-plain").
51 | Aria("label", c.IgnoreUpdateText).
52 | OnClick(func(ctx app.Context, e app.Event) {
53 | c.updateIgnored = true
54 | }).
55 | Body(
56 | app.Span().
57 | Class("pf-v6-c-button__icon").
58 | Body(
59 | app.I().
60 | Class("fas fa-times").
61 | Aria("hidden", true),
62 | ),
63 | ),
64 | ),
65 | app.If(
66 | c.UpdateDescription != "",
67 | func() app.UI {
68 | return app.Div().
69 | Class("pf-v6-c-alert__description").
70 | Body(
71 | app.P().Text(c.UpdateDescription),
72 | )
73 | },
74 | ),
75 | app.Div().
76 | Class("pf-v6-c-alert__action-group").
77 | Body(
78 | app.Button().
79 | Class("pf-v6-c-button pf-m-link pf-m-inline").
80 | Type("button").
81 | OnClick(func(ctx app.Context, e app.Event) {
82 | ctx.Reload()
83 | }).
84 | Body(
85 | app.Span().Class("pf-v6-c-button__icon pf-m-start").Body(
86 | app.I().Class("fas fas fa-arrow-up").Aria("hidden", true),
87 | ),
88 | app.Text(c.StartUpdateText),
89 | ),
90 | app.Button().
91 | Class("pf-v6-c-button pf-m-link pf-m-inline").
92 | Type("button").
93 | OnClick(func(ctx app.Context, e app.Event) {
94 | c.updateIgnored = true
95 | }).
96 | Body(
97 | app.Span().Class("pf-v6-c-button__icon pf-m-start").Body(
98 | app.I().Class("fas fa-ban").Aria("hidden", true),
99 | ),
100 | app.Text(c.IgnoreUpdateText),
101 | ),
102 | ),
103 | ),
104 | )
105 | },
106 | ).Else(func() app.UI {
107 | return app.Span()
108 | })
109 | }
110 |
111 | func (c *UpdateNotification) OnMount(ctx app.Context) {
112 | c.updateAvailable = ctx.AppUpdateAvailable()
113 | }
114 |
--------------------------------------------------------------------------------
/pkg/config/architecture_types.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import "strconv"
4 |
5 | // See https://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#processor-architecture
6 | func GetNameForArchId(id int) string {
7 | switch id {
8 | case 0x00:
9 | return "x86 BIOS"
10 | case 0x01:
11 | return "NEC/PC98 (DEPRECATED)"
12 | case 0x02:
13 | return "Itanium"
14 | case 0x03:
15 | return "DEC Alpha (DEPRECATED)"
16 | case 0x04:
17 | return "Arc x86 (DEPRECATED)"
18 | case 0x05:
19 | return "Intel Lean Client (DEPRECATED)"
20 | case 0x06:
21 | return "x86 UEFI"
22 | case 0x07:
23 | return "x64 UEFI"
24 | case 0x08:
25 | return "EFI Xscale (DEPRECATED)"
26 | case 0x09:
27 | return "EBC"
28 | case 0x0a:
29 | return "ARM 32-bit UEFI"
30 | case 0x0b:
31 | return "ARM 64-bit UEFI"
32 | case 0x0c:
33 | return "PowerPC Open Firmware"
34 | case 0x0d:
35 | return "PowerPC ePAPR"
36 | case 0x0e:
37 | return "POWER OPAL v3"
38 | case 0x0f:
39 | return "x86 uefi boot from http"
40 | case 0x10:
41 | return "x64 uefi boot from http"
42 | case 0x11:
43 | return "ebc boot from http"
44 | case 0x12:
45 | return "arm uefi 32 boot from http"
46 | case 0x13:
47 | return "arm uefi 64 boot from http"
48 | case 0x14:
49 | return "pc/at bios boot from http"
50 | case 0x15:
51 | return "arm 32 uboot"
52 | case 0x16:
53 | return "arm 64 uboot"
54 | case 0x17:
55 | return "arm uboot 32 boot from http"
56 | case 0x18:
57 | return "arm uboot 64 boot from http"
58 | case 0x19:
59 | return "RISC-V 32-bit UEFI"
60 | case 0x1a:
61 | return "RISC-V 32-bit UEFI boot from http"
62 | case 0x1b:
63 | return "RISC-V 64-bit UEFI"
64 | case 0x1c:
65 | return "RISC-V 64-bit UEFI boot from http"
66 | case 0x1d:
67 | return "RISC-V 128-bit UEFI"
68 | case 0x1e:
69 | return "RISC-V 128-bit UEFI boot from http"
70 | case 0x1f:
71 | return "s390 Basic"
72 | case 0x20:
73 | return "s390 Extended"
74 | case 0x21:
75 | return "MIPS 32-bit UEFI"
76 | case 0x22:
77 | return "MIPS 64-bit UEFI"
78 | case 0x23:
79 | return "Sunway 32-bit UEFI"
80 | case 0x24:
81 | return "Sunway 64-bit UEFI"
82 | default:
83 | return strconv.Itoa(id)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/pkg/config/yaegi.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "io/ioutil"
7 | "net/http"
8 | "os"
9 | "path/filepath"
10 |
11 | "github.com/codeclysm/extract/v3"
12 | "github.com/traefik/yaegi/interp"
13 | "github.com/traefik/yaegi/stdlib"
14 | )
15 |
16 | const (
17 | FilenameFunctionIdentifier = "config.Filename"
18 | ConfigureFunctionIdentifier = "config.Configure"
19 | )
20 |
21 | const initialConfigFileContent = `package config
22 |
23 | import "log"
24 |
25 | func Filename(
26 | ip string,
27 | macAddress string,
28 | arch string,
29 | archID int,
30 | ) string {
31 | log.Println("You did not set up boot files yet!")
32 |
33 | return "changeme"
34 | }
35 |
36 | func Configure() map[string]string {
37 | return map[string]string{
38 | "useStdlib": "true",
39 | }
40 | }
41 | `
42 |
43 | func GetFileName(
44 | configFileLocation string,
45 | ip string,
46 | macAddress string,
47 | arch string,
48 | archID int,
49 | pure bool,
50 | handleOutput func(string),
51 | ) (string, error) {
52 | // Read the config file (we are re-reading each time so that a server restart is unnecessary)
53 | src, err := ioutil.ReadFile(configFileLocation)
54 | if err != nil {
55 | return "", err
56 | }
57 |
58 | // Configure the interpreter
59 | useStdlib := false
60 | {
61 | // Setup stdout/stderr handling
62 | outputReader, outputWriter, err := os.Pipe()
63 | if err != nil {
64 | return "", err
65 | }
66 |
67 | // Start the interpreter (for configuration)
68 | i := interp.New(interp.Options{
69 | Stdout: outputWriter,
70 | Stderr: outputWriter,
71 | })
72 | i.Use(stdlib.Symbols)
73 |
74 | // "Run" the config file, exporting the config function identifier
75 | if _, err := i.Eval(string(src)); err != nil {
76 | return "", err
77 | }
78 |
79 | // Get the config function by it's identifier
80 | v, err := i.Eval(ConfigureFunctionIdentifier)
81 | if err != nil {
82 | return "", err
83 | }
84 |
85 | // Cast the function
86 | configure, ok := v.Interface().(func() map[string]string)
87 | if !ok {
88 | return "", errors.New("could not parse config function: invalid config function signature")
89 | }
90 |
91 | // Run the function
92 | configParameters := configure()
93 | for key, value := range configParameters {
94 | if key == "useStdlib" && value == "true" {
95 | useStdlib = true
96 | }
97 | }
98 |
99 | // Close the output pipe
100 | if err := outputWriter.Close(); err != nil {
101 | return "", err
102 | }
103 |
104 | // Read & handle output
105 | out, err := ioutil.ReadAll(outputReader)
106 | if err != nil {
107 | return "", err
108 | }
109 |
110 | handleOutput(string(out))
111 | }
112 |
113 | // Setup stdout/stderr handling
114 | outputReader, outputWriter, err := os.Pipe()
115 | if err != nil {
116 | return "", err
117 | }
118 |
119 | // Manually prevent stdlib use if set to pure
120 | if pure {
121 | useStdlib = false
122 | }
123 |
124 | // Start the interpreter (for file name)
125 | e := interp.New(interp.Options{
126 | Stdout: outputWriter,
127 | Stderr: outputWriter,
128 | })
129 | if useStdlib {
130 | e.Use(stdlib.Symbols)
131 | }
132 |
133 | // "Run" the config file, exporting the file name function identifier
134 | if _, err := e.Eval(string(src)); err != nil {
135 | return "", err
136 | }
137 |
138 | // Get the file name function by it's identifier
139 | w, err := e.Eval(FilenameFunctionIdentifier)
140 | if err != nil {
141 | return "", err
142 | }
143 |
144 | // Cast the function
145 | getFileName, ok := w.Interface().(func(
146 | ip string,
147 | macAddress string,
148 | arch string,
149 | archID int,
150 | ) string)
151 | if !ok {
152 | return "", errors.New("could not parse file name function: invalid file name function signature")
153 | }
154 |
155 | // Run the function
156 | rv, err := getFileName(
157 | ip,
158 | macAddress,
159 | arch,
160 | archID,
161 | ), nil
162 |
163 | // Close the output pipe
164 | if err := outputWriter.Close(); err != nil {
165 | return "", err
166 | }
167 |
168 | // Read & handle output
169 | out, err := ioutil.ReadAll(outputReader)
170 | if err != nil {
171 | return "", err
172 | }
173 |
174 | handleOutput(string(out))
175 |
176 | return rv, err
177 | }
178 |
179 | func CreateConfigIfNotExists(configFileLocation string) error {
180 | // If config file does not exist, create and write to it
181 | if _, err := os.Stat(configFileLocation); os.IsNotExist(err) {
182 | // Create leading directories
183 | leadingDir, _ := filepath.Split(configFileLocation)
184 | if err := os.MkdirAll(leadingDir, os.ModePerm); err != nil {
185 | return err
186 | }
187 |
188 | // Create file
189 | out, err := os.Create(configFileLocation)
190 | if err != nil {
191 | return err
192 | }
193 | defer out.Close()
194 |
195 | // Write to file
196 | if err := ioutil.WriteFile(configFileLocation, []byte(initialConfigFileContent), os.ModePerm); err != nil {
197 | return err
198 | }
199 |
200 | return nil
201 | }
202 |
203 | return nil
204 | }
205 |
206 | func GetStarterIfNotExists(configFileLocation string, starterURL string, outDir string) error {
207 | // If config file does not exist, get and extract starter
208 | if _, err := os.Stat(configFileLocation); os.IsNotExist(err) {
209 | // Create directory to extract to
210 | if err := os.MkdirAll(outDir, os.ModePerm); err != nil {
211 | return err
212 | }
213 |
214 | // Download .tar.gz
215 | resp, err := http.Get(starterURL)
216 | if err != nil {
217 | return err
218 | }
219 | defer resp.Body.Close()
220 |
221 | // Extract .tar.gz
222 | return extract.Gz(context.Background(), resp.Body, outDir, nil)
223 | }
224 |
225 | return nil
226 | }
227 |
--------------------------------------------------------------------------------
/pkg/constants/authorization.go:
--------------------------------------------------------------------------------
1 | package constants
2 |
3 | const (
4 | OIDCOverBasicAuthUsername = "user"
5 | )
6 |
--------------------------------------------------------------------------------
/pkg/constants/config.go:
--------------------------------------------------------------------------------
1 | package constants
2 |
3 | const (
4 | BootConfigFileName = "config.go"
5 | )
6 |
--------------------------------------------------------------------------------
/pkg/eventing/event.go:
--------------------------------------------------------------------------------
1 | package eventing
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "time"
7 |
8 | "github.com/ugjka/messenger"
9 | )
10 |
11 | type Event struct {
12 | CreatedAt time.Time
13 | Message string
14 | }
15 |
16 | type EventHandler struct {
17 | messenger *messenger.Messenger
18 | }
19 |
20 | func NewEventHandler() *EventHandler {
21 | return &EventHandler{
22 | messenger: messenger.New(0, true),
23 | }
24 | }
25 |
26 | func (h *EventHandler) Emit(s string, v ...interface{}) {
27 | // Construct the description
28 | msg := fmt.Sprintf(s, v...)
29 |
30 | // Log the emitted description
31 | log.Println(msg)
32 |
33 | // Broadcast the description
34 | h.messenger.Broadcast(Event{
35 | CreatedAt: time.Now(),
36 | Message: msg,
37 | })
38 | }
39 |
40 | // Proxy to internal messenger
41 | func (h *EventHandler) Sub() (client chan interface{}, err error) {
42 | return h.messenger.Sub()
43 | }
44 |
45 | // Proxy to internal messenger
46 | func (h *EventHandler) Unsub(client chan interface{}) {
47 | h.messenger.Unsub(client)
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/eventing/http_logging.go:
--------------------------------------------------------------------------------
1 | package eventing
2 |
3 | import (
4 | "net/http"
5 | )
6 |
7 | func LogRequestHandler(h http.Handler, eventHandler *EventHandler) http.Handler {
8 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
9 | if r.Method == http.MethodGet {
10 | eventHandler.Emit(`sending file "%v" to client "%v" with user agent "%v"`, r.URL.Path, r.RemoteAddr, r.UserAgent())
11 | }
12 |
13 | h.ServeHTTP(w, r)
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/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/v10/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(ctx app.Context)
29 |
30 | Error error
31 | Recover func(ctx app.Context)
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(ctx app.Context) {
59 | c.logout(true, ctx)
60 | },
61 |
62 | Error: c.err,
63 | Recover: c.recover,
64 | },
65 | )
66 | }
67 |
68 | func (c *IdentityProvider) OnMount(ctx app.Context) {
69 | // Only continue if there is no error state; this prevents endless loops
70 | if c.err == nil {
71 | c.authorize(ctx)
72 | }
73 | }
74 |
75 | func (c *IdentityProvider) OnNav(ctx app.Context) {
76 | // Only continue if there is no error state; this prevents endless loops
77 | if c.err == nil {
78 | c.authorize(ctx)
79 | }
80 | }
81 |
82 | func (c *IdentityProvider) panic(err error, ctx app.Context) {
83 | go func() {
84 | // Set the error
85 | c.err = err
86 |
87 | // Prevent infinite retries
88 | time.Sleep(time.Second)
89 |
90 | // Unset the error & enable re-trying
91 | c.err = err
92 | }()
93 | }
94 |
95 | func (c *IdentityProvider) recover(ctx app.Context) {
96 | // Clear the error
97 | c.err = nil
98 |
99 | // Logout
100 | c.logout(false, ctx)
101 | }
102 |
103 | func (c *IdentityProvider) watch(ctx app.Context) {
104 | for {
105 | // Wait till token expires
106 | if c.oauth2Token.Expiry.After(time.Now()) {
107 | time.Sleep(c.oauth2Token.Expiry.Sub(time.Now()))
108 | }
109 |
110 | // Fetch new OAuth2 token
111 | oauth2Token, err := oauth2.StaticTokenSource(&c.oauth2Token).Token()
112 | if err != nil {
113 | c.panic(err, ctx)
114 |
115 | return
116 | }
117 |
118 | // Parse ID token
119 | idToken, ok := oauth2Token.Extra("id_token").(string)
120 | if !ok {
121 | c.panic(err, ctx)
122 |
123 | return
124 | }
125 |
126 | // Persist state in storage
127 | if err := c.persist(*oauth2Token, idToken, c.userInfo, ctx); err != nil {
128 | c.panic(err, ctx)
129 |
130 | return
131 | }
132 |
133 | // Set the login state
134 | c.oauth2Token = *oauth2Token
135 | c.idToken = idToken
136 | }
137 | }
138 |
139 | func (c *IdentityProvider) logout(withRedirect bool, ctx app.Context) {
140 | // Remove from storage
141 | c.clear(ctx)
142 |
143 | // Reload the app
144 | if withRedirect {
145 | ctx.Reload()
146 | }
147 | }
148 |
149 | func (c *IdentityProvider) rehydrate(ctx app.Context) (oauth2.Token, string, oidc.UserInfo, error) {
150 | // Read state from storage
151 | oauth2Token := oauth2.Token{}
152 | idToken := ""
153 | userInfo := oidc.UserInfo{}
154 |
155 | if err := ctx.LocalStorage().Get(c.getKey(oauth2TokenKey), &oauth2Token); err != nil {
156 | return oauth2.Token{}, "", oidc.UserInfo{}, err
157 | }
158 | if err := ctx.LocalStorage().Get(c.getKey(idTokenKey), &idToken); err != nil {
159 | return oauth2.Token{}, "", oidc.UserInfo{}, err
160 | }
161 | if err := ctx.LocalStorage().Get(c.getKey(userInfoKey), &userInfo); err != nil {
162 | return oauth2.Token{}, "", oidc.UserInfo{}, err
163 | }
164 |
165 | return oauth2Token, idToken, userInfo, nil
166 | }
167 |
168 | func (c *IdentityProvider) persist(oauth2Token oauth2.Token, idToken string, userInfo oidc.UserInfo, ctx app.Context) error {
169 | // Write state to storage
170 | if err := ctx.LocalStorage().Set(c.getKey(oauth2TokenKey), oauth2Token); err != nil {
171 | return err
172 | }
173 | if err := ctx.LocalStorage().Set(c.getKey(idTokenKey), idToken); err != nil {
174 | return err
175 | }
176 | return ctx.LocalStorage().Set(c.getKey(userInfoKey), userInfo)
177 | }
178 |
179 | func (c *IdentityProvider) clear(ctx app.Context) {
180 | // Remove from storage
181 | ctx.LocalStorage().Del(c.getKey(oauth2TokenKey))
182 | ctx.LocalStorage().Del(c.getKey(idTokenKey))
183 | ctx.LocalStorage().Del(c.getKey(userInfoKey))
184 |
185 | // Remove cookies
186 | app.Window().Get("document").Set("cookie", "")
187 | }
188 |
189 | func (c *IdentityProvider) getKey(key string) string {
190 | // Get a prefixed key
191 | return fmt.Sprintf("%v.%v", c.StoragePrefix, key)
192 | }
193 |
194 | func (c *IdentityProvider) authorize(ctx app.Context) {
195 | // Read state from storage
196 | oauth2Token, idToken, userInfo, err := c.rehydrate(ctx)
197 | if err != nil {
198 | c.panic(err, ctx)
199 |
200 | return
201 | }
202 |
203 | // Create the OIDC provider
204 | provider, err := oidc.NewProvider(context.Background(), c.Issuer)
205 | if err != nil {
206 | c.panic(err, ctx)
207 |
208 | return
209 | }
210 |
211 | // Create the OAuth2 config
212 | config := &oauth2.Config{
213 | ClientID: c.ClientID,
214 | RedirectURL: c.RedirectURL,
215 | Endpoint: provider.Endpoint(),
216 | Scopes: append([]string{oidc.ScopeOpenID}, c.Scopes...),
217 | }
218 |
219 | // Log in
220 | if oauth2Token.AccessToken == "" || userInfo.Email == "" {
221 | // Logged out state, info neither in storage nor in URL: Redirect to login
222 | if app.Window().URL().Query().Get(StateQueryParameter) == "" {
223 | ctx.Navigate(config.AuthCodeURL(c.RedirectURL, oauth2.AccessTypeOffline))
224 |
225 | return
226 | }
227 |
228 | // Intermediate state, info is in URL: Parse OAuth2 token
229 | oauth2Token, err := config.Exchange(context.Background(), app.Window().URL().Query().Get(CodeQueryParameter))
230 | if err != nil {
231 | c.panic(err, ctx)
232 |
233 | return
234 | }
235 |
236 | // Parse ID token
237 | idToken, ok := oauth2Token.Extra(idTokenExtraKey).(string)
238 | if !ok {
239 | c.panic(err, ctx)
240 |
241 | return
242 | }
243 |
244 | // Parse user info
245 | userInfo, err := provider.UserInfo(context.Background(), oauth2.StaticTokenSource(oauth2Token))
246 | if err != nil {
247 | c.panic(err, ctx)
248 |
249 | return
250 | }
251 |
252 | // Persist state in storage
253 | if err := c.persist(*oauth2Token, idToken, *userInfo, ctx); err != nil {
254 | c.panic(err, ctx)
255 |
256 | return
257 | }
258 |
259 | // Test validity of storage
260 | if _, _, _, err = c.rehydrate(ctx); err != nil {
261 | c.panic(err, ctx)
262 |
263 | return
264 | }
265 |
266 | // Update and navigate to home URL
267 | ctx.Navigate(c.HomeURL)
268 |
269 | return
270 | }
271 |
272 | // Validation state
273 |
274 | // Create the OIDC config
275 | oidcConfig := &oidc.Config{
276 | ClientID: c.ClientID,
277 | }
278 |
279 | // Create the OIDC verifier and validate the token (i.e. check for it's expiry date)
280 | verifier := provider.Verifier(oidcConfig)
281 | if _, err := verifier.Verify(context.Background(), idToken); err != nil {
282 | // Invalid token; clear and re-authorize
283 | c.clear(ctx)
284 | c.authorize(ctx)
285 |
286 | return
287 | }
288 |
289 | // Logged in state
290 |
291 | // Set the login state
292 | c.oauth2Token = oauth2Token
293 | c.idToken = idToken
294 | c.userInfo = userInfo
295 |
296 | // Watch and renew token once expired
297 | go c.watch(ctx)
298 | }
299 |
--------------------------------------------------------------------------------
/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/v10/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, app.Context)
22 | ApplyConfig func(app.Context)
23 |
24 | Error error
25 | }
26 |
27 | type SetupProvider 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 *SetupProvider) 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, ctx app.Context) {
60 | c.ready = false
61 | c.backendURL = s
62 | },
63 | SetOIDCIssuer: func(s string, ctx app.Context) {
64 | c.ready = false
65 | c.oidcIssuer = s
66 | },
67 | SetOIDCClientID: func(s string, ctx app.Context) {
68 | c.ready = false
69 | c.oidcClientID = s
70 | },
71 | SetOIDCRedirectURL: func(s string, ctx app.Context) {
72 | c.ready = false
73 | c.oidcRedirectURL = s
74 | },
75 | ApplyConfig: func(ctx app.Context) {
76 | c.validate(ctx)
77 | },
78 |
79 | Error: c.err,
80 | })
81 | }
82 |
83 | func (c *SetupProvider) invalidate(err error) {
84 | // Set the error state
85 | c.err = err
86 | c.ready = false
87 | }
88 |
89 | func (c *SetupProvider) validate(ctx app.Context) {
90 | // Validate fields
91 | if c.oidcClientID == "" {
92 | c.invalidate(errors.New("invalid OIDC client ID"))
93 |
94 | return
95 | }
96 |
97 | if _, err := url.ParseRequestURI(c.oidcIssuer); err != nil {
98 | c.invalidate(fmt.Errorf("invalid OIDC issuer: %v", err))
99 |
100 | return
101 | }
102 |
103 | if _, err := url.ParseRequestURI(c.backendURL); err != nil {
104 | c.invalidate(fmt.Errorf("invalid backend URL: %v", err))
105 |
106 | return
107 | }
108 |
109 | if _, err := url.ParseRequestURI(c.oidcRedirectURL); err != nil {
110 | c.invalidate(fmt.Errorf("invalid OIDC redirect URL: %v", err))
111 |
112 | return
113 | }
114 |
115 | // Persist state
116 | if err := c.persist(ctx); err != nil {
117 | c.invalidate(err)
118 |
119 | return
120 | }
121 |
122 | // If all are valid, set ready state
123 | c.err = nil
124 | c.ready = true
125 | }
126 |
127 | func (c *SetupProvider) persist(ctx app.Context) error {
128 | // Write state to storage
129 | if err := ctx.LocalStorage().Set(c.getKey(backendURLKey), c.backendURL); err != nil {
130 | return err
131 | }
132 | if err := ctx.LocalStorage().Set(c.getKey(oidcIssuerKey), c.oidcIssuer); err != nil {
133 | return err
134 | }
135 | if err := ctx.LocalStorage().Set(c.getKey(oidcClientIDKey), c.oidcClientID); err != nil {
136 | return err
137 | }
138 |
139 | return ctx.LocalStorage().Set(c.getKey(oidcRedirectURLKey), c.oidcRedirectURL)
140 | }
141 |
142 | func (c *SetupProvider) rehydrateFromURL(ctx app.Context) bool {
143 | // Read state from URL
144 | query := app.Window().URL().Query()
145 |
146 | backendURL := query.Get(backendURLKey)
147 | oidcIssuer := query.Get(oidcIssuerKey)
148 | oidcClientID := query.Get(oidcClientIDKey)
149 | oidcRedirectURL := query.Get(oidcRedirectURLKey)
150 |
151 | // If all values are set, set them in the data provider
152 | if backendURL != "" && oidcIssuer != "" && oidcClientID != "" && oidcRedirectURL != "" {
153 | c.backendURL = backendURL
154 | c.oidcIssuer = oidcIssuer
155 | c.oidcClientID = oidcClientID
156 | c.oidcRedirectURL = oidcRedirectURL
157 |
158 | return true
159 | }
160 |
161 | return false
162 | }
163 |
164 | func (c *SetupProvider) rehydrateFromStorage(ctx app.Context) bool {
165 | // Read state from storage
166 | backendURL := ""
167 | oidcIssuer := ""
168 | oidcClientID := ""
169 | oidcRedirectURL := ""
170 |
171 | if err := ctx.LocalStorage().Get(c.getKey(backendURLKey), &backendURL); err != nil {
172 | c.invalidate(err)
173 |
174 | return false
175 | }
176 | if err := ctx.LocalStorage().Get(c.getKey(oidcIssuerKey), &oidcIssuer); err != nil {
177 | c.invalidate(err)
178 |
179 | return false
180 | }
181 | if err := ctx.LocalStorage().Get(c.getKey(oidcClientIDKey), &oidcClientID); err != nil {
182 | c.invalidate(err)
183 |
184 | return false
185 | }
186 | if err := ctx.LocalStorage().Get(c.getKey(oidcRedirectURLKey), &oidcRedirectURL); err != nil {
187 | c.invalidate(err)
188 |
189 | return false
190 | }
191 |
192 | // If all values are set, set them in the data provider
193 | if backendURL != "" && oidcIssuer != "" && oidcClientID != "" && oidcRedirectURL != "" {
194 | c.backendURL = backendURL
195 | c.oidcIssuer = oidcIssuer
196 | c.oidcClientID = oidcClientID
197 | c.oidcRedirectURL = oidcRedirectURL
198 |
199 | return true
200 | }
201 |
202 | return false
203 | }
204 |
205 | func (c *SetupProvider) rehydrateAuthenticationFromURL() bool {
206 | // Read state from URL
207 | query := app.Window().URL().Query()
208 |
209 | state := query.Get(c.StateQueryParameter)
210 | code := query.Get(c.CodeQueryParameter)
211 |
212 | // If all values are set, set them in the data provider
213 | if state != "" && code != "" {
214 | return true
215 | }
216 |
217 | return false
218 | }
219 |
220 | func (c *SetupProvider) getKey(key string) string {
221 | // Get a prefixed key
222 | return fmt.Sprintf("%v.%v", c.StoragePrefix, key)
223 | }
224 |
225 | func (c *SetupProvider) OnMount(ctx app.Context) {
226 | // Initialize state
227 | c.backendURL = ""
228 | c.oidcIssuer = ""
229 | c.oidcClientID = ""
230 | c.oidcRedirectURL = ""
231 | c.ready = false
232 |
233 | // If rehydrated from URL, validate & apply
234 | if c.rehydrateFromURL(ctx) {
235 | // Auto-apply if configured
236 | // Disabled until a flow for handling wrong input details has been implemented
237 | // c.validate()
238 | }
239 |
240 | // If rehydrated from storage, validate & apply
241 | if c.rehydrateFromStorage(ctx) {
242 | // Auto-apply if configured
243 | // Disabled until a flow for handling wrong input details has been implemented
244 | // c.validate()
245 | }
246 |
247 | // If rehydrated authentication from URL, continue
248 | if c.rehydrateAuthenticationFromURL() {
249 | // Auto-apply if configured; set ready state
250 | c.err = nil
251 | c.ready = true
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/pkg/servers/dhcp.go:
--------------------------------------------------------------------------------
1 | package servers
2 |
3 | import (
4 | "net"
5 |
6 | "github.com/pojntfx/bofied/pkg/eventing"
7 | "github.com/pojntfx/bofied/pkg/transcoding"
8 | )
9 |
10 | const (
11 | DHCPServerBootMenuPromptBIOS = "PXE"
12 | DHCPServerBootMenuDescriptionBIOS = "Boot from bofied (BIOS)"
13 | )
14 |
15 | type DHCPServer struct {
16 | UDPServer
17 | }
18 |
19 | func NewDHCPServer(listenAddress string, advertisedIP string, eventHandler *eventing.EventHandler) *DHCPServer {
20 | return &DHCPServer{
21 | UDPServer: UDPServer{
22 | listenAddress: listenAddress,
23 | advertisedIP: advertisedIP,
24 | handlePacket: func(conn *net.UDPConn, _ *net.UDPAddr, braddr *net.UDPAddr, rawIncomingUDPPacket []byte) (int, error) {
25 | return handleDHCPPacket(conn, braddr, rawIncomingUDPPacket, net.ParseIP(advertisedIP).To4(), eventHandler.Emit)
26 | },
27 | },
28 | }
29 | }
30 |
31 | func handleDHCPPacket(conn *net.UDPConn, braddr *net.UDPAddr, rawIncomingUDPPacket []byte, advertisedIP net.IP, emit func(f string, v ...interface{})) (int, error) {
32 | // Decode packet
33 | incomingDHCPPacket, err := transcoding.DecodeDHCPPacket(rawIncomingUDPPacket)
34 | if err != nil {
35 | return 0, err
36 | }
37 |
38 | // Ignore non-PXE packets
39 | if !incomingDHCPPacket.IsPXE {
40 | return 0, nil
41 | }
42 |
43 | // Encode packet
44 | outgoingDHCPPacket := transcoding.EncodeDHCPPacket(
45 | incomingDHCPPacket.ClientHWAddr,
46 | incomingDHCPPacket.Xid,
47 | advertisedIP,
48 | incomingDHCPPacket.ClientIdentifierOpt,
49 | incomingDHCPPacket.Arch,
50 | DHCPServerBootMenuPromptBIOS,
51 | DHCPServerBootMenuDescriptionBIOS,
52 | )
53 |
54 | emit(`sending %v bytes of DHCP packets to client "%v"`, len(outgoingDHCPPacket), braddr)
55 |
56 | // Broadcast the packet
57 | return conn.WriteToUDP(outgoingDHCPPacket, braddr)
58 | }
59 |
--------------------------------------------------------------------------------
/pkg/servers/extended_http.go:
--------------------------------------------------------------------------------
1 | package servers
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/pojntfx/bofied/pkg/authorization"
7 | "github.com/pojntfx/bofied/pkg/constants"
8 | "github.com/pojntfx/bofied/pkg/eventing"
9 | "github.com/pojntfx/bofied/pkg/validators"
10 | "github.com/rs/cors"
11 | "golang.org/x/net/webdav"
12 | )
13 |
14 | const (
15 | WebDAVRealmDescription = `bofied protected area. You can find your credentials (username and password/token) with the "Mount Folder" option in the frontend.`
16 | HTTPPrefix = "/public"
17 | WebDAVPrefix = "/private"
18 | GRPCPrefix = "/grpc"
19 | )
20 |
21 | type ExtendedHTTPServer struct {
22 | FileServer
23 |
24 | eventsServerHandler http.Handler
25 | eventHandler *eventing.EventHandler
26 |
27 | oidcValidator *validators.OIDCValidator
28 | }
29 |
30 | func NewExtendedHTTPServer(
31 | workingDir string,
32 | listenAddress string,
33 | oidcValidator *validators.OIDCValidator,
34 | eventsServerHandler http.Handler,
35 | eventHandler *eventing.EventHandler,
36 | ) *ExtendedHTTPServer {
37 | return &ExtendedHTTPServer{
38 | FileServer: FileServer{
39 | workingDir: workingDir,
40 | listenAddress: listenAddress,
41 | },
42 |
43 | eventsServerHandler: eventsServerHandler,
44 | eventHandler: eventHandler,
45 |
46 | oidcValidator: oidcValidator,
47 | }
48 | }
49 |
50 | func (s *ExtendedHTTPServer) GetWebDAVHandler(prefix string) webdav.Handler {
51 | return webdav.Handler{
52 | Prefix: prefix,
53 | FileSystem: webdav.Dir(s.workingDir),
54 | LockSystem: webdav.NewMemLS(),
55 | }
56 | }
57 |
58 | func (s *ExtendedHTTPServer) GetHTTPHandler() http.Handler {
59 | return eventing.LogRequestHandler(
60 | http.FileServer(
61 | http.Dir(s.workingDir),
62 | ),
63 | s.eventHandler,
64 | )
65 | }
66 |
67 | func (s *ExtendedHTTPServer) ListenAndServe() error {
68 | webDAVHandler := s.GetWebDAVHandler(WebDAVPrefix)
69 | httpHandler := s.GetHTTPHandler()
70 |
71 | mux := http.NewServeMux()
72 |
73 | mux.Handle(
74 | HTTPPrefix+"/",
75 | http.StripPrefix(HTTPPrefix, httpHandler),
76 | )
77 | mux.Handle(
78 | WebDAVPrefix+"/",
79 | cors.New(cors.Options{
80 | AllowedMethods: []string{
81 | "GET",
82 | "PUT",
83 | "PROPFIND",
84 | "MKCOL",
85 | "MOVE",
86 | "COPY",
87 | "DELETE",
88 | },
89 | AllowCredentials: true,
90 | AllowedHeaders: []string{
91 | "*",
92 | },
93 | }).Handler(
94 | authorization.OIDCOverBasicAuth(
95 | &webDAVHandler,
96 | constants.OIDCOverBasicAuthUsername,
97 | s.oidcValidator,
98 | WebDAVRealmDescription,
99 | ),
100 | ),
101 | )
102 | mux.Handle(
103 | GRPCPrefix,
104 | s.eventsServerHandler,
105 | )
106 |
107 | return http.ListenAndServe(
108 | s.listenAddress,
109 | mux,
110 | )
111 | }
112 |
--------------------------------------------------------------------------------
/pkg/servers/file_server.go:
--------------------------------------------------------------------------------
1 | package servers
2 |
3 | type FileServer struct {
4 | workingDir string
5 | listenAddress string
6 | }
7 |
--------------------------------------------------------------------------------
/pkg/servers/grpc.go:
--------------------------------------------------------------------------------
1 | package servers
2 |
3 | import (
4 | "net"
5 | "sync"
6 |
7 | api "github.com/pojntfx/bofied/pkg/api/proto/v1"
8 | "github.com/pojntfx/bofied/pkg/services"
9 | "github.com/pojntfx/bofied/pkg/websocketproxy"
10 | "google.golang.org/grpc"
11 | "google.golang.org/grpc/reflection"
12 | )
13 |
14 | type GRPCServer struct {
15 | listenAddress string
16 |
17 | eventsService *services.EventsService
18 | metadataService *services.MetadataService
19 |
20 | proxy *websocketproxy.WebSocketProxyServer
21 | }
22 |
23 | func NewGRPCServer(listenAddress string, eventsService *services.EventsService, metadataService *services.MetadataService) (*GRPCServer, *websocketproxy.WebSocketProxyServer) {
24 | proxy := websocketproxy.NewWebSocketProxyServer()
25 |
26 | return &GRPCServer{
27 | listenAddress: listenAddress,
28 | eventsService: eventsService,
29 | metadataService: metadataService,
30 | proxy: proxy,
31 | }, proxy
32 | }
33 |
34 | func (s *GRPCServer) ListenAndServe() error {
35 | listener, err := net.Listen("tcp", s.listenAddress)
36 | if err != nil {
37 | return err
38 | }
39 |
40 | server := grpc.NewServer()
41 | reflection.Register(server)
42 |
43 | api.RegisterEventsServiceServer(server, s.eventsService)
44 | api.RegisterMetadataServiceServer(server, s.metadataService)
45 |
46 | doneChan := make(chan struct{})
47 | errChan := make(chan error)
48 |
49 | var wg sync.WaitGroup
50 | wg.Add(2)
51 |
52 | go func() {
53 | wg.Wait()
54 |
55 | close(doneChan)
56 | }()
57 |
58 | go func() {
59 | if err := server.Serve(listener); err != nil {
60 | errChan <- err
61 | }
62 |
63 | wg.Done()
64 | }()
65 |
66 | go func() {
67 | if err := server.Serve(s.proxy); err != nil {
68 | errChan <- err
69 | }
70 |
71 | wg.Done()
72 | }()
73 |
74 | select {
75 | case <-doneChan:
76 | return nil
77 | case <-errChan:
78 | return err
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/servers/proxy_dhcp.go:
--------------------------------------------------------------------------------
1 | package servers
2 |
3 | import (
4 | "net"
5 |
6 | "github.com/pojntfx/bofied/pkg/config"
7 | "github.com/pojntfx/bofied/pkg/eventing"
8 | "github.com/pojntfx/bofied/pkg/transcoding"
9 | )
10 |
11 | type ProxyDHCPServer struct {
12 | UDPServer
13 | }
14 |
15 | func NewProxyDHCPServer(listenAddress string, advertisedIP string, configFileLocation string, eventHandler *eventing.EventHandler, pureConfig bool) *ProxyDHCPServer {
16 | return &ProxyDHCPServer{
17 | UDPServer: UDPServer{
18 | listenAddress: listenAddress,
19 | advertisedIP: advertisedIP,
20 | handlePacket: func(conn *net.UDPConn, raddr *net.UDPAddr, braddr *net.UDPAddr, rawIncomingUDPPacket []byte) (int, error) {
21 | return handleProxyDHCPPacket(conn, raddr, braddr, rawIncomingUDPPacket, net.ParseIP(advertisedIP).To4(), configFileLocation, eventHandler.Emit, pureConfig)
22 | },
23 | },
24 | }
25 | }
26 |
27 | func handleProxyDHCPPacket(conn *net.UDPConn, raddr *net.UDPAddr, _ *net.UDPAddr, rawIncomingUDPPacket []byte, advertisedIP net.IP, configFileLocation string, emit func(f string, v ...interface{}), pureConfig bool) (int, error) {
28 | // Decode packet
29 | incomingDHCPPacket, err := transcoding.DecodeDHCPPacket(rawIncomingUDPPacket)
30 | if err != nil {
31 | return 0, err
32 | }
33 |
34 | // Ignore non-PXE packets
35 | if !incomingDHCPPacket.IsPXE {
36 | return 0, nil
37 | }
38 |
39 | emit(
40 | `handling proxyDHCP for client with IP %v, MAC %v, architecture %v and architecture ID %v`,
41 | raddr.IP.String(),
42 | incomingDHCPPacket.ClientHWAddr.String(),
43 | config.GetNameForArchId(incomingDHCPPacket.Arch),
44 | incomingDHCPPacket.Arch,
45 | )
46 |
47 | // Get the boot file name
48 | bootFileName, err := config.GetFileName(
49 | configFileLocation,
50 | raddr.IP.String(),
51 | incomingDHCPPacket.ClientHWAddr.String(),
52 | config.GetNameForArchId(incomingDHCPPacket.Arch),
53 | incomingDHCPPacket.Arch,
54 | pureConfig,
55 | func(s string) {
56 | if s != "" {
57 | emit("from config: %v", s)
58 | }
59 | },
60 | )
61 | if err != nil {
62 | emit("could not process config: %v", err)
63 |
64 | return 0, err
65 | }
66 |
67 | // Encode packet
68 | outgoingDHCPPacket := transcoding.EncodeProxyDHCPPacket(
69 | incomingDHCPPacket.ClientHWAddr,
70 | incomingDHCPPacket.Xid,
71 | advertisedIP,
72 | incomingDHCPPacket.ClientIdentifierOpt,
73 | raddr.IP.To4(),
74 | bootFileName,
75 | )
76 |
77 | emit(`sending %v bytes of proxyDHCP packets to client "%v"`, len(outgoingDHCPPacket), raddr)
78 |
79 | // Send the packet to the client
80 | return conn.WriteToUDP(outgoingDHCPPacket, raddr)
81 | }
82 |
--------------------------------------------------------------------------------
/pkg/servers/tftp.go:
--------------------------------------------------------------------------------
1 | package servers
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 |
10 | tftp "github.com/pin/tftp/v3"
11 | "github.com/pojntfx/bofied/pkg/eventing"
12 | )
13 |
14 | type TFTPServer struct {
15 | FileServer
16 |
17 | eventHandler *eventing.EventHandler
18 | }
19 |
20 | func NewTFTPServer(workingDir string, listenAddress string, eventHandler *eventing.EventHandler) *TFTPServer {
21 | return &TFTPServer{
22 | FileServer: FileServer{
23 | workingDir: workingDir,
24 | listenAddress: listenAddress,
25 | },
26 |
27 | eventHandler: eventHandler,
28 | }
29 | }
30 |
31 | func (s *TFTPServer) ListenAndServe() error {
32 | h := tftp.NewServer(
33 | func(filename string, rf io.ReaderFrom) error {
34 | // Get remote IP
35 | raddr := rf.(tftp.OutgoingTransfer).RemoteAddr()
36 |
37 | // Prevent accessing any parent directories
38 | fullFilename := filepath.Join(s.workingDir, filename)
39 | if strings.Contains(filename, "..") {
40 | s.eventHandler.Emit(`could not send file: get request to file "%v" by client "%v" blocked because it is located outside the working directory "%v"`, fullFilename, raddr.String(), s.workingDir)
41 |
42 | return errors.New("unauthorized: tried to access file outside working directory")
43 | }
44 |
45 | // Open file to send
46 | file, err := os.Open(fullFilename)
47 | if err != nil {
48 | s.eventHandler.Emit(`could not open file "%v" for client "%v": %v`, fullFilename, raddr.String(), err)
49 |
50 | return err
51 | }
52 |
53 | // Send the file to the client
54 | n, err := rf.ReadFrom(file)
55 | if err != nil {
56 | s.eventHandler.Emit(`could not sent file "%v" to client "%v": %v`, fullFilename, raddr.String(), err)
57 |
58 | return err
59 | }
60 |
61 | s.eventHandler.Emit(`sent file "%v" (%v bytes) to client "%v"`, fullFilename, n, raddr.String())
62 |
63 | return nil
64 | },
65 | nil,
66 | )
67 |
68 | return h.ListenAndServe(s.listenAddress)
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/servers/udp_server.go:
--------------------------------------------------------------------------------
1 | package servers
2 |
3 | import (
4 | "errors"
5 | "log"
6 | "net"
7 |
8 | "github.com/pojntfx/bofied/pkg/utils"
9 | )
10 |
11 | const (
12 | UDPServerReadBufSize = 1024
13 | )
14 |
15 | type UDPServer struct {
16 | listenAddress string
17 | advertisedIP string
18 | handlePacket func(conn *net.UDPConn, raddr *net.UDPAddr, braddr *net.UDPAddr, rawIncomingUDPPacket []byte) (int, error)
19 | }
20 |
21 | func (s *UDPServer) ListenAndServe() error {
22 | // Parse the addresses
23 | laddr, err := net.ResolveUDPAddr("udp", s.listenAddress)
24 | if err != nil {
25 | return err
26 | }
27 |
28 | aaddr, err := net.ResolveIPAddr("ip", s.advertisedIP)
29 | if err != nil {
30 | return err
31 | }
32 |
33 | // Get all interfaces
34 | ifaces, err := net.Interfaces()
35 | if err != nil {
36 | return err
37 | }
38 |
39 | // Find the broadcast address of the interface with the advertised IP
40 | broadcastIP := ""
41 | ifaceLoop:
42 | for _, iface := range ifaces {
43 | addrs, err := iface.Addrs()
44 | if err != nil {
45 | return err
46 | }
47 |
48 | for _, addr := range addrs {
49 | ipnet, ok := addr.(*net.IPNet)
50 | if !ok {
51 | continue
52 | }
53 |
54 | if ipnet.IP.Equal(aaddr.IP) {
55 | broadcastIP, err = utils.GetBroadcastAddress(ipnet)
56 | if err != nil {
57 | return err
58 | }
59 |
60 | break ifaceLoop
61 | }
62 | }
63 | }
64 |
65 | // Return if no interface with the advertised IP could be found
66 | if broadcastIP == "" {
67 | return errors.New("could not resolve broadcast IP")
68 | }
69 |
70 | // Construct the broadcast address
71 | braddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(broadcastIP, "68"))
72 | if err != nil {
73 | return err
74 | }
75 |
76 | // Listen
77 | conn, err := net.ListenUDP("udp", laddr)
78 | if err != nil {
79 | return err
80 | }
81 |
82 | // Loop over packets
83 | for {
84 | // Read packet into buffer
85 | buf := make([]byte, UDPServerReadBufSize)
86 | length, raddr, err := conn.ReadFromUDP(buf)
87 | if err != nil {
88 | return err
89 | }
90 |
91 | // Handle the read packet
92 | go func() {
93 | if _, err := s.handlePacket(conn, raddr, braddr, buf[:length]); err != nil {
94 | log.Println("could not handle packet:", err)
95 | }
96 | }()
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/pkg/services/events.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | //go:generate sh -c "mkdir -p ../api/proto/v1 && protoc --go_out=paths=source_relative:../api/proto/v1 --go-grpc_out=paths=source_relative:../api/proto/v1 -I=../../api/proto/v1 ../../api/proto/v1/*.proto"
4 |
5 | import (
6 | "fmt"
7 | "log"
8 | "time"
9 |
10 | api "github.com/pojntfx/bofied/pkg/api/proto/v1"
11 | "github.com/pojntfx/bofied/pkg/eventing"
12 | "github.com/pojntfx/bofied/pkg/validators"
13 | "google.golang.org/grpc/codes"
14 | "google.golang.org/grpc/status"
15 | )
16 |
17 | const (
18 | AuthorizationMetadataKey = "X-Bofied-Authorization"
19 | )
20 |
21 | type EventsService struct {
22 | api.UnimplementedEventsServiceServer
23 |
24 | eventsHandler *eventing.EventHandler
25 |
26 | contextValidator *validators.ContextValidator
27 | }
28 |
29 | func NewEventsService(eventsHandler *eventing.EventHandler, contextValidator *validators.ContextValidator) *EventsService {
30 | return &EventsService{
31 | eventsHandler: eventsHandler,
32 |
33 | contextValidator: contextValidator,
34 | }
35 | }
36 |
37 | func (s *EventsService) SubscribeToEvents(_ *api.Empty, stream api.EventsService_SubscribeToEventsServer) error {
38 | // Authorize
39 | valid, err := s.contextValidator.Validate(stream.Context())
40 | if err != nil || !valid {
41 | return status.Errorf(codes.Unauthenticated, "could not authorize: %v", err)
42 | }
43 |
44 | // Subscribe to events
45 | events, err := s.eventsHandler.Sub()
46 | if err != nil {
47 | msg := fmt.Sprintf("could not get events from messenger: %v", err)
48 |
49 | log.Println(msg)
50 |
51 | return status.Error(codes.Unknown, msg)
52 | }
53 | defer s.eventsHandler.Unsub(events)
54 |
55 | // Receive event from bus
56 | for event := range events {
57 | e := event.(eventing.Event)
58 |
59 | // Send event to client
60 | if err := stream.Send(&api.EventMessage{
61 | CreatedAt: e.CreatedAt.Format(time.RFC3339),
62 | Message: e.Message,
63 | }); err != nil {
64 | log.Printf("could send event to client: %v\n", err)
65 |
66 | return err
67 | }
68 | }
69 |
70 | return nil
71 | }
72 |
--------------------------------------------------------------------------------
/pkg/services/metadata.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | //go:generate sh -c "mkdir -p ../api/proto/v1 && protoc --go_out=paths=source_relative:../api/proto/v1 --go-grpc_out=paths=source_relative:../api/proto/v1 -I=../../api/proto/v1 ../../api/proto/v1/*.proto"
4 |
5 | import (
6 | "context"
7 |
8 | api "github.com/pojntfx/bofied/pkg/api/proto/v1"
9 | "github.com/pojntfx/bofied/pkg/validators"
10 | "google.golang.org/grpc/codes"
11 | "google.golang.org/grpc/status"
12 | )
13 |
14 | type MetadataService struct {
15 | api.UnimplementedMetadataServiceServer
16 |
17 | advertisedIP string
18 | tftpPort int32
19 | httpPort int32
20 |
21 | contextValidator *validators.ContextValidator
22 | }
23 |
24 | func NewMetadataService(
25 | advertisedIP string,
26 | tftpPort int32,
27 | httpPort int32,
28 | contextValidator *validators.ContextValidator,
29 | ) *MetadataService {
30 | return &MetadataService{
31 | advertisedIP: advertisedIP,
32 | tftpPort: tftpPort,
33 | httpPort: httpPort,
34 |
35 | contextValidator: contextValidator,
36 | }
37 | }
38 |
39 | func (s *MetadataService) GetMetadata(ctx context.Context, _ *api.Empty) (*api.MetadataMessage, error) {
40 | // Authorize
41 | valid, err := s.contextValidator.Validate(ctx)
42 | if err != nil || !valid {
43 | return nil, status.Errorf(codes.Unauthenticated, "could not authorize: %v", err)
44 | }
45 |
46 | // Return the constructed message
47 | return &api.MetadataMessage{
48 | AdvertisedIP: s.advertisedIP,
49 | TFTPPort: s.tftpPort,
50 | HTTPPort: s.httpPort,
51 | }, nil
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/transcoding/dhcp.go:
--------------------------------------------------------------------------------
1 | package transcoding
2 |
3 | import (
4 | "errors"
5 | "net"
6 |
7 | "github.com/google/gopacket"
8 | "github.com/google/gopacket/layers"
9 | )
10 |
11 | const (
12 | DHCPBaseClassID = "PXEClient"
13 | DHCPEFIArch = 7 // X86-64_EFI
14 | DHCPOptUUIDGUIDClientIdentifier = 97 // Option: (97) UUID/GUID-based Client Identifier
15 | DHCPRootPath = 17 // Option: (17) Root Path
16 | )
17 |
18 | type DecodedDHCPPacket struct {
19 | IsPXE bool
20 | ClientHWAddr net.HardwareAddr
21 | Xid uint32
22 | ClientIdentifierOpt layers.DHCPOption
23 | Arch int
24 | }
25 |
26 | func DecodeDHCPPacket(rawIncomingUDPPacket []byte) (DecodedDHCPPacket, error) {
27 | // Decode and parse packet
28 | incomingUDPPacket := gopacket.NewPacket(rawIncomingUDPPacket, layers.LayerTypeDHCPv4, gopacket.Default)
29 |
30 | dhcpLayer := incomingUDPPacket.Layer(layers.LayerTypeDHCPv4)
31 | if dhcpLayer == nil {
32 | return DecodedDHCPPacket{}, errors.New("could not parse DHCP layer: not a DHCP layer")
33 | }
34 |
35 | incomingDHCPPacket, ok := dhcpLayer.(*layers.DHCPv4)
36 | if !ok {
37 | return DecodedDHCPPacket{}, errors.New("could not parse DHCP layer: invalid DHCP layer")
38 | }
39 |
40 | // Parse DHCP options
41 | isPXE := false
42 | arch := 0
43 | clientIdentifierOpt := layers.DHCPOption{}
44 | if incomingDHCPPacket.Operation == layers.DHCPOpRequest {
45 | for _, option := range incomingDHCPPacket.Options {
46 | switch option.Type {
47 | case layers.DHCPOptClassID:
48 | pxe, a, err := ParsePXEClassIdentifier(string(option.Data))
49 | if err != nil {
50 | return DecodedDHCPPacket{}, err
51 | }
52 |
53 | isPXE = pxe
54 | arch = a
55 | case DHCPOptUUIDGUIDClientIdentifier:
56 | clientIdentifierOpt = option
57 | }
58 | }
59 | }
60 |
61 | return DecodedDHCPPacket{
62 | IsPXE: isPXE,
63 | ClientHWAddr: incomingDHCPPacket.ClientHWAddr,
64 | Xid: incomingDHCPPacket.Xid,
65 | ClientIdentifierOpt: clientIdentifierOpt,
66 | Arch: arch,
67 | }, nil
68 | }
69 |
70 | func EncodeDHCPPacket(
71 | clientHWAddr net.HardwareAddr,
72 | xid uint32,
73 | advertisedIP net.IP,
74 | clientIdentifierOpt layers.DHCPOption,
75 | arch int,
76 | bootMenuPromptBIOS string,
77 | bootMenuDescriptionBIOS string,
78 | ) []byte {
79 | // Create the outgoing packet
80 | outgoingDHCPPacket := &layers.DHCPv4{
81 | Operation: layers.DHCPOpReply,
82 | HardwareType: layers.LinkTypeEthernet,
83 | HardwareLen: uint8(len(clientHWAddr)),
84 | Xid: xid,
85 | ClientIP: net.ParseIP("0.0.0.0").To4(),
86 | YourClientIP: net.ParseIP("0.0.0.0").To4(),
87 | NextServerIP: advertisedIP,
88 | RelayAgentIP: net.ParseIP("0.0.0.0").To4(),
89 | ClientHWAddr: clientHWAddr,
90 | Options: layers.DHCPOptions{
91 | layers.NewDHCPOption(
92 | layers.DHCPOptMessageType,
93 | []byte{byte(layers.DHCPMsgTypeOffer)},
94 | ),
95 | layers.NewDHCPOption(
96 | layers.DHCPOptServerID,
97 | advertisedIP,
98 | ),
99 | layers.NewDHCPOption(
100 | layers.DHCPOptClassID,
101 | []byte(DHCPBaseClassID),
102 | ),
103 | layers.NewDHCPOption(
104 | DHCPRootPath,
105 | []byte(advertisedIP.String()),
106 | ),
107 | clientIdentifierOpt,
108 | },
109 | }
110 |
111 | // If the packet is not intended for EFI systems, add additional required options
112 | if arch != DHCPEFIArch {
113 | // Create DHCP Option 43 suboptions
114 | subOptions := []layers.DHCPOption{
115 | layers.NewDHCPOption(
116 | 6, // Option 43 Suboption: (6) PXE discovery control
117 | []byte{byte(0x00000003)}, // discovery control: 0x03, Disable Broadcast, Disable Multicast
118 | ),
119 | layers.NewDHCPOption(
120 | 10, // Option 43 Suboption: (10) PXE menu prompt
121 | append( // menu prompt: 00505845
122 | []byte{
123 | 0x00, // Timeout: 0
124 | },
125 | []byte(bootMenuPromptBIOS)..., // Prompt: PXE
126 | ),
127 | ),
128 | layers.NewDHCPOption(
129 | 8, // Option 43 Suboption: (8) PXE boot servers
130 | append( // boot servers: 80000164409af2
131 | []byte{
132 | 0x80, 0x00, // Type: Unknown (32768)
133 | 0x01, // IP count: 1
134 | },
135 | advertisedIP..., // IP: 100.64.154.246
136 | ),
137 | ),
138 | layers.NewDHCPOption(
139 | 9, // Option 43 Suboption: (9) PXE boot menu
140 | append(
141 | []byte{
142 | 0x80, 0x00, // Type: Unknown (32768)
143 | byte(len(bootMenuDescriptionBIOS)), // Length: 16
144 | },
145 | []byte(bootMenuDescriptionBIOS)..., // Description: Boot iPXE (BIOS)
146 | ),
147 | ),
148 | }
149 |
150 | // Serialize DHCP Option 43 suboptions
151 | serializedSubOptions := []byte{}
152 | for _, subOption := range subOptions {
153 | serializedSubOptions = append(
154 | serializedSubOptions,
155 | append(
156 | []byte{
157 | byte(subOption.Type),
158 | subOption.Length,
159 | },
160 | subOption.Data...,
161 | )...,
162 | )
163 | }
164 |
165 | // Add DHCP Option 43 suboptions and set the next server IP to 0.0.0.0
166 | outgoingDHCPPacket.Options = append(
167 | outgoingDHCPPacket.Options,
168 | layers.NewDHCPOption(
169 | layers.DHCPOptVendorOption,
170 | serializedSubOptions,
171 | ),
172 | )
173 | outgoingDHCPPacket.NextServerIP = net.ParseIP("0.0.0.0").To4()
174 | }
175 |
176 | // Serialize the outgoing packet
177 | buf := gopacket.NewSerializeBuffer()
178 | gopacket.SerializeLayers(
179 | buf,
180 | gopacket.SerializeOptions{
181 | FixLengths: true,
182 | },
183 | outgoingDHCPPacket,
184 | )
185 |
186 | return buf.Bytes()
187 | }
188 |
--------------------------------------------------------------------------------
/pkg/transcoding/proxy_dhcp.go:
--------------------------------------------------------------------------------
1 | package transcoding
2 |
3 | import (
4 | "net"
5 |
6 | "github.com/google/gopacket"
7 | "github.com/google/gopacket/layers"
8 | )
9 |
10 | func EncodeProxyDHCPPacket(
11 | clientHWAddr net.HardwareAddr,
12 | xid uint32,
13 | advertisedIP net.IP,
14 | clientIdentifierOpt layers.DHCPOption,
15 | yourClientIP net.IP,
16 | bootFileName string,
17 | ) []byte {
18 | // Create DHCP Option 43 suboptions
19 | subOption :=
20 | layers.NewDHCPOption(
21 | 71, // Option 43 Suboption: (71) PXE boot item
22 | []byte{ // boot item: 80000000
23 | 0x80, 0x00, // Type: 32768
24 | 0x00, 0x00, // Layer: 0000
25 | },
26 | )
27 |
28 | // Serialize DHCP Option 43 suboptions
29 | serializedSubOptions :=
30 | append(
31 | append(
32 | []byte{
33 | byte(subOption.Type),
34 | subOption.Length,
35 | },
36 | subOption.Data...,
37 | ),
38 | byte(0xff), // PXE Client End: 255
39 | )
40 |
41 | // Create the outgoing packet
42 | outgoingDHCPPacket := &layers.DHCPv4{
43 | Operation: layers.DHCPOpReply,
44 | HardwareType: layers.LinkTypeEthernet,
45 | HardwareLen: uint8(len(clientHWAddr)),
46 | Xid: xid,
47 | ClientIP: net.ParseIP("0.0.0.0").To4(),
48 | YourClientIP: yourClientIP,
49 | NextServerIP: advertisedIP,
50 | RelayAgentIP: net.ParseIP("0.0.0.0").To4(),
51 | ClientHWAddr: clientHWAddr,
52 | File: []byte(bootFileName),
53 | Options: layers.DHCPOptions{
54 | layers.NewDHCPOption(
55 | layers.DHCPOptMessageType,
56 | []byte{byte(layers.DHCPMsgTypeAck)},
57 | ),
58 | layers.NewDHCPOption(
59 | layers.DHCPOptServerID,
60 | advertisedIP,
61 | ),
62 | layers.NewDHCPOption(
63 | layers.DHCPOptClassID,
64 | []byte(DHCPBaseClassID),
65 | ),
66 | clientIdentifierOpt,
67 | layers.NewDHCPOption(
68 | layers.DHCPOptVendorOption,
69 | serializedSubOptions,
70 | ),
71 | layers.NewDHCPOption(
72 | DHCPRootPath,
73 | []byte(advertisedIP.String()),
74 | ),
75 | },
76 | }
77 |
78 | // Serialize the outgoing packet
79 | buf := gopacket.NewSerializeBuffer()
80 | gopacket.SerializeLayers(
81 | buf,
82 | gopacket.SerializeOptions{
83 | FixLengths: true,
84 | },
85 | outgoingDHCPPacket,
86 | )
87 |
88 | return buf.Bytes()
89 | }
90 |
--------------------------------------------------------------------------------
/pkg/transcoding/pxe_class_identifier.go:
--------------------------------------------------------------------------------
1 | package transcoding
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 | )
7 |
8 | func ParsePXEClassIdentifier(classID string) (isPXE bool, arch int, err error) {
9 | parts := strings.Split(classID, ":")
10 |
11 | for i, part := range parts {
12 | switch part {
13 | case "PXEClient":
14 | isPXE = true
15 | case "Arch":
16 | if len(parts) > i {
17 | arch, err = strconv.Atoi(parts[i+1])
18 | if err != nil {
19 | return
20 | }
21 | }
22 | }
23 | }
24 |
25 | return
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/utils/ip.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "net"
5 | )
6 |
7 | // Based on https://gist.github.com/kotakanbe/d3059af990252ba89a82
8 | func GetBroadcastAddress(ipnet *net.IPNet) (string, error) {
9 | ips := []string{}
10 | for ip := ipnet.IP.Mask(ipnet.Mask); ipnet.Contains(ip); inc(ip) {
11 | ips = append(ips, ip.String())
12 | }
13 |
14 | // The last address is the broadcast address
15 | return ips[len(ips)-1], nil
16 | }
17 |
18 | // See http://play.golang.org/p/m8TNTtygK0
19 | func inc(ip net.IP) {
20 | for j := len(ip) - 1; j >= 0; j-- {
21 | ip[j]++
22 | if ip[j] > 0 {
23 | break
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/validators/check_syntax.go:
--------------------------------------------------------------------------------
1 | package validators
2 |
3 | import (
4 | "go/parser"
5 | "go/token"
6 | )
7 |
8 | func CheckGoSyntax(src string) error {
9 | _, err := parser.ParseFile(token.NewFileSet(), "", src, parser.ParseComments)
10 |
11 | return err
12 | }
13 |
--------------------------------------------------------------------------------
/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/format.go:
--------------------------------------------------------------------------------
1 | package validators
2 |
3 | import "go/format"
4 |
5 | func FormatGoSrc(src string) (string, error) {
6 | s, err := format.Source([]byte(src))
7 | if err != nil {
8 | return src, err
9 | }
10 |
11 | return string(s), err
12 | }
13 |
--------------------------------------------------------------------------------
/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/websocketproxy/client.go:
--------------------------------------------------------------------------------
1 | package websocketproxy
2 |
3 | import (
4 | "context"
5 | "net"
6 | "time"
7 |
8 | "nhooyr.io/websocket"
9 | )
10 |
11 | type WebSocketProxyClient struct {
12 | timeout time.Duration
13 | }
14 |
15 | func NewWebSocketProxyClient(timeout time.Duration) *WebSocketProxyClient {
16 | return &WebSocketProxyClient{timeout}
17 | }
18 |
19 | func (p *WebSocketProxyClient) Dialer(ctx context.Context, url string) (net.Conn, error) {
20 | ctx, cancel := context.WithTimeout(ctx, p.timeout)
21 | defer cancel()
22 |
23 | conn, _, err := websocket.Dial(ctx, url, nil)
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | return websocket.NetConn(context.Background(), conn, websocket.MessageBinary), nil
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/websocketproxy/server.go:
--------------------------------------------------------------------------------
1 | package websocketproxy
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net"
7 | "net/http"
8 |
9 | "nhooyr.io/websocket"
10 | )
11 |
12 | type WebSocketProxyServerAddr struct {
13 | network string
14 | address string
15 | }
16 |
17 | func (a *WebSocketProxyServerAddr) Network() string {
18 | return a.network
19 | }
20 |
21 | func (a *WebSocketProxyServerAddr) String() string {
22 | return a.address
23 | }
24 |
25 | type WebSocketProxyServer struct {
26 | stopChan chan struct{}
27 | errorChan chan error
28 | connectionChan chan net.Conn
29 | }
30 |
31 | func NewWebSocketProxyServer() *WebSocketProxyServer {
32 | return &WebSocketProxyServer{
33 | stopChan: make(chan struct{}),
34 | errorChan: make(chan error, 1),
35 | connectionChan: make(chan net.Conn),
36 | }
37 | }
38 |
39 | func (p *WebSocketProxyServer) ServeHTTP(wr http.ResponseWriter, r *http.Request) {
40 | conn, err := websocket.Accept(wr, r, &websocket.AcceptOptions{
41 | InsecureSkipVerify: true, // CORS
42 | })
43 | if err != nil {
44 | log.Printf("could not accept on WebSocket: %v\n", err)
45 |
46 | return
47 | }
48 | defer conn.Close(websocket.StatusInternalError, "fail")
49 |
50 | ctx := r.Context()
51 |
52 | select {
53 | case <-p.stopChan:
54 | return
55 |
56 | default:
57 | p.connectionChan <- websocket.NetConn(ctx, conn, websocket.MessageBinary)
58 |
59 | select {
60 | case <-p.stopChan:
61 | case <-r.Context().Done():
62 | }
63 | }
64 |
65 | conn.Close(websocket.StatusNormalClosure, "ok")
66 | }
67 |
68 | func (p *WebSocketProxyServer) Accept() (net.Conn, error) {
69 | select {
70 | case <-p.stopChan:
71 | return nil, fmt.Errorf("server stopped")
72 |
73 | case err := <-p.errorChan:
74 | _ = p.Close()
75 |
76 | return nil, err
77 |
78 | case c := <-p.connectionChan:
79 | return c, nil
80 | }
81 | }
82 |
83 | func (p *WebSocketProxyServer) Close() error {
84 | select {
85 | case <-p.stopChan:
86 |
87 | default:
88 | close(p.stopChan)
89 | }
90 |
91 | return nil
92 | }
93 |
94 | func (p *WebSocketProxyServer) Addr() net.Addr {
95 | return &WebSocketProxyServerAddr{}
96 | }
97 |
--------------------------------------------------------------------------------
/web/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/web/icon.png
--------------------------------------------------------------------------------
/web/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --pf-x-base-color: #0066cc;
3 | }
4 |
5 | @media (prefers-color-scheme: dark) {
6 | :root {
7 | --pf-x-base-color: #92c5f9;
8 | }
9 | }
10 |
11 | @supports (color: AccentColor) {
12 | :root {
13 | --pf-x-base-color: color-mix(in srgb, AccentColor 60%, black);
14 | }
15 | }
16 |
17 | @media (prefers-color-scheme: dark) {
18 | @supports (color: AccentColor) {
19 | :root {
20 | --pf-x-base-color: oklch(from AccentColor max(0.85, l) c h);
21 | }
22 | }
23 | }
24 |
25 | :root {
26 | --pf-t--global--color--brand--100: color-mix(
27 | in srgb,
28 | var(--pf-x-base-color) 80%,
29 | white
30 | );
31 | --pf-t--global--color--brand--200: var(--pf-x-base-color);
32 | --pf-t--global--color--brand--300: color-mix(
33 | in srgb,
34 | var(--pf-x-base-color) 80%,
35 | black
36 | );
37 | }
38 |
39 | @media (prefers-color-scheme: dark) {
40 | :root {
41 | --pf-t--global--color--brand--100: color-mix(
42 | in srgb,
43 | var(--pf-x-base-color) 80%,
44 | black
45 | );
46 | --pf-t--global--color--brand--200: var(--pf-x-base-color);
47 | --pf-t--global--color--brand--300: color-mix(
48 | in srgb,
49 | var(--pf-x-base-color) 80%,
50 | white
51 | );
52 | }
53 | }
54 |
55 | :root {
56 | --pf-t--global--color--severity--none--100: var(
57 | --pf-t--global--color--brand--100
58 | );
59 | --pf-t--global--text--color--link--100: var(
60 | --pf-t--global--color--brand--100
61 | );
62 | --pf-t--global--text--color--link--200: var(
63 | --pf-t--global--color--brand--200
64 | );
65 | --pf-t--global--text--color--link--300: var(
66 | --pf-t--global--color--brand--300
67 | );
68 | --pf-t--global--dark--color--brand--100: color-mix(
69 | in srgb,
70 | var(--pf-x-base-color) 80%,
71 | white
72 | );
73 | --pf-t--global--dark--color--brand--200: var(--pf-x-base-color);
74 | --pf-t--global--dark--color--brand--300: color-mix(
75 | in srgb,
76 | var(--pf-x-base-color) 80%,
77 | black
78 | );
79 | }
80 |
81 | @media (prefers-color-scheme: dark) {
82 | :root {
83 | --pf-t--global--dark--color--brand--100: color-mix(
84 | in srgb,
85 | var(--pf-x-base-color) 80%,
86 | black
87 | );
88 | --pf-t--global--dark--color--brand--200: var(--pf-x-base-color);
89 | --pf-t--global--dark--color--brand--300: color-mix(
90 | in srgb,
91 | var(--pf-x-base-color) 80%,
92 | white
93 | );
94 | }
95 | }
96 |
97 | :root {
98 | --pf-t--global--dark--color--severity--none--100: var(
99 | --pf-t--global--dark--color--brand--100
100 | );
101 | --pf-t--global--dark--text--color--link--100: var(
102 | --pf-t--global--dark--color--brand--100
103 | );
104 | --pf-t--global--dark--text--color--link--200: var(
105 | --pf-t--global--dark--color--brand--200
106 | );
107 | --pf-t--global--dark--text--color--link--300: var(
108 | --pf-t--global--dark--color--brand--300
109 | );
110 | }
111 |
112 | .pf-v6-x-c-brand--main {
113 | max-width: 12.5rem;
114 | }
115 |
116 | .pf-v6-x-c-brand--nav {
117 | height: 50px;
118 | }
119 |
120 | .pf-v6-x-ws-router {
121 | height: 100vh;
122 | }
123 |
124 | .pf-v6-x-u-resize-none {
125 | resize: none;
126 | }
127 |
128 | /* A nested dialog */
129 | .pf-v6-x-m-modal-overlay {
130 | z-index: calc(var(--pf-v6-global--ZIndex--lg) + 1);
131 | }
132 |
133 | /* Backdrop which doesn't overlap */
134 | .pf-v6-x-c-backdrop--nested {
135 | padding-top: var(
136 | --pf-v6-c-page__header--MinHeight
137 | ); /* TODO: This does depend on the navbar being no higher than the min value; in the future, object queries might be used. */
138 | }
139 |
140 | .pf-v6-x-m-gap-md {
141 | gap: var(--pf-v6-global--spacer--md);
142 | }
143 |
144 | /* Prevent unnecessary vertical scrolling in variable height text editors */
145 | .pf-v6-x-m-overflow-y-hidden {
146 | overflow-y: hidden;
147 | }
148 |
149 | /* Re-add old selectable card state styling for plain cards */
150 | .pf-v6-c-card.pf-m-plain.pf-m-selectable {
151 | cursor: pointer;
152 | outline: var(--pf-v6-c-card--m-selectable--BorderWidth) solid
153 | var(--pf-v6-c-card--BorderColor, transparent);
154 | }
155 |
156 | .pf-v6-c-card.pf-m-plain.pf-m-selectable:hover,
157 | .pf-v6-c-card.pf-m-plain.pf-m-selectable:focus {
158 | --pf-v6-c-card--BorderColor: var(
159 | --pf-v6-c-card--m-selectable--hover--BorderColor
160 | );
161 | }
162 |
163 | .pf-v6-x-u-position-absolute {
164 | position: absolute;
165 | }
166 |
167 | @media (max-width: 48rem) {
168 | .pf-v6-c-menu.pf-v6-x-u-position-absolute {
169 | left: 0;
170 | width: 100%;
171 | }
172 | }
173 |
174 | @media (prefers-color-scheme: light) {
175 | .pf-v6-c-brand--dark {
176 | display: none;
177 | }
178 | }
179 |
180 | @media (prefers-color-scheme: dark) {
181 | .pf-v6-c-brand--light {
182 | display: none;
183 | }
184 | }
185 |
186 | .pf-v6-x-dropdown-menu {
187 | z-index: 100;
188 | }
189 |
190 | .pf-v6-c-breadcrumb__list {
191 | flex-wrap: nowrap;
192 | }
193 |
194 | .pf-v6-c-breadcrumb__link {
195 | white-space: nowrap;
196 | }
197 |
--------------------------------------------------------------------------------
/web/logo-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/web/logo-dark.png
--------------------------------------------------------------------------------
/web/logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pojntfx/bofied/c07c42f01e293bb8dbc2db0b01b6dd9cdd3ed1ab/web/logo-light.png
--------------------------------------------------------------------------------