Compare commits

..

54 Commits

Author SHA1 Message Date
4b5bc0bc82 feat: update arcad/edge dependency
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-13 12:08:13 +02:00
dee62184b9 feat: update arcad/edge dependency
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-13 11:35:51 +02:00
76656e8dbf feat: update arcad/edge dependency
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-13 11:28:22 +02:00
41b1619fc1 feat: update arcad/edge dependency
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-13 11:05:12 +02:00
35d5ee868f chore: update sample specs
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-12 11:10:11 +02:00
765257b4b1 feat(datastore): add basic testsuite for agent repository
Some checks reported errors
arcad/emissary/pipeline/head Something is wrong with the build of this commit
2023-04-12 11:09:53 +02:00
2315ee7b61 feat: update arcad/edge dependency
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-11 15:11:15 +02:00
86a6d81e1d chore: execute tests before commit on edge lib update
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-11 12:06:16 +02:00
c4427dfd2b feat(controller,app): sort apps by id 2023-04-11 12:05:51 +02:00
280b0fbd50 feat(controller,app): validate app manifests on app load 2023-04-11 12:05:19 +02:00
8fb86c600f feat: update arcad/edge dependency
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-11 11:13:41 +02:00
12f8b3aa25 chore: add task to update arcad/edge dependency
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-06 20:56:43 +02:00
2d2dc29c84 feat: update arcad/edge dependency 2023-04-06 20:56:00 +02:00
4cf53d9f15 chore: tidy dependencies
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-06 19:25:01 +02:00
34e4769b49 feat: update edge dependency
Some checks reported warnings
arcad/emissary/pipeline/head This commit is unstable
2023-04-06 19:19:23 +02:00
47c2546d54 fix(controller,app): break loop when app is found
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-06 18:25:34 +02:00
21173911fb feat: update edge dependency
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-06 18:17:17 +02:00
b213b8d1ae fix(module,app): handle non existent interface in app url resolver
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-06 15:57:00 +02:00
9dcddc5566 chore(jenkins): cancel older jobs
All checks were successful
arcad/emissary/pipeline/head This commit looks good
2023-04-06 15:12:06 +02:00
9a46c9d3d0 chore: tidy dependencies
Some checks reported errors
arcad/emissary/pipeline/head Something is wrong with the build of this commit
2023-04-06 15:08:23 +02:00
69f183d126 chore(jenkins): do not wait emissary-firmware job completion
Some checks reported errors
arcad/emissary/pipeline/head Something is wrong with the build of this commit
2023-04-06 15:07:35 +02:00
e8829170e5 feat(sqlite): use busy_timeout pragma to prevent database locking errors 2023-04-06 15:06:16 +02:00
253c93dbac fix(module,app): handle host without port in cookie domain identification
Some checks reported errors
arcad/emissary/pipeline/head Something is wrong with the build of this commit
2023-04-06 11:00:35 +02:00
d2f865ccbb chore: tidy dependencies
Some checks reported errors
arcad/emissary/pipeline/head Something is wrong with the build of this commit
2023-04-06 10:44:05 +02:00
7ee4344adc fix(jenkins): do not trigger emissary-firmware with dirty tag
Some checks reported warnings
arcad/emissary/pipeline/head This commit is unstable
2023-04-06 10:40:05 +02:00
06b1235707 fix(module,app): handle hosts without port
Some checks failed
arcad/emissary/pipeline/head There was a failure building this commit
2023-04-06 10:21:52 +02:00
2e1ee44e6a feat(module,app): iface-based app url resolving
Some checks failed
arcad/emissary/pipeline/head There was a failure building this commit
2023-04-05 23:21:43 +02:00
242a247222 feat: add mdns controller
Some checks failed
arcad/emissary/pipeline/head There was a failure building this commit
2023-04-04 20:26:19 +02:00
562d698066 feat(controller, app): add fetch module
Some checks failed
arcad/emissary/pipeline/head There was a failure building this commit
2023-04-02 18:05:53 +02:00
909549f056 feat(agent): do not block execution of controllers on error
Some checks reported errors
arcad/emissary/pipeline/head Something is wrong with the build of this commit
2023-04-01 19:44:00 +02:00
7d551a8312 feat(auth): accept clock skew for token validation
Some checks reported errors
arcad/emissary/pipeline/head Something is wrong with the build of this commit
2023-04-01 19:30:45 +02:00
d02eb91b11 feat(agent): add contactedAt attribute to agent
Some checks reported errors
arcad/emissary/pipeline/head Something is wrong with the build of this commit
2023-04-01 14:33:19 +02:00
d2bcdd2999 feat(storage,agent): add label attribute
Some checks reported errors
arcad/emissary/pipeline/head Something is wrong with the build of this commit
2023-04-01 13:28:18 +02:00
c638fe102b chore(jenkins): use global git config for credentials
Some checks reported errors
arcad/emissary/pipeline/head Something is wrong with the build of this commit
2023-03-31 17:31:44 +02:00
273265c3ef feat(agent,run): start proxy controller after app one
Some checks failed
arcad/emissary/pipeline/head There was a failure building this commit
2023-03-31 17:29:33 +02:00
3e02a9f031 chore(controller,app): refactor app server mutex usage
Some checks failed
arcad/emissary/pipeline/head There was a failure building this commit
2023-03-31 17:28:52 +02:00
b52c687643 feat(sqlite): add default pragmas to dsn 2023-03-31 17:27:54 +02:00
8119a01bf6 chore: add jenkins pipeline
Some checks failed
arcad/emissary/pipeline/head There was a failure building this commit
2023-03-31 17:24:33 +02:00
e5b6c5e949 chore(sqlite): use wal journal mode and enable fk checks by default 2023-03-29 20:58:46 +02:00
9c69dc7ec8 feat(auth): use utc time 2023-03-29 20:49:44 +02:00
4e6b450338 feat(storage,sqlite): log row closing errors 2023-03-29 20:10:06 +02:00
351f22e216 feat(controller,app): automatically redirect requests to cookie domain 2023-03-29 17:29:16 +02:00
854a6ae41b fix(controller,app): include auth configuration in changes detection 2023-03-29 15:32:23 +02:00
a48c2ebe14 fix(controller,app): update app repository on spec changes 2023-03-29 11:27:47 +02:00
cdd78e4031 chore: update go.mod 2023-03-28 20:48:00 +02:00
e832b7dedd feat(controller,app): add app module 2023-03-28 20:43:45 +02:00
3d0b0102c2 chore: only watch json files in internal directory 2023-03-28 11:03:47 +02:00
e60c441d43 feat: move proxy package from arcad/edge 2023-03-28 11:02:53 +02:00
f7d70da174 chore: generate armv7 binaries 2023-03-27 17:17:42 +02:00
24b7519579 chore(packaging): update default agent config 2023-03-27 16:25:35 +02:00
a9e5263a19 feat(cli,api): implements agent query filters flags 2023-03-27 11:05:57 +02:00
56063a66ac chore: release apk and deb packages 2023-03-26 18:58:18 +02:00
0b783c374a feat(controller,sysupgrade): openwrt upgrade controller 2023-03-24 23:17:55 +01:00
97a60e2617 chore: use date based versioning 2023-03-22 18:41:18 +01:00
94 changed files with 3182 additions and 761 deletions

2
.gitignore vendored
View File

@ -4,7 +4,7 @@ dist/
/tools
/tmp
/state.json
/emissary.sqlite
/emissary.sqlite*
/.gitea-release
/agent-key.json
/apps

View File

@ -46,6 +46,9 @@ builds:
- arm64
- arm
- "386"
goarm:
- "6"
- "7"
main: ./cmd/agent
archives:
- id: server
@ -64,7 +67,7 @@ archives:
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
name_template: "{{ .Version }}"
changelog:
sort: asc
filters:

85
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,85 @@
@Library('cadoles') _
pipeline {
agent {
dockerfile {
label 'docker'
filename 'Dockerfile'
dir 'misc/jenkins'
}
}
stages {
stage('Cancel older jobs') {
steps {
script {
def buildNumber = env.BUILD_NUMBER as int
if (buildNumber > 1) milestone(buildNumber - 1)
milestone(buildNumber)
}
}
}
stage('Run unit tests') {
steps {
script {
withCredentials([
usernamePassword([
credentialsId: 'forge-jenkins',
usernameVariable: 'GIT_USERNAME',
passwordVariable: 'GIT_PASSWORD'
])
]) {
sh '''
git config --global credential.https://forge.cadoles.com.username "$GIT_USERNAME"
git config --global credential.https://forge.cadoles.com.helper '!f() { test "$1" = get && echo "password=$GIT_PASSWORD"; }; f'
export GOPRIVATE=forge.cadoles.com/arcad/edge
make test
'''
}
}
}
}
stage('Release') {
when {
anyOf {
branch 'master'
branch 'develop'
}
}
steps {
script {
withCredentials([
usernamePassword([
credentialsId: 'forge-jenkins',
usernameVariable: 'GITEA_RELEASE_USERNAME',
passwordVariable: 'GITEA_RELEASE_PASSWORD'
])
]) {
sh 'make gitea-release'
}
def currentVersion = sh(returnStdout: true, script: 'make full-version').trim()
if (currentVersion.endsWith('-dirty')) {
unstable('Could not trigger emissary-firmware build, dirty version !')
} else {
build(
job: "../emissary-firmware/${env.GIT_BRANCH}",
parameters: [
[$class: 'StringParameterValue', name: 'emissaryRelease', value: currentVersion]
],
wait: false
)
}
}
}
}
}
post {
always {
cleanWs()
}
}
}

View File

@ -1,14 +1,16 @@
LINT_ARGS ?= --timeout 5m
GORELEASER_VERSION ?= v1.13.1
GORELEASER_ARGS ?= release --auto-snapshot --snapshot --rm-dist
GORELEASER_ARGS ?= release --snapshot --rm-dist
GITCHLOG_ARGS ?=
SHELL := /bin/bash
EMISSARY_VERSION ?=
GIT_VERSION ?= $(shell git describe --always)
GIT_VERSION := $(shell git describe --always)
DATE_VERSION := $(shell date +%Y.%-m.%-d)
FULL_VERSION := v$(DATE_VERSION)-$(GIT_VERSION)$(if $(shell git diff --stat),-dirty,)
DOCKER_IMAGE_NAME ?= bornholm/emissary
DOCKER_IMAGE_TAG ?= $(GIT_VERSION)$(if $(shell git diff --stat),-dirty,)
DOCKER_IMAGE_TAG ?= $(FULL_VERSION)
GOTEST_ARGS ?= -short
@ -43,7 +45,7 @@ build-emissary-%: deps ## Build executable
-v \
-ldflags "\
-X 'main.GitRef=$(shell git rev-parse --short HEAD)' \
-X 'main.ProjectVersion=$(shell git describe --always)' \
-X 'main.ProjectVersion=$(FULL_VERSION)' \
-X 'main.BuildDate=$(shell date --utc --rfc-3339=seconds)' \
" \
-o ./bin/$* \
@ -73,7 +75,7 @@ dump-config: build-emissary
.PHONY: goreleaser
goreleaser: deps
( set -o allexport && source .env && set +o allexport && VERSION=$(GORELEASER_VERSION) curl -sfL https://goreleaser.com/static/run | bash /dev/stdin $(GORELEASER_ARGS) )
( set -o allexport && source .env && set +o allexport && VERSION=$(GORELEASER_VERSION) curl -sfL https://goreleaser.com/static/run | GORELEASER_CURRENT_TAG="$(FULL_VERSION)" bash /dev/stdin $(GORELEASER_ARGS) )
.PHONY: start-release
start-release:
@ -122,16 +124,18 @@ gitea-release: tools/gitea-release/bin/gitea-release.sh goreleaser
rm -rf .gitea-release/*
cp dist/*.tar.gz .gitea-release/
cp dist/*.apk .gitea-release/
cp dist/*.deb .gitea-release/
GITEA_RELEASE_PROJECT="emissary" \
GITEA_RELEASE_ORG="arcad" \
GITEA_RELEASE_BASE_URL="https://forge.cadoles.com" \
GITEA_RELEASE_VERSION="$(GIT_VERSION)" \
GITEA_RELEASE_NAME="$(GIT_VERSION)" \
GITEA_RELEASE_VERSION="$(FULL_VERSION)" \
GITEA_RELEASE_NAME="$(FULL_VERSION)" \
GITEA_RELEASE_COMMITISH_TARGET="$(GIT_VERSION)" \
GITEA_RELEASE_IS_DRAFT="false" \
GITEA_RELEASE_BODY="" \
GITEA_RELEASE_ATTACHMENTS="$(shell find .gitea-release/* -type f)" \
GITEA_RELEASE_ATTACHMENTS="$$(find .gitea-release/* -type f)" \
tools/gitea-release/bin/gitea-release.sh
tools/gitea-release/bin/gitea-release.sh:
@ -140,4 +144,23 @@ tools/gitea-release/bin/gitea-release.sh:
chmod +x tools/gitea-release/bin/gitea-release.sh
.emissary-token:
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server auth create-token --role writer > .emissary-token"
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server auth create-token --role writer > .emissary-token"
AGENT_ID ?= 1
load-sample-specs:
cat misc/spec-samples/app.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name app.emissary.cadoles.com
cat misc/spec-samples/proxy.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name proxy.emissary.cadoles.com
cat misc/spec-samples/mdns.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name mdns.emissary.cadoles.com
full-version:
@echo $(FULL_VERSION)
update-edge-lib:
git pull --rebase
GOPRIVATE=forge.cadoles.com/arcad/edge go get -u forge.cadoles.com/arcad/edge
go mod tidy
$(MAKE) test
git add go.mod go.sum
git commit -m "feat: update arcad/edge dependency"
git push

View File

@ -6,6 +6,9 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/command"
"forge.cadoles.com/Cadoles/emissary/internal/command/agent"
"forge.cadoles.com/Cadoles/emissary/internal/command/api"
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/format"
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/spec"
)
// nolint: gochecknoglobals

View File

@ -7,8 +7,9 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/command/api"
"forge.cadoles.com/Cadoles/emissary/internal/command/server"
_ "github.com/jackc/pgx/v5/stdlib"
_ "modernc.org/sqlite"
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/format"
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/spec"
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/sql"
)
// nolint: gochecknoglobals

13
go.mod
View File

@ -3,9 +3,11 @@ module forge.cadoles.com/Cadoles/emissary
go 1.19
require (
forge.cadoles.com/arcad/edge v0.0.0-20230322170544-cf8a3f8ac077
forge.cadoles.com/arcad/edge v0.0.0-20230413100752-dc93c585eb7e
github.com/Masterminds/sprig/v3 v3.2.3
github.com/alecthomas/participle/v2 v2.0.0-beta.5
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
github.com/brutella/dnssd v1.2.6
github.com/btcsuite/btcd/btcutil v1.1.3
github.com/davecgh/go-spew v1.1.1
github.com/denisbrodbeck/machineid v1.0.1
@ -22,7 +24,6 @@ require (
github.com/mitchellh/mapstructure v1.5.0
github.com/pkg/errors v0.9.1
github.com/qri-io/jsonschema v0.2.1
github.com/santhosh-tekuri/jsonschema/v5 v5.1.1
github.com/urfave/cli/v2 v2.24.4
gitlab.com/wpetit/goweb v0.0.0-20230227162855-a1f09bafccb3
gopkg.in/yaml.v3 v3.0.1
@ -30,6 +31,8 @@ require (
)
require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692 // indirect
github.com/dop251/goja_nodejs v0.0.0-20230320130059-dcf93ba651dd // indirect
github.com/gabriel-vasile/mimetype v1.4.1 // indirect
@ -38,10 +41,16 @@ require (
github.com/google/pprof v0.0.0-20230309165930-d61513b1440d // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/mdns v1.0.5 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/igm/sockjs-go/v3 v3.0.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/miekg/dns v1.1.51 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/oklog/ulid/v2 v2.1.0 // indirect
github.com/orcaman/concurrent-map v1.0.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
golang.org/x/net v0.8.0 // indirect
google.golang.org/genproto v0.0.0-20220728213248-dd149ef739b9 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

28
go.sum
View File

@ -54,8 +54,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
forge.cadoles.com/arcad/edge v0.0.0-20230322170544-cf8a3f8ac077 h1:vsYcNHZevZrs0VeOTasvJoqvPynb8OvH+MMpIUvNT6Q=
forge.cadoles.com/arcad/edge v0.0.0-20230322170544-cf8a3f8ac077/go.mod h1:ONd6vyQ0IM0vHi1i+bmZBRc1Fd0BoXMuDdY/+0sZefw=
forge.cadoles.com/arcad/edge v0.0.0-20230413100752-dc93c585eb7e h1:d2PnsAjKnklhI4zj/re2+/FuF0YSpkkNEmfjHl/7G9A=
forge.cadoles.com/arcad/edge v0.0.0-20230413100752-dc93c585eb7e/go.mod h1:Vx4iq/oewXUOkGyi8QKc14clTLNO1sWpb0SjBYELlAs=
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg=
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
@ -84,6 +84,12 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
@ -195,6 +201,8 @@ github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/brutella/dnssd v1.2.6 h1:/0P13JkHLRzeLQkWRPEn4hJCr4T3NfknIFw3aNPIC34=
github.com/brutella/dnssd v1.2.6/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
@ -761,6 +769,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@ -771,6 +781,7 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ=
@ -969,11 +980,14 @@ github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88J
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@ -987,6 +1001,8 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
@ -1169,8 +1185,6 @@ github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2u
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/santhosh-tekuri/jsonschema/v5 v5.1.1 h1:lEOLY2vyGIqKWUI9nzsOJRV3mb3WC9dXYORsLEUcoeY=
github.com/santhosh-tekuri/jsonschema/v5 v5.1.1/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
@ -1182,6 +1196,7 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
@ -1206,6 +1221,8 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
@ -1381,6 +1398,7 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1498,6 +1516,7 @@ golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -1788,6 +1807,7 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=

View File

@ -44,8 +44,6 @@ func (a *Agent) Run(ctx context.Context) error {
if err := a.registerAgent(ctx, client, state); err != nil {
logger.Error(ctx, "could not register agent", logger.E(errors.WithStack(err)))
return
}
logger.Debug(ctx, "state before reconciliation", logger.F("state", state))
@ -81,7 +79,7 @@ func (a *Agent) Reconcile(ctx context.Context, state *State) error {
)
if err := ctrl.Reconcile(ctrlCtx, state); err != nil {
return errors.WithStack(err)
logger.Error(ctx, "could not reconcile", logger.E(errors.WithStack(err)))
}
}

View File

@ -0,0 +1,251 @@
package app
import (
"bytes"
"context"
"database/sql"
"net"
"path/filepath"
"text/template"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
appSpec "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus"
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/module"
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
"forge.cadoles.com/arcad/edge/pkg/module/blob"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
fetchModule "forge.cadoles.com/arcad/edge/pkg/module/fetch"
netModule "forge.cadoles.com/arcad/edge/pkg/module/net"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"github.com/Masterminds/sprig/v3"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
const defaultSQLiteParams = "?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000"
func (c *Controller) getHandlerOptions(ctx context.Context, appKey string, specs *spec.Spec) ([]edgeHTTP.HandlerOptionFunc, error) {
dataDir, err := c.ensureAppDataDir(ctx, appKey)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve app data dir")
}
dbFile := filepath.Join(dataDir, appKey+".sqlite")
db, err := sqlite.Open(dbFile + defaultSQLiteParams)
if err != nil {
return nil, errors.Wrapf(err, "could not open database file '%s'", dbFile)
}
keySet, err := getAuthKeySet(specs.Config)
if err != nil {
return nil, errors.Wrap(err, "could not retrieve auth key set")
}
bus := memory.NewBus()
modules := c.getAppModules(bus, db, specs, keySet)
options := []edgeHTTP.HandlerOptionFunc{
edgeHTTP.WithBus(bus),
edgeHTTP.WithServerModules(modules...),
}
return options, nil
}
func getAuthKeySet(config *spec.Config) (jwk.Set, error) {
keySet := jwk.NewSet()
if config == nil {
return nil, nil
}
auth := config.Auth
if auth == nil {
return nil, nil
}
switch {
case auth.Local != nil:
var (
key jwk.Key
err error
)
switch typedKey := auth.Local.Key.(type) {
case string:
key, err = jwk.FromRaw([]byte(typedKey))
if err != nil {
return nil, errors.Wrap(err, "could not parse local auth key")
}
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
return nil, errors.WithStack(err)
}
default:
return nil, errors.Errorf("unexpected key type '%T'", auth.Local.Key)
}
if err := keySet.AddKey(key); err != nil {
return nil, errors.WithStack(err)
}
}
return keySet, nil
}
func createResolveAppURL(specs *spec.Spec) (ResolveAppURLFunc, error) {
rawIfaceMappings := make(map[string]string, 0)
if specs.Config != nil && specs.Config.AppURLResolving != nil && specs.Config.AppURLResolving.IfaceMappings != nil {
rawIfaceMappings = specs.Config.AppURLResolving.IfaceMappings
}
ifaceMappings := make(map[string]*template.Template, len(rawIfaceMappings))
for iface, rawTemplate := range rawIfaceMappings {
tmpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(rawTemplate)
if err != nil {
return nil, errors.Wrapf(err, "could not parse iface '%s' template", iface)
}
ifaceMappings[iface] = tmpl
}
defaultRawTemplate := `http://{{ .DeviceIP }}:{{ .AppPort }}`
if specs.Config != nil && specs.Config.AppURLResolving != nil && specs.Config.AppURLResolving.DefaultURLTemplate != "" {
defaultRawTemplate = specs.Config.AppURLResolving.DefaultURLTemplate
}
defaultTemplate, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(defaultRawTemplate)
if err != nil {
return nil, errors.WithStack(err)
}
return func(ctx context.Context, manifest *app.Manifest, from string) (string, error) {
var (
urlTemplate *template.Template
deviceIP net.IP
)
fromIP := net.ParseIP(from)
if fromIP != nil {
LOOP:
for ifaceName, ifaceTmpl := range ifaceMappings {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
logger.Error(
ctx, "could not find interface",
logger.E(errors.WithStack(err)), logger.F("iface", ifaceName),
)
continue
}
addresses, err := iface.Addrs()
if err != nil {
logger.Error(
ctx, "could not list interface addresses",
logger.E(errors.WithStack(err)),
logger.F("iface", iface.Name),
)
continue
}
for _, addr := range addresses {
ifaIP, network, err := net.ParseCIDR(addr.String())
if err != nil {
logger.Error(
ctx, "could not parse interface ip",
logger.E(errors.WithStack(err)),
logger.F("iface", iface.Name),
)
continue
}
if !network.Contains(fromIP) {
continue
}
deviceIP = ifaIP
urlTemplate = ifaceTmpl
break LOOP
}
}
}
if urlTemplate == nil {
urlTemplate = defaultTemplate
}
if deviceIP == nil {
deviceIP = net.ParseIP("127.0.0.1")
}
var appEntry *spec.AppEntry
for appID, entry := range specs.Apps {
if manifest.ID != app.ID(appID) {
continue
}
appEntry = &entry
break
}
if appEntry == nil {
return "", errors.Errorf("could not find app '%s' in specs", manifest.ID)
}
_, port, err := net.SplitHostPort(appEntry.Address)
if err != nil {
return "", errors.WithStack(err)
}
data := struct {
Manifest *app.Manifest
Specs *spec.Spec
DeviceIP string
AppPort string
}{
Manifest: manifest,
Specs: specs,
DeviceIP: deviceIP.String(),
AppPort: port,
}
var buf bytes.Buffer
if err := urlTemplate.Execute(&buf, data); err != nil {
return "", errors.WithStack(err)
}
return buf.String(), nil
}, nil
}
func (c *Controller) getAppModules(bus bus.Bus, db *sql.DB, spec *appSpec.Spec, keySet jwk.Set) []app.ServerModuleFactory {
ds := sqlite.NewDocumentStoreWithDB(db)
bs := sqlite.NewBlobStoreWithDB(db)
return []app.ServerModuleFactory{
module.ContextModuleFactory(),
module.ConsoleModuleFactory(),
cast.CastModuleFactory(),
module.LifecycleModuleFactory(),
netModule.ModuleFactory(bus),
module.RPCModuleFactory(bus),
module.StoreModuleFactory(ds),
blob.ModuleFactory(bus, bs),
authModule(keySet),
appModule.ModuleFactory(c.appRepository),
fetchModule.ModuleFactory(bus),
}
}

View File

@ -0,0 +1,73 @@
package app
import (
"context"
"testing"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
"forge.cadoles.com/arcad/edge/pkg/app"
"github.com/pkg/errors"
)
func TestCreateResolveAppURL(t *testing.T) {
specs := &spec.Spec{
Apps: map[string]spec.AppEntry{
"app.arcad.test": {
Address: ":8080",
},
"app.arcad.foo": {
Address: ":8081",
},
"app.arcad.bar": {
Address: ":8082",
},
},
Config: &spec.Config{
AppURLResolving: &spec.AppURLResolving{
IfaceMappings: map[string]string{
"lo": "http://{{ .DeviceIP }}:{{ .AppPort }}",
"does-not-exists": "http://{{ .DeviceIP }}:{{ .AppPort }}",
},
DefaultURLTemplate: `http://{{ last ( splitList "." ( toString .Manifest.ID ) ) }}.arcad.local`,
},
},
}
resolveAppURL, err := createResolveAppURL(specs)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
manifest := &app.Manifest{
ID: "app.arcad.test",
}
ctx := context.Background()
url, err := resolveAppURL(ctx, manifest, "127.0.0.2")
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := "http://127.0.0.1:8080", url; e != g {
t.Errorf("url: expected '%s', got '%s", e, g)
}
url, err = resolveAppURL(ctx, manifest, "")
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := "http://test.arcad.local", url; e != g {
t.Errorf("url: expected '%s', got '%s", e, g)
}
url, err = resolveAppURL(ctx, manifest, "192.168.0.100")
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := "http://test.arcad.local", url; e != g {
t.Errorf("url: expected '%s', got '%s", e, g)
}
}

View File

@ -0,0 +1,137 @@
package app
import (
"context"
"sort"
"sync"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bundle"
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type ResolveAppURLFunc func(context.Context, *app.Manifest, string) (string, error)
type AppRepository struct {
resolveAppURL ResolveAppURLFunc
bundles []string
mutex sync.RWMutex
}
// Get implements app.Repository
func (r *AppRepository) Get(ctx context.Context, id app.ID) (*app.Manifest, error) {
r.mutex.RLock()
defer r.mutex.RUnlock()
manifest, err := r.findManifest(ctx, id)
if err != nil {
return nil, errors.WithStack(err)
}
return manifest, nil
}
// GetURL implements app.Repository
func (r *AppRepository) GetURL(ctx context.Context, id app.ID, from string) (string, error) {
r.mutex.RLock()
defer r.mutex.RUnlock()
manifest, err := r.findManifest(ctx, id)
if err != nil {
return "", errors.WithStack(err)
}
url, err := r.resolveAppURL(ctx, manifest, from)
if err != nil {
return "", errors.WithStack(err)
}
return url, nil
}
// List implements app.Repository
func (r *AppRepository) List(ctx context.Context) ([]*app.Manifest, error) {
r.mutex.RLock()
defer r.mutex.RUnlock()
manifests := make([]*app.Manifest, 0)
for _, path := range r.bundles {
bundleCtx := logger.With(ctx, logger.F("path", path))
bundle, err := bundle.FromPath(path)
if err != nil {
logger.Error(bundleCtx, "could not load bundle", logger.E(errors.WithStack(err)))
continue
}
manifest, err := app.LoadManifest(bundle)
if err != nil {
logger.Error(bundleCtx, "could not load manifest", logger.E(errors.WithStack(err)))
continue
}
manifests = append(manifests, manifest)
}
sort.Sort(ByID(manifests))
return manifests, nil
}
func (r *AppRepository) Update(resolveAppURL ResolveAppURLFunc, bundles []string) {
r.mutex.Lock()
defer r.mutex.Unlock()
r.resolveAppURL = resolveAppURL
r.bundles = bundles
}
func (r *AppRepository) findManifest(ctx context.Context, id app.ID) (*app.Manifest, error) {
for _, path := range r.bundles {
bundleCtx := logger.With(ctx, logger.F("path", path))
bundle, err := bundle.FromPath(path)
if err != nil {
logger.Error(bundleCtx, "could not load bundle", logger.E(errors.WithStack(err)))
continue
}
manifest, err := app.LoadManifest(bundle)
if err != nil {
logger.Error(bundleCtx, "could not load manifest", logger.E(errors.WithStack(err)))
continue
}
if manifest.ID != id {
continue
}
return manifest, nil
}
return nil, errors.WithStack(appModule.ErrNotFound)
}
func NewAppRepository() *AppRepository {
return &AppRepository{
resolveAppURL: func(ctx context.Context, m *app.Manifest, from string) (string, error) {
return "", errors.New("unavailable")
},
bundles: []string{},
}
}
var _ appModule.Repository = &AppRepository{}
type ByID []*app.Manifest
func (a ByID) Len() int { return len(a) }
func (a ByID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByID) Less(i, j int) bool { return a[i].ID > a[j].ID }

View File

@ -0,0 +1,65 @@
package app
import (
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/module/auth"
"github.com/dop251/goja"
"github.com/pkg/errors"
)
const (
RoleVisitor string = "visitor"
RoleUser string = "user"
RoleSuperuser string = "superuser"
RoleAdmin string = "admin"
RoleSuperadmin string = "superadmin"
)
func authModule(keySet jwk.Set) app.ServerModuleFactory {
return module.Extends(
auth.ModuleFactory(
auth.WithJWT(func() (jwk.Set, error) {
return keySet, nil
}),
),
func(o *goja.Object) {
if err := o.Set("CLAIM_TENANT", "arcad_tenant"); err != nil {
panic(errors.New("could not set 'CLAIM_TENANT' property"))
}
if err := o.Set("CLAIM_ENTRYPOINT", "arcad_entrypoint"); err != nil {
panic(errors.New("could not set 'CLAIM_ENTRYPOINT' property"))
}
if err := o.Set("CLAIM_ROLE", "arcad_role"); err != nil {
panic(errors.New("could not set 'CLAIM_ROLE' property"))
}
if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil {
panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property"))
}
if err := o.Set("ROLE_VISITOR", RoleVisitor); err != nil {
panic(errors.New("could not set 'ROLE_VISITOR' property"))
}
if err := o.Set("ROLE_USER", RoleUser); err != nil {
panic(errors.New("could not set 'ROLE_USER' property"))
}
if err := o.Set("ROLE_SUPERUSER", RoleSuperuser); err != nil {
panic(errors.New("could not set 'ROLE_SUPERUSER' property"))
}
if err := o.Set("ROLE_ADMIN", RoleAdmin); err != nil {
panic(errors.New("could not set 'ROLE_ADMIN' property"))
}
if err := o.Set("ROLE_SUPERADMIN", RoleSuperadmin); err != nil {
panic(errors.New("could not set 'ROLE_SUPERADMIN' property"))
}
},
)
}

View File

@ -8,24 +8,25 @@ import (
"path/filepath"
"forge.cadoles.com/Cadoles/emissary/internal/agent"
"forge.cadoles.com/Cadoles/emissary/internal/spec/app"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bundle"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"github.com/mitchellh/hashstructure/v2"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type serverEntry struct {
SpecHash uint64
Server *Server
AppDefHash uint64
Server *Server
}
type Controller struct {
client *http.Client
downloadDir string
dataDir string
servers map[string]*serverEntry
client *http.Client
downloadDir string
dataDir string
servers map[string]*serverEntry
appRepository *AppRepository
}
// Name implements node.Controller.
@ -35,9 +36,9 @@ func (c *Controller) Name() string {
// Reconcile implements node.Controller.
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
appSpec := app.NewSpec()
appSpec := spec.NewSpec()
if err := state.GetSpec(app.NameApp, appSpec); err != nil {
if err := state.GetSpec(spec.Name, appSpec); err != nil {
if errors.Is(err, agent.ErrSpecNotFound) {
logger.Info(ctx, "could not find app spec")
@ -56,7 +57,7 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
return nil
}
func (c *Controller) stopAllApps(ctx context.Context, spec *app.Spec) {
func (c *Controller) stopAllApps(ctx context.Context, spec *spec.Spec) {
if len(c.servers) > 0 {
logger.Info(ctx, "stopping all apps")
}
@ -76,122 +77,169 @@ func (c *Controller) stopAllApps(ctx context.Context, spec *app.Spec) {
}
}
func (c *Controller) updateApps(ctx context.Context, spec *app.Spec) {
func (c *Controller) updateApps(ctx context.Context, specs *spec.Spec) {
// Stop and remove obsolete apps
for appID, entry := range c.servers {
if _, exists := spec.Apps[appID]; exists {
for appKey, server := range c.servers {
if _, exists := specs.Apps[appKey]; exists {
continue
}
logger.Info(ctx, "stopping app", logger.F("appID", appID))
logger.Info(ctx, "stopping app", logger.F("appKey", appKey))
if err := entry.Server.Stop(); err != nil {
if err := server.Server.Stop(); err != nil {
logger.Error(
ctx, "error while stopping app",
logger.F("gatewayID", appID),
logger.F("appKey", appKey),
logger.E(errors.WithStack(err)),
)
delete(c.servers, appID)
delete(c.servers, appKey)
}
}
// (Re)start apps
for appID, appSpec := range spec.Apps {
appCtx := logger.With(ctx, logger.F("appID", appID))
if err := c.updateAppRepository(ctx, specs); err != nil {
logger.Error(
ctx, "could not update app repository",
logger.E(errors.WithStack(err)),
)
if err := c.updateApp(ctx, appID, appSpec, spec.Auth); err != nil {
return
}
// (Re)start apps if necessary
for appKey := range specs.Apps {
appCtx := logger.With(ctx, logger.F("appKey", appKey))
if err := c.updateApp(ctx, specs, appKey); err != nil {
logger.Error(appCtx, "could not update app", logger.E(errors.WithStack(err)))
continue
}
}
}
func (c *Controller) updateApp(ctx context.Context, appID string, appSpec app.AppEntry, auth *app.Auth) (err error) {
newAppSpecHash, err := hashstructure.Hash(appSpec, hashstructure.FormatV2, nil)
func (c *Controller) updateAppRepository(ctx context.Context, specs *spec.Spec) error {
bundles := make([]string, 0, len(specs.Apps))
for appKey, app := range specs.Apps {
path := c.getAppBundlePath(appKey, app.Format)
bundles = append(bundles, path)
}
resolveAppURL, err := createResolveAppURL(specs)
if err != nil {
return errors.WithStack(err)
}
bundle, sha256sum, err := c.ensureAppBundle(ctx, appID, appSpec)
if err != nil {
return errors.Wrap(err, "could not download app bundle")
}
dataDir, err := c.ensureAppDataDir(ctx, appID)
if err != nil {
return errors.Wrap(err, "could not retrieve app data dir")
}
var entry *serverEntry
entry, exists := c.servers[appID]
if !exists {
logger.Info(ctx, "app currently not running")
} else if sha256sum != appSpec.SHA256Sum {
logger.Info(
ctx, "bundle hash mismatch, stopping app",
logger.F("currentHash", sha256sum),
logger.F("specHash", appSpec.SHA256Sum),
)
if err := entry.Server.Stop(); err != nil {
return errors.Wrap(err, "could not stop app")
}
entry = nil
}
if entry == nil {
dbFile := filepath.Join(dataDir, appID+".sqlite")
db, err := sqlite.Open(dbFile)
if err != nil {
return errors.Wrapf(err, "could not opend database file '%s'", dbFile)
}
entry = &serverEntry{
Server: NewServer(bundle, db, auth),
SpecHash: 0,
}
c.servers[appID] = entry
}
specChanged := newAppSpecHash != entry.SpecHash
if entry.Server.Running() && !specChanged {
return nil
}
if specChanged && entry.SpecHash != 0 {
logger.Info(
ctx, "restarting app",
logger.F("address", appSpec.Address),
)
} else {
logger.Info(
ctx, "starting app",
logger.F("address", appSpec.Address),
)
}
if err := entry.Server.Start(ctx, appSpec.Address); err != nil {
delete(c.servers, appID)
return errors.Wrap(err, "could not start app")
}
entry.SpecHash = newAppSpecHash
c.appRepository.Update(resolveAppURL, bundles)
return nil
}
func (c *Controller) ensureAppBundle(ctx context.Context, appID string, spec app.AppEntry) (bundle.Bundle, string, error) {
func (c *Controller) updateApp(ctx context.Context, specs *spec.Spec, appKey string) (err error) {
appEntry := specs.Apps[appKey]
appDef := struct {
App spec.AppEntry
Config *spec.Config
}{
App: appEntry,
Config: specs.Config,
}
newAppDefHash, err := hashstructure.Hash(appDef, hashstructure.FormatV2, nil)
if err != nil {
return errors.WithStack(err)
}
bundle, sha256sum, err := c.ensureAppBundle(ctx, appKey, appEntry)
if err != nil {
return errors.Wrap(err, "could not download app bundle")
}
server, exists := c.servers[appKey]
if !exists {
logger.Info(ctx, "app currently not running")
} else if sha256sum != appEntry.SHA256Sum {
logger.Info(
ctx, "bundle hash mismatch, stopping app",
logger.F("currentHash", sha256sum),
logger.F("specHash", appEntry.SHA256Sum),
)
if err := server.Server.Stop(); err != nil {
return errors.Wrap(err, "could not stop app")
}
server = nil
}
newServerEntry := func() (*serverEntry, error) {
options, err := c.getHandlerOptions(ctx, appKey, specs)
if err != nil {
return nil, errors.Wrap(err, "could not create handler options")
}
server = &serverEntry{
Server: NewServer(bundle, specs.Config, options...),
AppDefHash: 0,
}
return server, nil
}
if server == nil {
serverEntry, err := newServerEntry()
if err != nil {
return errors.WithStack(err)
}
c.servers[appKey] = serverEntry
}
defChanged := newAppDefHash != server.AppDefHash
if server.Server.Running() && !defChanged {
return nil
}
if defChanged && server.AppDefHash != 0 {
logger.Info(
ctx, "restarting app",
logger.F("address", appEntry.Address),
)
if err := server.Server.Stop(); err != nil {
return errors.WithStack(err)
}
serverEntry, err := newServerEntry()
if err != nil {
return errors.WithStack(err)
}
c.servers[appKey] = serverEntry
} else {
logger.Info(
ctx, "starting app",
logger.F("address", appEntry.Address),
)
}
if err := server.Server.Start(ctx, appEntry.Address); err != nil {
delete(c.servers, appKey)
return errors.Wrap(err, "could not start app")
}
server.AppDefHash = newAppDefHash
return nil
}
func (c *Controller) ensureAppBundle(ctx context.Context, appID string, spec spec.AppEntry) (bundle.Bundle, string, error) {
if err := os.MkdirAll(c.downloadDir, os.ModePerm); err != nil {
return nil, "", errors.WithStack(err)
}
bundlePath := filepath.Join(c.downloadDir, appID+"."+spec.Format)
bundlePath := c.getAppBundlePath(appID, spec.Format)
_, err := os.Stat(bundlePath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
@ -229,7 +277,21 @@ func (c *Controller) ensureAppBundle(ctx context.Context, appID string, spec app
return nil, "", errors.WithStack(err)
}
return bdle, "", nil
manifest, err := app.LoadManifest(bdle)
if err != nil {
return nil, "", errors.WithStack(err)
}
valid, err := validateManifest(manifest)
if err != nil {
return nil, "", errors.WithStack(err)
}
if !valid {
return nil, "", errors.New("bundle's manifest is invalid")
}
return bdle, spec.SHA256Sum, nil
}
func (c *Controller) downloadFile(url string, sha256sum string, dest string) error {
@ -285,6 +347,10 @@ func (c *Controller) ensureAppDataDir(ctx context.Context, appID string) (string
return dataDir, nil
}
func (c *Controller) getAppBundlePath(appKey string, format string) string {
return filepath.Join(c.downloadDir, appKey+"."+format)
}
func NewController(funcs ...OptionFunc) *Controller {
opts := defaultOptions()
for _, fn := range funcs {
@ -292,10 +358,11 @@ func NewController(funcs ...OptionFunc) *Controller {
}
return &Controller{
client: opts.Client,
downloadDir: opts.DownloadDir,
dataDir: opts.DataDir,
servers: make(map[string]*serverEntry),
client: opts.Client,
downloadDir: opts.DownloadDir,
dataDir: opts.DataDir,
servers: make(map[string]*serverEntry),
appRepository: NewAppRepository(),
}
}

View File

@ -0,0 +1,19 @@
package app
import (
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/app/metadata"
"github.com/pkg/errors"
)
func validateManifest(manifest *app.Manifest) (bool, error) {
valid, err := manifest.Validate(
metadata.WithMinimumRoleValidator(RoleVisitor, RoleUser, RoleSuperuser, RoleAdmin, RoleSuperadmin),
metadata.WithNamedPathsValidator(metadata.NamedPathAdmin, metadata.NamedPathIcon),
)
if err != nil {
return false, errors.WithStack(err)
}
return valid, nil
}

View File

@ -2,97 +2,71 @@ package app
import (
"context"
"database/sql"
"net"
"net/http"
"strings"
"sync"
"time"
appSpec "forge.cadoles.com/Cadoles/emissary/internal/spec/app"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/bus"
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
appSpec "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
"forge.cadoles.com/Cadoles/emissary/internal/proxy/wildcard"
edgeHTTP "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/module/auth"
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"forge.cadoles.com/arcad/edge/pkg/module/net"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/arcad/edge/pkg/bundle"
"github.com/dop251/goja"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/chi/v5"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
_ "forge.cadoles.com/Cadoles/emissary/internal/imports/passwd"
)
const defaultCookieDuration time.Duration = 24 * time.Hour
type Server struct {
bundle bundle.Bundle
db *sql.DB
server *http.Server
serverMutex sync.RWMutex
auth *appSpec.Auth
keySet jwk.Set
bundle bundle.Bundle
handlerOptions []edgeHTTP.HandlerOptionFunc
server *http.Server
serverMutex sync.RWMutex
config *appSpec.Config
}
func (s *Server) Start(ctx context.Context, addr string) (err error) {
if s.server != nil {
if s.Running() {
if err := s.Stop(); err != nil {
return errors.WithStack(err)
}
}
s.serverMutex.Lock()
defer s.serverMutex.Unlock()
router := chi.NewRouter()
router.Use(middleware.Logger)
bus := memory.NewBus()
ds := sqlite.NewDocumentStoreWithDB(s.db)
bs := sqlite.NewBlobStoreWithDB(s.db)
handler := edgeHTTP.NewHandler(
edgeHTTP.WithBus(bus),
edgeHTTP.WithServerModules(s.getAppModules(bus, ds, bs)...),
)
handler := edgeHTTP.NewHandler(s.handlerOptions...)
if err := handler.Load(s.bundle); err != nil {
return errors.Wrap(err, "could not load app bundle")
}
if s.auth != nil {
if s.auth.Local != nil {
var rawKey any = s.auth.Local.Key
if strKey, ok := rawKey.(string); ok {
rawKey = []byte(strKey)
}
key, err := jwk.FromRaw(rawKey)
if err != nil {
return errors.WithStack(err)
}
if err := key.Set(jwk.AlgorithmKey, jwa.HS256); err != nil {
return errors.WithStack(err)
}
keySet := jwk.NewSet()
if err := keySet.AddKey(key); err != nil {
return errors.WithStack(err)
}
s.keySet = keySet
router.Handle("/auth/*", authHTTP.NewLocalHandler(
jwa.HS256, key,
authHTTP.WithRoutePrefix("/auth"),
authHTTP.WithAccounts(s.auth.Local.Accounts...),
if s.config != nil {
if s.config.UnexpectedHostRedirect != nil {
router.Use(unexpectedHostRedirect(
s.config.UnexpectedHostRedirect.HostTarget,
s.config.UnexpectedHostRedirect.AcceptedHostPatterns...,
))
}
if s.config.Auth != nil {
if err := s.configureAuth(router, s.config.Auth); err != nil {
return errors.WithStack(err)
}
}
}
router.Handle("/*", handler)
@ -126,9 +100,7 @@ func (s *Server) Start(ctx context.Context, addr string) (err error) {
}
}()
s.serverMutex.Lock()
s.server = server
s.serverMutex.Unlock()
return nil
}
@ -141,66 +113,116 @@ func (s *Server) Running() bool {
}
func (s *Server) Stop() error {
if !s.Running() {
return nil
}
s.serverMutex.Lock()
defer s.serverMutex.Unlock()
if s.server == nil {
return nil
}
defer func() {
s.serverMutex.Lock()
s.server = nil
s.serverMutex.Unlock()
}()
if err := s.server.Close(); err != nil {
panic(errors.WithStack(err))
s.server = nil
return errors.WithStack(err)
}
s.server = nil
return nil
}
func (s *Server) configureAuth(router chi.Router, auth *spec.Auth) error {
switch {
case auth.Local != nil:
var rawKey any = auth.Local.Key
if strKey, ok := rawKey.(string); ok {
rawKey = []byte(strKey)
}
key, err := jwk.FromRaw(rawKey)
if err != nil {
return errors.WithStack(err)
}
cookieDuration := defaultCookieDuration
if auth.Local.CookieDuration != "" {
cookieDuration, err = time.ParseDuration(auth.Local.CookieDuration)
if err != nil {
return errors.WithStack(err)
}
}
router.Handle("/auth/*", authHTTP.NewLocalHandler(
jwa.HS256, key,
authHTTP.WithRoutePrefix("/auth"),
authHTTP.WithAccounts(auth.Local.Accounts...),
authHTTP.WithCookieOptions(getCookieDomain, cookieDuration),
))
}
return nil
}
func (s *Server) getAppModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore) []app.ServerModuleFactory {
return []app.ServerModuleFactory{
module.ContextModuleFactory(),
module.ConsoleModuleFactory(),
cast.CastModuleFactory(),
module.LifecycleModuleFactory(),
net.ModuleFactory(bus),
module.RPCModuleFactory(bus),
module.StoreModuleFactory(ds),
module.BlobModuleFactory(bus, bs),
module.Extends(
auth.ModuleFactory(
auth.WithJWT(s.getJWTKeySet),
),
func(o *goja.Object) {
if err := o.Set("CLAIM_TENANT", "arcad_tenant"); err != nil {
panic(errors.New("could not set 'CLAIM_TENANT' property"))
}
if err := o.Set("CLAIM_ENTRYPOINT", "arcad_entrypoint"); err != nil {
panic(errors.New("could not set 'CLAIM_ENTRYPOINT' property"))
}
if err := o.Set("CLAIM_ROLE", "arcad_role"); err != nil {
panic(errors.New("could not set 'CLAIM_ROLE' property"))
}
if err := o.Set("CLAIM_PREFERRED_USERNAME", "preferred_username"); err != nil {
panic(errors.New("could not set 'CLAIM_PREFERRED_USERNAME' property"))
}
},
),
}
}
func (s *Server) getJWTKeySet() (jwk.Set, error) {
return s.keySet, nil
}
func NewServer(bundle bundle.Bundle, db *sql.DB, auth *appSpec.Auth) *Server {
func NewServer(bundle bundle.Bundle, config *spec.Config, handlerOptions ...edgeHTTP.HandlerOptionFunc) *Server {
return &Server{
bundle: bundle,
db: db,
auth: auth,
bundle: bundle,
config: config,
handlerOptions: handlerOptions,
}
}
func getCookieDomain(r *http.Request) (string, error) {
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
host = r.Host
}
// If host is an IP address
if wildcard.Match(host, "*.*.*.*") {
return "", nil
}
// If host is an domain, return top level domain
domainParts := strings.Split(host, ".")
if len(domainParts) >= 2 {
topLevelDomain := strings.Join(domainParts[len(domainParts)-2:], ".")
return topLevelDomain, nil
}
// By default, return host
return host, nil
}
func unexpectedHostRedirect(hostTarget string, acceptedHostPatterns ...string) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
host, port, err := net.SplitHostPort(r.Host)
if err != nil {
host = r.Host
}
matched := wildcard.MatchAny(host, acceptedHostPatterns...)
if !matched {
url := r.URL
url.Host = hostTarget
if port != "" {
url.Host += ":" + port
}
http.Redirect(w, r, url.String(), http.StatusTemporaryRedirect)
return
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}

View File

@ -1,4 +1,4 @@
package app
package spec
import (
_ "embed"
@ -11,7 +11,7 @@ import (
var schema []byte
func init() {
if err := spec.Register(NameApp, schema); err != nil {
if err := spec.Register(Name, schema); err != nil {
panic(errors.WithStack(err))
}
}

View File

@ -0,0 +1,135 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://app.edge.emissary.cadoles.com/spec.json",
"title": "AppSpec",
"description": "Emissary 'App' specification",
"type": "object",
"properties": {
"apps": {
"type": "object",
"patternProperties": {
".*": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"sha256sum": {
"type": "string"
},
"address": {
"type": "string"
},
"format": {
"type": "string",
"enum": [
"zip",
"tar.gz"
]
}
},
"required": [
"url",
"sha256sum",
"address",
"format"
],
"additionalProperties": false
}
}
},
"config": {
"type": "object",
"properties": {
"appUrlResolving": {
"type": "object",
"properties": {
"ifaceMappings": {
"type": "object",
"patternProperties": {
".*": {
"type": "string"
}
}
},
"defaultUrlTemplate": {
"type": "string"
}
},
"required": ["defaultUrlTemplate"],
"additionalProperties": false
},
"unexpectedHostRedirect": {
"type": "object",
"properties": {
"acceptedHostPatterns": {
"type": "array",
"items": {
"type": "string"
}
},
"hostTarget": {
"type": "string"
}
},
"required": ["acceptedHostPatterns", "hostTarget"],
"additionalProperties": false
},
"auth": {
"type": "object",
"properties": {
"local": {
"type": "object",
"properties": {
"key": {
"type": ["object", "string"]
},
"accounts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"algo": {
"type": "string"
},
"claims": {
"type": "object"
}
},
"required": [
"username",
"password",
"algo"
]
}
},
"cookieDomain": {
"type": "string"
},
"cookieDuration": {
"type": "string"
}
},
"required": [
"key"
],
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"required": [
"apps"
],
"additionalProperties": false
}

View File

@ -0,0 +1,71 @@
package spec
import (
"forge.cadoles.com/Cadoles/emissary/internal/spec"
edgeAuth "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
)
const Name spec.Name = "app.emissary.cadoles.com"
type Spec struct {
Revision int `json:"revision"`
Apps map[string]AppEntry `json:"apps"`
Config *Config `json:"config"`
}
type AppEntry struct {
URL string `json:"url"`
SHA256Sum string `json:"sha256sum"`
Address string `json:"address"`
Format string `json:"format"`
}
type Auth struct {
Local *LocalAuth `json:"local,omitempty"`
}
type LocalAuth struct {
Key any `json:"key"`
Accounts []edgeAuth.LocalAccount `json:"accounts"`
CookieDomain string `json:"cookieDomain"`
CookieDuration string `json:"cookieDuration"`
}
type Config struct {
Auth *Auth `json:"auth"`
UnexpectedHostRedirect *UnexpectedHostRedirect `json:"unexpectedHostRedirect"`
AppURLResolving *AppURLResolving `json:"appUrlResolving"`
}
type UnexpectedHostRedirect struct {
AcceptedHostPatterns []string `json:"acceptedHostPatterns"`
HostTarget string `json:"hostTarget"`
}
type AppURLResolving struct {
IfaceMappings map[string]string `json:"ifaceMappings"`
DefaultURLTemplate string `json:"defaultUrlTemplate"`
}
func (s *Spec) SpecName() spec.Name {
return Name
}
func (s *Spec) SpecRevision() int {
return s.Revision
}
func (s *Spec) SpecData() map[string]any {
return map[string]any{
"apps": s.Apps,
"config": s.Config,
}
}
func NewSpec() *Spec {
return &Spec{
Revision: -1,
}
}
var _ spec.Spec = &Spec{}

View File

@ -0,0 +1,54 @@
{
"name": "app.emissary.cadoles.com",
"data": {
"apps": {
"edge.sdk.client.test": {
"url": "http://example.com/edge.sdk.client.test_0.0.0.zip",
"sha256sum": "58019192dacdae17755707719707db007e26dac856102280583fbd18427dd352",
"address": ":8081",
"format": "zip"
}
},
"config": {
"auth": {
"local": {
"key": {
"d": "YOre0WZefGfUGFvDg42oL5Oad5Zsb1N_hqPyLVM5ajpTZzcHpB3wT6In9tFO_VshB6lxVtPA9ckPkpMTFY7ygt1Yomc1HkoOKRtmIaqdr4VgNQifU-4yiLiJkSbdYSeMV-KkkN8mGR1keJpJeS34W1X0W6CkU2nw7F5VueBCJfWJA0funRfuWdI68MTUgT9kRZFp-SfvptvRL6jVYHV_5hqxzHCvgEdBSF6QKwx4M6P6QBMt7ft6uMLmFx9abKFw2V51hX3PkxiSepVB3w5CYg4HtS3AHX6bILL4m0R2pdTIkap7i3tkH_xAOuKWt8D6JhadI8X1rEAwXmCS5KrRgQ",
"dp": "U0HfvBC6hk-SCpuotGIv3vbHCVt1aF3SHK0y32EYCOe8e_9G6YCEILfcvEJ5fiOCc2kvx6TasHQu4qj1uWRKenZlK1sJ6KDybGCkZL1D3jYnbeLZYBuWBL__YbZiST3ewbxzj_EDMWiZ8sUltahza_1weSgg8auSzTHS2LJBHIE",
"dq": "hVom4ScDxgqhCsQNVpZlN7M3v0tgWjl_gTOHjOyzKCHQJeC0QmJJaMKkQZPWJ8jjLqy7VwVpqC2nZU7QDuX1Cq5eJDQcXi9XtaAfIBico9WcYDre6mDyhL588YHpekyRke8HnZ810iesr0G3gU1h0QvZVVuW-pXTJOXhZTt6nFc",
"e": "AQAB",
"kty": "RSA",
"n": "vPnpkE3-HfNgJSru_K40LstkjiG2Bq_Tt-m0d_yUBBSbirFxF3qH4EXi7WrtZdeDahg2iV2BvpbVVj9GlmGo9OLol6jc7AP2yvZrkbABiiJhCbuPdkYbNpx6B7Itl8RT_bUSYAMZhmux5lpsn4weQ01fzjICi1rA-bIJpOfotdOjP4_lol-LxGZOGJQv9kndP8bgmssJb3Y_2s4gPtkmXySLrhpr5So-_6dVksyuBD9aLcnsMLDbywusjEMCdhqzQbvOjryomnmEXwyz_Ewb5HFK2PfgFtoHkdjqDz-mrEs3tw5g4TdYhCftzJxgbyNAEq4aEiOQrAncYyrXlotP_w",
"p": "8TNMF0WUe7CEeNVUTsuEcBAAXRguNtpvVifIjlwzFRGOYVGIpKuHsqQPKlZL07I9gPr9LifQnyQus3oEmTOrVs6LB9sfbukbg43ZRKoGVM40JYF5Xjs7R3mEZhgU0WaYOVe3iLtBGMfXNWFwlbfQP-zEb-dPCBX1jWT3LdgNBcE",
"q": "yJJLNc9w6O4y2icME8k99FugV9E7ObwUxF3v5JN3y1cmAT0h2njyE3iAGqaDZwcY1_jGCisjwoqX6i5E8xqhxX3Gcy3J7SmUAf8fhY8wU3zv9DK7skg2IdvanDb8Y1OM6GchbYZAOVPEg2IvVio8zI-Ih3DDwDk8Df0ufzoHRb8",
"qi": "zOE-4R3cjPesm3MX-4PdwmsaF9QZLUVRUvvHJ08pKs6kAXP18hzjctAoOjhQDxlTYqNYNePfKzKwost3OJoPgRIc9w9qwUCK1gNOS4Z_xozCIaXgMddNFhkoAfZ4JaKjNCiinzjGfqG99Lf-yzmmREuuhRv7SdS3ST4VQjiJQew"
},
"accounts": [
{
"username": "foo",
"algo": "plain",
"password": "bar",
"claims": {
"arcad_role": "user",
"arcad_tenant": "dev.cli",
"preferred_username": "Foo",
"sub": "foo"
}
}
]
}
},
"unexpectedHostRedirect": {
"acceptedHostPatterns": ["arcad.local", "*.arcad.local", "arcad-*.local", "*.*.*.*"],
"hostTarget": "arcad.local"
},
"appUrlResolving": {
"ifaceMappings": {
"eth0": "http://{{ .DeviceIP }}:{{ .AppHost }}"
},
"defaultUrlTemplate": "http://{{ last ( splitList \".\" ( toString .Manifest.ID ) ) }}.arcad.local"
}
}
},
"revision": 0
}

View File

@ -1,4 +1,4 @@
package app
package spec
import (
"context"
@ -28,7 +28,7 @@ func TestValidator(t *testing.T) {
t.Parallel()
validator := spec.NewValidator()
if err := validator.Register(NameApp, schema); err != nil {
if err := validator.Register(Name, schema); err != nil {
t.Fatalf("+%v", errors.WithStack(err))
}

View File

@ -0,0 +1,181 @@
package mdns
import (
"context"
"net"
"sync"
"forge.cadoles.com/Cadoles/emissary/internal/agent"
mdns "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/mdns/spec"
"github.com/brutella/dnssd"
"github.com/mitchellh/hashstructure/v2"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
const (
DefaultDomain = "local"
)
type Controller struct {
serviceDefHash uint64
cancel context.CancelFunc
responder dnssd.Responder
mutex sync.RWMutex
}
// Name implements node.Controller.
func (c *Controller) Name() string {
return "mdns-controller"
}
// Reconcile implements node.Controller.
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
mdnsSpec := mdns.NewSpec()
if err := state.GetSpec(mdns.Name, mdnsSpec); err != nil {
if errors.Is(err, agent.ErrSpecNotFound) {
logger.Info(ctx, "could not find mdns spec")
c.stopResponder(ctx)
return nil
}
return errors.WithStack(err)
}
logger.Info(ctx, "retrieved spec", logger.F("spec", mdnsSpec.SpecName()), logger.F("revision", mdnsSpec.SpecRevision()))
if err := c.updateResponder(ctx, mdnsSpec); err != nil {
return errors.Wrap(err, "could not update responder")
}
return nil
}
func (c *Controller) stopResponder(ctx context.Context) {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.responder == nil {
return
}
c.cancel()
c.responder = nil
c.cancel = nil
}
func (c *Controller) updateResponder(ctx context.Context, spec *mdns.Spec) error {
serviceDef := struct {
Services map[string]mdns.Service
}{
Services: spec.Services,
}
newServerDefHash, err := hashstructure.Hash(serviceDef, hashstructure.FormatV2, nil)
if err != nil {
return errors.WithStack(err)
}
c.mutex.RLock()
if newServerDefHash == c.serviceDefHash && c.responder != nil {
c.mutex.RUnlock()
return nil
}
c.mutex.RUnlock()
c.stopResponder(ctx)
defaultIfaces, err := c.getDefaultIfaces()
if err != nil {
return errors.WithStack(err)
}
services := make([]dnssd.Service, 0, len(spec.Services))
for name, service := range spec.Services {
domain := service.Domain
if domain == "" {
domain = DefaultDomain
}
ifaces := service.Ifaces
if len(ifaces) == 0 {
ifaces = defaultIfaces
}
config := dnssd.Config{
Name: name,
Type: service.Type,
Domain: domain,
Host: service.Host,
Ifaces: ifaces,
Port: service.Port,
}
service, err := dnssd.NewService(config)
if err != nil {
logger.Error(ctx, "could not create mdns service", logger.E(errors.WithStack(err)))
continue
}
services = append(services, service)
}
responder, err := dnssd.NewResponder()
if err != nil {
return errors.WithStack(err)
}
for _, service := range services {
if _, err := responder.Add(service); err != nil {
logger.Error(ctx, "could not add mdns service", logger.E(errors.WithStack(err)))
continue
}
}
ctx, cancel := context.WithCancel(context.Background())
c.responder = responder
c.cancel = cancel
c.serviceDefHash = newServerDefHash
go func() {
defer c.stopResponder(ctx)
if err := responder.Respond(ctx); err != nil && !errors.Is(err, context.Canceled) {
logger.Error(ctx, "could not respond to mdns queries", logger.E(errors.WithStack(err)))
}
}()
return nil
}
func (c *Controller) getDefaultIfaces() ([]string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, errors.WithStack(err)
}
ifaceNames := make([]string, len(ifaces))
for idx, ifa := range ifaces {
ifaceNames[idx] = ifa.Name
}
return ifaceNames, nil
}
func NewController() *Controller {
return &Controller{
cancel: nil,
responder: nil,
serviceDefHash: 0,
}
}
var _ agent.Controller = &Controller{}

View File

@ -0,0 +1,17 @@
package spec
import (
_ "embed"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"github.com/pkg/errors"
)
//go:embed schema.json
var schema []byte
func init() {
if err := spec.Register(Name, schema); err != nil {
panic(errors.WithStack(err))
}
}

View File

@ -0,0 +1,47 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://mdns.edge.emissary.cadoles.com/spec.json",
"title": "MDNSSpec",
"description": "Emissary 'MDNS' specification",
"type": "object",
"properties": {
"services": {
"type": "object",
"patternProperties": {
".*": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"domain": {
"type": "string"
},
"host": {
"type": "string"
},
"ifaces": {
"type": "array",
"items": {
"type": "string"
}
},
"port": {
"type": "number"
}
},
"required": [
"type",
"host",
"port"
],
"additionalProperties": false
}
}
}
},
"required": [
"services"
],
"additionalProperties": false
}

View File

@ -0,0 +1,42 @@
package spec
import (
"forge.cadoles.com/Cadoles/emissary/internal/spec"
)
const Name spec.Name = "mdns.emissary.cadoles.com"
type Spec struct {
Revision int `json:"revision"`
Services map[string]Service `json:"services"`
}
type Service struct {
Type string `json:"type"`
Domain string `json:"domain"`
Host string `json:"host"`
Ifaces []string `json:"ifaces"`
Port int `json:"port"`
}
func (s *Spec) SpecName() spec.Name {
return Name
}
func (s *Spec) SpecRevision() int {
return s.Revision
}
func (s *Spec) SpecData() map[string]any {
return map[string]any{
"services": s.Services,
}
}
func NewSpec() *Spec {
return &Spec{
Revision: -1,
}
}
var _ spec.Spec = &Spec{}

View File

@ -0,0 +1,15 @@
{
"name": "mdns.emissary.cadoles.com",
"data": {
"services": {
"My Website": {
"type": "_http._tcp",
"domain": "local",
"host": "mywebsite",
"ifaces": ["lo", "eth0"],
"port": 80
}
}
},
"revision": 0
}

View File

@ -0,0 +1,65 @@
package spec
import (
"context"
"encoding/json"
"io/ioutil"
"testing"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"github.com/pkg/errors"
)
type validatorTestCase struct {
Name string
Source string
ShouldFail bool
}
var validatorTestCases = []validatorTestCase{
{
Name: "SpecOK",
Source: "testdata/spec-ok.json",
ShouldFail: false,
},
}
func TestValidator(t *testing.T) {
t.Parallel()
validator := spec.NewValidator()
if err := validator.Register(Name, schema); err != nil {
t.Fatalf("+%v", errors.WithStack(err))
}
for _, tc := range validatorTestCases {
func(tc validatorTestCase) {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
rawSpec, err := ioutil.ReadFile(tc.Source)
if err != nil {
t.Fatalf("+%v", errors.WithStack(err))
}
var spec spec.RawSpec
if err := json.Unmarshal(rawSpec, &spec); err != nil {
t.Fatalf("+%v", errors.WithStack(err))
}
ctx := context.Background()
err = validator.Validate(ctx, &spec)
if !tc.ShouldFail && err != nil {
t.Errorf("+%v", errors.WithStack(err))
}
if tc.ShouldFail && err == nil {
t.Error("validation should have failed")
}
})
}(tc)
}
}

View File

@ -0,0 +1,31 @@
package openwrt
import (
"crypto/sha256"
"encoding/hex"
"io"
"os"
"github.com/pkg/errors"
)
func hash(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", errors.WithStack(err)
}
hasher := sha256.New()
defer func() {
if err := file.Close(); err != nil {
panic(errors.WithStack(err))
}
}()
if _, err := io.Copy(hasher, file); err != nil {
return "", errors.WithStack(err)
}
return hex.EncodeToString(hasher.Sum(nil)), nil
}

View File

@ -0,0 +1,17 @@
package sysupgrade
import (
_ "embed"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"github.com/pkg/errors"
)
//go:embed schema.json
var schema []byte
func init() {
if err := spec.Register(Name, schema); err != nil {
panic(errors.WithStack(err))
}
}

View File

@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://sysupgrade.openwrt.emissary.cadoles.com/spec.json",
"title": "SysUpgradeSpec",
"description": "Emissary 'SysUpgrade' specification",
"type": "object",
"properties": {
"url": {
"type": "string"
},
"sha256sum": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": ["url", "sha256sum", "version"],
"additionalProperties": false
}

View File

@ -0,0 +1,38 @@
package sysupgrade
import (
"forge.cadoles.com/Cadoles/emissary/internal/spec"
)
const Name spec.Name = "sysupgrade.openwrt.emissary.cadoles.com"
type Spec struct {
Revision int `json:"revision"`
URL string `json:"url"`
SHA256Sum string `json:"sha256sum"`
Version string `json:"version"`
}
func (s *Spec) SpecName() spec.Name {
return Name
}
func (s *Spec) SpecRevision() int {
return s.Revision
}
func (s *Spec) SpecData() map[string]any {
return map[string]any{
"url": s.URL,
"version": s.Version,
"sha256sum": s.SHA256Sum,
}
}
func NewSpec() *Spec {
return &Spec{
Revision: -1,
}
}
var _ spec.Spec = &Spec{}

View File

@ -0,0 +1,9 @@
{
"name": "sysupgrade.openwrt.emissary.cadoles.com",
"data": {
"url": "http://example.com/firmware.img",
"sha256sum": "58019192dacdae17755707719707db007e26dac856102280583fbd18427dd352",
"version": "0.0.0"
},
"revision": 0
}

View File

@ -0,0 +1,65 @@
package sysupgrade
import (
"context"
"encoding/json"
"io/ioutil"
"testing"
"forge.cadoles.com/Cadoles/emissary/internal/spec"
"github.com/pkg/errors"
)
type validatorTestCase struct {
Name string
Source string
ShouldFail bool
}
var validatorTestCases = []validatorTestCase{
{
Name: "SpecOK",
Source: "testdata/spec-ok.json",
ShouldFail: false,
},
}
func TestValidator(t *testing.T) {
t.Parallel()
validator := spec.NewValidator()
if err := validator.Register(Name, schema); err != nil {
t.Fatalf("+%v", errors.WithStack(err))
}
for _, tc := range validatorTestCases {
func(tc validatorTestCase) {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
rawSpec, err := ioutil.ReadFile(tc.Source)
if err != nil {
t.Fatalf("+%v", errors.WithStack(err))
}
var spec spec.RawSpec
if err := json.Unmarshal(rawSpec, &spec); err != nil {
t.Fatalf("+%v", errors.WithStack(err))
}
ctx := context.Background()
err = validator.Validate(ctx, &spec)
if !tc.ShouldFail && err != nil {
t.Errorf("+%v", errors.WithStack(err))
}
if tc.ShouldFail && err == nil {
t.Error("validation should have failed")
}
})
}(tc)
}
}

View File

@ -0,0 +1,177 @@
package openwrt
import (
"context"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"forge.cadoles.com/Cadoles/emissary/internal/agent"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/openwrt/spec/sysupgrade"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type SysUpgradeController struct {
client *http.Client
command string
args []string
firmwareVersion FirmwareVersion
}
// Name implements agent.Controller
func (*SysUpgradeController) Name() string {
return "sysupgrade-controller"
}
// Reconcile implements agent.Controller
func (c *SysUpgradeController) Reconcile(ctx context.Context, state *agent.State) error {
sysSpec := sysupgrade.NewSpec()
if err := state.GetSpec(sysupgrade.Name, sysSpec); err != nil {
if errors.Is(err, agent.ErrSpecNotFound) {
logger.Info(ctx, "could not find sysupgrade spec, doing nothing")
return nil
}
return errors.WithStack(err)
}
firmwareVersion, err := c.firmwareVersion.FirmwareVersion(ctx)
if err != nil {
return errors.WithStack(err)
}
ctx = logger.With(ctx,
logger.F("currentFirmwareVersion", firmwareVersion),
logger.F("newFirmwareVersion", sysSpec.Version),
)
if firmwareVersion == sysSpec.Version {
logger.Info(ctx, "firmware version did not change, doing nothing")
return nil
}
downloadDir, err := os.MkdirTemp(os.TempDir(), "emissary_sysupgrade_*")
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := os.RemoveAll(downloadDir); err != nil {
logger.Error(
ctx, "could not remove download direction",
logger.E(errors.WithStack(err)),
logger.F("downloadDir", downloadDir),
)
}
}()
firmwareFile := filepath.Join(downloadDir, "firmware.bin")
logger.Info(
ctx, "downloading firmware",
logger.F("url", sysSpec.URL), logger.F("sha256sum", sysSpec.SHA256Sum),
)
if err := c.downloadFile(ctx, sysSpec.URL, sysSpec.SHA256Sum, firmwareFile); err != nil {
return errors.WithStack(err)
}
logger.Info(ctx, "upgrading firmware")
if err := c.upgradeFirmware(ctx, firmwareFile); err != nil {
return errors.WithStack(err)
}
return nil
}
func (c *SysUpgradeController) downloadFile(ctx context.Context, url string, sha256sum string, dest string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return errors.WithStack(err)
}
res, err := c.client.Do(req)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := res.Body.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
panic(errors.WithStack(err))
}
}()
tmp, err := os.CreateTemp(filepath.Dir(dest), "download_")
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := os.Remove(tmp.Name()); err != nil && !os.IsNotExist(err) {
panic(errors.WithStack(err))
}
}()
if _, err := io.Copy(tmp, res.Body); err != nil {
return errors.WithStack(err)
}
tmpFileHash, err := hash(tmp.Name())
if err != nil {
return errors.WithStack(err)
}
if tmpFileHash != sha256sum {
return errors.Errorf("sha256 sum mismatch: expected '%s', got '%s'", sha256sum, tmpFileHash)
}
if err := os.Rename(tmp.Name(), dest); err != nil {
return errors.WithStack(err)
}
return nil
}
func (c *SysUpgradeController) upgradeFirmware(ctx context.Context, firmwareFile string) error {
templatizedArgs := make([]string, len(c.args))
for i, a := range c.args {
templatizedArgs[i] = strings.Replace(a, FirmwareFileTemplate, firmwareFile, 1)
}
command := exec.CommandContext(ctx, c.command, templatizedArgs...)
command.Stdout = os.Stdout
command.Stderr = os.Stderr
logger.Debug(ctx, "executing command", logger.F("command", c.command), logger.F("args", templatizedArgs))
if err := command.Run(); err != nil {
return errors.WithStack(err)
}
return nil
}
func NewSysUpgradeController(funcs ...SysUpgradeOptionFunc) *SysUpgradeController {
opts := defaultSysUpgradeOptions()
for _, fn := range funcs {
fn(opts)
}
return &SysUpgradeController{
command: opts.Command,
args: opts.Args,
client: opts.Client,
firmwareVersion: opts.FirmwareVersion,
}
}
var _ agent.Controller = &SysUpgradeController{}

View File

@ -0,0 +1,46 @@
package openwrt
import (
"bytes"
"context"
"os"
"os/exec"
"strings"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type ShellFirmwareVersion struct {
command string
args []string
}
// FirmwareVersion implements FirmwareVersion
func (fv *ShellFirmwareVersion) FirmwareVersion(ctx context.Context) (string, error) {
command := exec.CommandContext(ctx, fv.command, fv.args...)
var buf bytes.Buffer
command.Stdout = &buf
command.Stderr = os.Stderr
logger.Debug(ctx, "executing command", logger.F("command", fv.command), logger.F("args", fv.args))
if err := command.Run(); err != nil {
return "", errors.WithStack(err)
}
version := strings.TrimSpace(buf.String())
return version, nil
}
func NewShellFirmwareVersion(command string, args ...string) *ShellFirmwareVersion {
return &ShellFirmwareVersion{
command: command,
args: args,
}
}
var _ FirmwareVersion = &ShellFirmwareVersion{}

View File

@ -0,0 +1,58 @@
package openwrt
import (
"context"
"net/http"
"time"
)
const FirmwareFileTemplate = "%FIRMWARE%"
type FirmwareVersion interface {
FirmwareVersion(context.Context) (string, error)
}
type SysUpgradeOptions struct {
Command string
Args []string
FirmwareVersion FirmwareVersion
Client *http.Client
}
func defaultSysUpgradeOptions() *SysUpgradeOptions {
return &SysUpgradeOptions{
Command: `echo`,
Args: []string{`[DUMMY UPGRADE]`, FirmwareFileTemplate},
Client: &http.Client{
Timeout: 30 * time.Second,
},
FirmwareVersion: NewShellFirmwareVersion(`echo`, "0.0.0-dummy"),
}
}
type SysUpgradeOptionFunc func(*SysUpgradeOptions)
func WithSysUpgradeCommand(command string, args ...string) SysUpgradeOptionFunc {
return func(opts *SysUpgradeOptions) {
opts.Command = command
opts.Args = args
}
}
func WithSysUpgradeFirmwareVersion(firmwareVersion FirmwareVersion) SysUpgradeOptionFunc {
return func(opts *SysUpgradeOptions) {
opts.FirmwareVersion = firmwareVersion
}
}
func WithSysUpgradeShellFirmwareVersion(command string, args ...string) SysUpgradeOptionFunc {
return func(opts *SysUpgradeOptions) {
opts.FirmwareVersion = NewShellFirmwareVersion(command, args...)
}
}
func WithSysUpgradeClient(client *http.Client) SysUpgradeOptionFunc {
return func(opts *SysUpgradeOptions) {
opts.Client = client
}
}

View File

@ -5,8 +5,8 @@ import (
"net/url"
"forge.cadoles.com/Cadoles/emissary/internal/agent"
"forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
edgeProxy "forge.cadoles.com/arcad/edge/pkg/proxy"
"forge.cadoles.com/Cadoles/emissary/internal/proxy"
spec "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
"github.com/mitchellh/hashstructure/v2"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
@ -18,7 +18,7 @@ type proxyEntry struct {
}
type Controller struct {
proxies map[proxy.ID]*proxyEntry
proxies map[spec.ID]*proxyEntry
}
// Name implements node.Controller.
@ -28,9 +28,9 @@ func (c *Controller) Name() string {
// Reconcile implements node.Controller.
func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
proxySpec := proxy.NewSpec()
proxySpec := spec.NewSpec()
if err := state.GetSpec(proxy.NameProxy, proxySpec); err != nil {
if err := state.GetSpec(spec.NameProxy, proxySpec); err != nil {
if errors.Is(err, agent.ErrSpecNotFound) {
logger.Info(ctx, "could not find proxy spec")
@ -69,7 +69,7 @@ func (c *Controller) stopAllProxies(ctx context.Context) {
}
}
func (c *Controller) updateProxies(ctx context.Context, spec *proxy.Spec) {
func (c *Controller) updateProxies(ctx context.Context, spec *spec.Spec) {
// Stop and remove obsolete proxys
for proxyID, entry := range c.proxies {
if _, exists := spec.Proxies[proxyID]; exists {
@ -100,7 +100,7 @@ func (c *Controller) updateProxies(ctx context.Context, spec *proxy.Spec) {
}
}
func (c *Controller) updateProxy(ctx context.Context, proxyID proxy.ID, proxySpec proxy.ProxyEntry) (err error) {
func (c *Controller) updateProxy(ctx context.Context, proxyID spec.ID, proxySpec spec.ProxyEntry) (err error) {
newProxySpecHash, err := hashstructure.Hash(proxySpec, hashstructure.FormatV2, nil)
if err != nil {
return errors.WithStack(err)
@ -140,7 +140,7 @@ func (c *Controller) updateProxy(ctx context.Context, proxyID proxy.ID, proxySpe
)
}
options := make([]edgeProxy.OptionFunc, 0)
options := make([]proxy.OptionFunc, 0)
allowedHosts := make([]string, len(proxySpec.Mappings))
mappings := make(map[string]*url.URL, len(proxySpec.Mappings))
@ -156,8 +156,8 @@ func (c *Controller) updateProxy(ctx context.Context, proxyID proxy.ID, proxySpe
options = append(
options,
edgeProxy.WithAllowedHosts(allowedHosts...),
edgeProxy.WithRewriteHosts(mappings),
proxy.WithAllowedHosts(allowedHosts...),
proxy.WithRewriteHosts(mappings),
)
if err := entry.Proxy.Start(ctx, proxySpec.Address, options...); err != nil {
@ -173,7 +173,7 @@ func (c *Controller) updateProxy(ctx context.Context, proxyID proxy.ID, proxySpe
func NewController() *Controller {
return &Controller{
proxies: make(map[proxy.ID]*proxyEntry),
proxies: make(map[spec.ID]*proxyEntry),
}
}

View File

@ -5,10 +5,9 @@ import (
"net/http"
"sync"
"forge.cadoles.com/Cadoles/emissary/internal/proxy"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
"forge.cadoles.com/arcad/edge/pkg/proxy"
)
type ReverseProxy struct {

View File

@ -4,6 +4,7 @@ import (
"context"
"net/http"
"strings"
"time"
"forge.cadoles.com/Cadoles/emissary/internal/auth"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
@ -13,8 +14,11 @@ import (
"gitlab.com/wpetit/goweb/logger"
)
const DefaultAcceptableSkew = 5 * time.Minute
type Authenticator struct {
repo datastore.AgentRepository
repo datastore.AgentRepository
acceptableSkew time.Duration
}
// Authenticate implements auth.Authenticator.
@ -71,11 +75,19 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
[]byte(rawToken),
jwt.WithKeySet(agent.KeySet.Set, jws.WithRequireKid(false)),
jwt.WithValidate(true),
jwt.WithAcceptableSkew(a.acceptableSkew),
)
if err != nil {
return nil, errors.WithStack(err)
}
contactedAt := time.Now()
agent, err = a.repo.Update(ctx, agent.ID, datastore.WithAgentUpdateContactedAt(contactedAt))
if err != nil {
return nil, errors.WithStack(err)
}
user := &User{
agent: agent,
}
@ -83,9 +95,10 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
return user, nil
}
func NewAuthenticator(repo datastore.AgentRepository) *Authenticator {
func NewAuthenticator(repo datastore.AgentRepository, acceptableSkew time.Duration) *Authenticator {
return &Authenticator{
repo: repo,
repo: repo,
acceptableSkew: acceptableSkew,
}
}

View File

@ -18,7 +18,7 @@ func GenerateToken(key jwk.Key, thumbprint string) (string, error) {
return "", errors.WithStack(err)
}
now := time.Now()
now := time.Now().UTC()
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
return "", errors.WithStack(err)

View File

@ -4,6 +4,7 @@ import (
"context"
"net/http"
"strings"
"time"
"forge.cadoles.com/Cadoles/emissary/internal/auth"
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
@ -11,9 +12,12 @@ import (
"gitlab.com/wpetit/goweb/logger"
)
const DefaultAcceptableSkew = 5 * time.Minute
type Authenticator struct {
keys jwk.Set
issuer string
keys jwk.Set
issuer string
acceptableSkew time.Duration
}
// Authenticate implements auth.Authenticator.
@ -30,7 +34,7 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
return nil, errors.WithStack(auth.ErrUnauthenticated)
}
token, err := parseToken(ctx, a.keys, a.issuer, rawToken)
token, err := parseToken(ctx, a.keys, a.issuer, rawToken, a.acceptableSkew)
if err != nil {
return nil, errors.WithStack(err)
}
@ -57,10 +61,11 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
return user, nil
}
func NewAuthenticator(keys jwk.Set, issuer string) *Authenticator {
func NewAuthenticator(keys jwk.Set, issuer string, acceptableSkew time.Duration) *Authenticator {
return &Authenticator{
keys: keys,
issuer: issuer,
keys: keys,
issuer: issuer,
acceptableSkew: acceptableSkew,
}
}

View File

@ -13,12 +13,13 @@ import (
const keyRole = "role"
func parseToken(ctx context.Context, keys jwk.Set, issuer string, rawToken string) (jwt.Token, error) {
func parseToken(ctx context.Context, keys jwk.Set, issuer string, rawToken string, acceptableSkew time.Duration) (jwt.Token, error) {
token, err := jwt.Parse(
[]byte(rawToken),
jwt.WithKeySet(keys, jws.WithRequireKid(false)),
jwt.WithIssuer(issuer),
jwt.WithValidate(true),
jwt.WithAcceptableSkew(acceptableSkew),
)
if err != nil {
return nil, errors.WithStack(err)
@ -42,7 +43,7 @@ func GenerateToken(ctx context.Context, key jwk.Key, issuer, subject string, rol
return "", errors.WithStack(err)
}
now := time.Now()
now := time.Now().UTC()
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
return "", errors.WithStack(err)

View File

@ -10,6 +10,7 @@ import (
type UpdateAgentOptions struct {
Status *int
Label *string
Options []OptionFunc
}
@ -21,6 +22,12 @@ func WithAgentStatus(status int) UpdateAgentOptionFunc {
}
}
func WithAgentLabel(label string) UpdateAgentOptionFunc {
return func(opts *UpdateAgentOptions) {
opts.Label = &label
}
}
func WithUpdateAgentsOptions(funcs ...OptionFunc) UpdateAgentOptionFunc {
return func(opts *UpdateAgentOptions) {
opts.Options = funcs
@ -39,6 +46,10 @@ func (c *Client) UpdateAgent(ctx context.Context, agentID datastore.AgentID, fun
payload["status"] = *opts.Status
}
if opts.Label != nil {
payload["label"] = *opts.Label
}
response := withResponse[struct {
Agent *datastore.Agent `json:"agent"`
}]()

View File

@ -10,7 +10,6 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
"forge.cadoles.com/Cadoles/emissary/internal/openwrt/uci"
"github.com/pkg/errors"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
"github.com/urfave/cli/v2"
)

View File

@ -5,6 +5,7 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/agent"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/mdns"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/openwrt"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/persistence"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/proxy"
@ -49,10 +50,6 @@ func RunCommand() *cli.Command {
controllers = append(controllers, spec.NewController())
}
if ctrlConf.Proxy.Enabled {
controllers = append(controllers, proxy.NewController())
}
if ctrlConf.UCI.Enabled {
controllers = append(controllers, openwrt.NewUCIController(
string(ctrlConf.UCI.BinPath),
@ -66,6 +63,37 @@ func RunCommand() *cli.Command {
))
}
if ctrlConf.Proxy.Enabled {
controllers = append(controllers, proxy.NewController())
}
if ctrlConf.MDNS.Enabled {
controllers = append(controllers, mdns.NewController())
}
if ctrlConf.SysUpgrade.Enabled {
sysUpgradeArgs := make([]string, 0)
if len(ctrlConf.SysUpgrade.SysUpgradeCommand) > 1 {
sysUpgradeArgs = ctrlConf.SysUpgrade.SysUpgradeCommand[1:]
}
firmwareVersionArgs := make([]string, 0)
if len(ctrlConf.SysUpgrade.FirmwareVersionCommand) > 1 {
firmwareVersionArgs = ctrlConf.SysUpgrade.FirmwareVersionCommand[1:]
}
controllers = append(controllers, openwrt.NewSysUpgradeController(
openwrt.WithSysUpgradeCommand(
ctrlConf.SysUpgrade.SysUpgradeCommand[0],
sysUpgradeArgs...,
),
openwrt.WithSysUpgradeShellFirmwareVersion(
ctrlConf.SysUpgrade.FirmwareVersionCommand[0],
firmwareVersionArgs...,
),
))
}
key, err := jwk.LoadOrGenerate(string(conf.Agent.PrivateKeyPath), jwk.DefaultKeySize)
if err != nil {
return errors.WithStack(err)

View File

@ -6,6 +6,7 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/client"
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"forge.cadoles.com/Cadoles/emissary/internal/format"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
@ -15,7 +16,21 @@ func QueryCommand() *cli.Command {
return &cli.Command{
Name: "query",
Usage: "Query agents",
Flags: clientFlag.ComposeFlags(),
Flags: clientFlag.ComposeFlags(
&cli.StringSliceFlag{
Name: "thumbprints",
Usage: "use `THUMBPRINTS` as query filter",
Value: nil,
},
&cli.Int64SliceFlag{
Name: "statuses",
Usage: "use `STATUSES` as query filter",
},
&cli.Int64SliceFlag{
Name: "ids",
Usage: "use `IDS` as query filter",
},
),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
@ -24,9 +39,40 @@ func QueryCommand() *cli.Command {
return errors.WithStack(apierr.Wrap(err))
}
options := make([]client.QueryAgentsOptionFunc, 0)
thumbprints := ctx.StringSlice("thumbprints")
if thumbprints != nil {
options = append(options, client.WithQueryAgentsThumbprints(thumbprints...))
}
rawIDs := ctx.Int64Slice("ids")
if rawIDs != nil {
agentIDs := func(ids []int64) []datastore.AgentID {
agentIDs := make([]datastore.AgentID, len(ids))
for i, id := range ids {
agentIDs[i] = datastore.AgentID(id)
}
return agentIDs
}(rawIDs)
options = append(options, client.WithQueryAgentsID(agentIDs...))
}
rawStatuses := ctx.Int64Slice("statuses")
if rawStatuses != nil {
statuses := func(rawStatuses []int64) []datastore.AgentStatus {
statuses := make([]datastore.AgentStatus, len(rawStatuses))
for i, status := range rawStatuses {
statuses[i] = datastore.AgentStatus(status)
}
return statuses
}(rawStatuses)
options = append(options, client.WithQueryAgentsStatus(statuses...))
}
client := client.New(baseFlags.ServerURL, client.WithToken(token))
agents, _, err := client.QueryAgents(ctx.Context)
agents, _, err := client.QueryAgents(ctx.Context, options...)
if err != nil {
return errors.WithStack(apierr.Wrap(err))
}

View File

@ -13,11 +13,6 @@ import (
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
// Import specs
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/app"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"
)
func UpdateCommand() *cli.Command {

View File

@ -22,6 +22,11 @@ func UpdateCommand() *cli.Command {
Usage: "Set `STATUS` to selected agent",
Value: -1,
},
&cli.StringFlag{
Name: "label",
Usage: "Set `LABEL` to selected agent",
Value: "",
},
),
Action: func(ctx *cli.Context) error {
baseFlags := clientFlag.GetBaseFlags(ctx)
@ -43,6 +48,11 @@ func UpdateCommand() *cli.Command {
options = append(options, client.WithAgentStatus(status))
}
label := ctx.String("label")
if label != "" {
options = append(options, client.WithAgentLabel(label))
}
client := client.New(baseFlags.ServerURL, client.WithToken(token))
agent, err := client.UpdateAgent(ctx.Context, agentID, options...)

View File

@ -7,9 +7,10 @@ func agentHints(outputMode format.OutputMode) format.Hints {
OutputMode: outputMode,
Props: []format.Prop{
format.NewProp("ID", "ID"),
format.NewProp("Label", "Label"),
format.NewProp("Thumbprint", "Thumbprint"),
format.NewProp("Status", "Status"),
format.NewProp("CreatedAt", "CreatedAt"),
format.NewProp("ContactedAt", "ContactedAt"),
format.NewProp("UpdatedAt", "UpdatedAt"),
},
}

View File

@ -3,10 +3,6 @@ package api
import (
"forge.cadoles.com/Cadoles/emissary/internal/command/api/agent"
"github.com/urfave/cli/v2"
// Output format
_ "forge.cadoles.com/Cadoles/emissary/internal/format/json"
_ "forge.cadoles.com/Cadoles/emissary/internal/format/table"
)
func Root() *cli.Command {

View File

@ -6,7 +6,6 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
"forge.cadoles.com/Cadoles/emissary/internal/config"
"github.com/pkg/errors"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/logger"
)

View File

@ -9,10 +9,6 @@ import (
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
// Spec validation
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"
)
func Main(buildDate, projectVersion, gitRef, defaultConfigPath string, commands ...*cli.Command) {

View File

@ -7,7 +7,6 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
"forge.cadoles.com/Cadoles/emissary/internal/server"
"github.com/pkg/errors"
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
"github.com/urfave/cli/v2"
"gitlab.com/wpetit/goweb/logger"
)

View File

@ -1,5 +1,7 @@
package config
import "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/openwrt"
type AgentConfig struct {
ServerURL InterpolatedString `yaml:"serverUrl"`
PrivateKeyPath InterpolatedString `yaml:"privateKeyPath"`
@ -20,6 +22,8 @@ type ControllersConfig struct {
Proxy ProxyControllerConfig `yaml:"proxy"`
UCI UCIControllerConfig `yaml:"uci"`
App AppControllerConfig `yaml:"app"`
SysUpgrade SysUpgradeControllerConfig `yaml:"sysupgrade"`
MDNS MDNSControllerConfig `yaml:"mdns"`
}
type PersistenceControllerConfig struct {
@ -46,6 +50,16 @@ type AppControllerConfig struct {
DownloadDir InterpolatedString `yaml:"downloadDir"`
}
type SysUpgradeControllerConfig struct {
Enabled InterpolatedBool `yaml:"enabled"`
SysUpgradeCommand InterpolatedStringSlice `yaml:"sysupgradeCommand"`
FirmwareVersionCommand InterpolatedStringSlice `yaml:"firmwareVersionCommand"`
}
type MDNSControllerConfig struct {
Enabled InterpolatedBool `yaml:"enabled"`
}
func NewDefaultAgentConfig() AgentConfig {
return AgentConfig{
ServerURL: "http://127.0.0.1:3000",
@ -72,6 +86,14 @@ func NewDefaultAgentConfig() AgentConfig {
DataDir: "apps/data",
DownloadDir: "apps/bundles",
},
SysUpgrade: SysUpgradeControllerConfig{
Enabled: true,
SysUpgradeCommand: InterpolatedStringSlice{"sysupgrade", "--force", "-u", "-v", openwrt.FirmwareFileTemplate},
FirmwareVersionCommand: InterpolatedStringSlice{"sh", "-c", `source /etc/openwrt_release && echo "$DISTRIB_ID-$DISTRIB_RELEASE-$DISTRIB_REVISION"`},
},
MDNS: MDNSControllerConfig{
Enabled: true,
},
},
Collectors: []ShellCollectorConfig{
{

View File

@ -15,6 +15,6 @@ type DatabaseConfig struct {
func NewDefaultDatabaseConfig() DatabaseConfig {
return DatabaseConfig{
Driver: "sqlite",
DSN: "sqlite://emissary.sqlite",
DSN: "sqlite://emissary.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000",
}
}

View File

@ -20,13 +20,15 @@ const (
)
type Agent struct {
ID AgentID `json:"id"`
Thumbprint string `json:"thumbprint"`
KeySet *SerializableKeySet `json:"keyset,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Status AgentStatus `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID AgentID `json:"id"`
Label string `json:"label"`
Thumbprint string `json:"thumbprint"`
KeySet *SerializableKeySet `json:"keyset,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Status AgentStatus `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ContactedAt *time.Time `json:"contactedAt,omitempty"`
}
type SerializableKeySet struct {

View File

@ -2,6 +2,7 @@ package datastore
import (
"context"
"time"
"github.com/lestrrat-go/jwx/v2/jwk"
)
@ -68,10 +69,12 @@ func WithAgentQueryThumbprints(thumbprints ...string) AgentQueryOptionFunc {
type AgentUpdateOptionFunc func(*AgentUpdateOptions)
type AgentUpdateOptions struct {
Status *AgentStatus
Metadata *map[string]any
KeySet *jwk.Set
Thumbprint *string
Label *string
Status *AgentStatus
ContactedAt *time.Time
Metadata *map[string]any
KeySet *jwk.Set
Thumbprint *string
}
func WithAgentUpdateStatus(status AgentStatus) AgentUpdateOptionFunc {
@ -97,3 +100,15 @@ func WithAgentUpdateThumbprint(thumbprint string) AgentUpdateOptionFunc {
opts.Thumbprint = &thumbprint
}
}
func WithAgentUpdateLabel(label string) AgentUpdateOptionFunc {
return func(opts *AgentUpdateOptions) {
opts.Label = &label
}
}
func WithAgentUpdateContactedAt(contactedAt time.Time) AgentUpdateOptionFunc {
return func(opts *AgentUpdateOptions) {
opts.ContactedAt = &contactedAt
}
}

View File

@ -20,9 +20,24 @@ type AgentRepository struct {
// DeleteSpec implements datastore.AgentRepository.
func (r *AgentRepository) DeleteSpec(ctx context.Context, agentID datastore.AgentID, name string) error {
query := `DELETE FROM specs WHERE agent_id = $1 AND name = $2`
err := r.withTx(ctx, func(tx *sql.Tx) error {
exists, err := r.agentExists(ctx, tx, agentID)
if err != nil {
return errors.WithStack(err)
}
_, err := r.db.ExecContext(ctx, query, agentID, name)
if !exists {
return errors.WithStack(datastore.ErrNotFound)
}
query := `DELETE FROM specs WHERE agent_id = $1 AND name = $2`
if _, err = tx.ExecContext(ctx, query, agentID, name); err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return errors.WithStack(err)
}
@ -34,31 +49,55 @@ func (r *AgentRepository) DeleteSpec(ctx context.Context, agentID datastore.Agen
func (r *AgentRepository) GetSpecs(ctx context.Context, agentID datastore.AgentID) ([]*datastore.Spec, error) {
specs := make([]*datastore.Spec, 0)
query := `
err := r.withTx(ctx, func(tx *sql.Tx) error {
exists, err := r.agentExists(ctx, tx, agentID)
if err != nil {
return errors.WithStack(err)
}
if !exists {
return errors.WithStack(datastore.ErrNotFound)
}
query := `
SELECT id, name, revision, data, created_at, updated_at
FROM specs
WHERE agent_id = $1
`
`
rows, err := r.db.QueryContext(ctx, query, agentID)
if err != nil {
return nil, errors.WithStack(err)
}
defer rows.Close()
for rows.Next() {
spec := &datastore.Spec{}
data := JSONMap{}
if err := rows.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt); err != nil {
return nil, errors.WithStack(err)
rows, err := tx.QueryContext(ctx, query, agentID)
if err != nil {
return errors.WithStack(err)
}
spec.Data = data
defer func() {
if err := rows.Close(); err != nil {
logger.Error(ctx, "could not close rows", logger.E(errors.WithStack(err)))
}
}()
specs = append(specs, spec)
for rows.Next() {
spec := &datastore.Spec{}
data := JSONMap{}
if err := rows.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt); err != nil {
return errors.WithStack(err)
}
spec.Data = data
specs = append(specs, spec)
}
if err := rows.Err(); err != nil {
return errors.WithStack(err)
}
return nil
})
if err != nil {
return nil, errors.WithStack(err)
}
return specs, nil
@ -69,6 +108,15 @@ func (r *AgentRepository) UpdateSpec(ctx context.Context, agentID datastore.Agen
spec := &datastore.Spec{}
err := r.withTx(ctx, func(tx *sql.Tx) error {
exists, err := r.agentExists(ctx, tx, agentID)
if err != nil {
return errors.WithStack(err)
}
if !exists {
return errors.WithStack(datastore.ErrNotFound)
}
now := time.Now().UTC()
query := `
@ -88,7 +136,7 @@ func (r *AgentRepository) UpdateSpec(ctx context.Context, agentID datastore.Agen
data := JSONMap{}
err := row.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt)
err = row.Scan(&spec.ID, &spec.Name, &spec.Revision, &data, &spec.CreatedAt, &spec.UpdatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return errors.WithStack(datastore.ErrUnexpectedRevision)
@ -119,7 +167,7 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
count := 0
err := r.withTx(ctx, func(tx *sql.Tx) error {
query := `SELECT id, thumbprint, status, created_at, updated_at FROM agents`
query := `SELECT id, label, thumbprint, status, contacted_at, created_at, updated_at FROM agents`
limit := 10
if options.Limit != nil {
@ -176,22 +224,34 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
return errors.WithStack(err)
}
defer rows.Close()
defer func() {
if err := rows.Close(); err != nil {
logger.Error(ctx, "could not close rows", logger.E(errors.WithStack(err)))
}
}()
for rows.Next() {
agent := &datastore.Agent{}
metadata := JSONMap{}
contactedAt := sql.NullTime{}
if err := rows.Scan(&agent.ID, &agent.Thumbprint, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
if err := rows.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
return errors.WithStack(err)
}
agent.Metadata = metadata
if contactedAt.Valid {
agent.ContactedAt = &contactedAt.Time
}
agents = append(agents, agent)
}
if err := rows.Err(); err != nil {
return errors.WithStack(err)
}
row := tx.QueryRowContext(ctx, `SELECT count(id) FROM agents `+filters, args...)
if err := row.Scan(&count); err != nil {
return errors.WithStack(err)
@ -299,7 +359,7 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
err := r.withTx(ctx, func(tx *sql.Tx) error {
query := `
SELECT "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
SELECT "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at"
FROM agents
WHERE id = $1
`
@ -307,9 +367,10 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
row := r.db.QueryRowContext(ctx, query, id)
metadata := JSONMap{}
contactedAt := sql.NullTime{}
var rawKeySet []byte
if err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return datastore.ErrNotFound
}
@ -318,6 +379,9 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
}
agent.Metadata = metadata
if contactedAt.Valid {
agent.ContactedAt = &contactedAt.Time
}
keySet := jwk.NewSet()
if err := json.Unmarshal(rawKeySet, &keySet); err != nil {
@ -346,15 +410,11 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
err := r.withTx(ctx, func(tx *sql.Tx) error {
query := `
UPDATE agents SET updated_at = $2
UPDATE agents SET id = $1
`
now := time.Now().UTC()
args := []any{
id, now,
}
index := 3
args := []any{id}
index := 2
if options.Status != nil {
query += fmt.Sprintf(`, status = $%d`, index)
@ -379,23 +439,51 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
index++
}
if options.Label != nil {
query += fmt.Sprintf(`, label = $%d`, index)
args = append(args, *options.Label)
index++
}
if options.ContactedAt != nil {
query += fmt.Sprintf(`, contacted_at = $%d`, index)
utc := options.ContactedAt.UTC()
args = append(args, utc)
index++
}
if options.Metadata != nil {
query += fmt.Sprintf(`, metadata = $%d`, index)
args = append(args, JSONMap(*options.Metadata))
index++
}
updated := options.Metadata != nil ||
options.Status != nil ||
options.Label != nil ||
options.KeySet != nil ||
options.Thumbprint != nil
if updated {
now := time.Now().UTC()
query += fmt.Sprintf(`, updated_at = $%d`, index)
args = append(args, now)
index++
}
query += `
WHERE id = $1
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
RETURNING "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at"
`
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
row := tx.QueryRowContext(ctx, query, args...)
metadata := JSONMap{}
contactedAt := sql.NullTime{}
var rawKeySet []byte
if err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return datastore.ErrNotFound
}
@ -404,6 +492,9 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
}
agent.Metadata = metadata
if contactedAt.Valid {
agent.ContactedAt = &contactedAt.Time
}
keySet := jwk.NewSet()
if err := json.Unmarshal(rawKeySet, &keySet); err != nil {
@ -421,8 +512,28 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
return agent, nil
}
func (r *AgentRepository) agentExists(ctx context.Context, tx *sql.Tx, agentID datastore.AgentID) (bool, error) {
row := tx.QueryRowContext(ctx, `SELECT count(id) FROM agents WHERE id = $1`, agentID)
var count int
if err := row.Scan(&count); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, errors.WithStack(datastore.ErrNotFound)
}
return false, errors.WithStack(err)
}
if count == 0 {
return false, errors.WithStack(datastore.ErrNotFound)
}
return true, nil
}
func (r *AgentRepository) withTx(ctx context.Context, fn func(*sql.Tx) error) error {
tx, err := r.db.Begin()
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return errors.WithStack(err)
}

View File

@ -0,0 +1,46 @@
package sqlite
import (
"database/sql"
"fmt"
"os"
"testing"
"time"
"forge.cadoles.com/Cadoles/emissary/internal/datastore/testsuite"
"forge.cadoles.com/Cadoles/emissary/internal/migrate"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
_ "modernc.org/sqlite"
)
func TestSQLiteAgentRepository(t *testing.T) {
logger.SetLevel(logger.LevelDebug)
file := "testdata/agent_repository_test.sqlite"
if err := os.Remove(file); err != nil && !errors.Is(err, os.ErrNotExist) {
t.Fatalf("%+v", errors.WithStack(err))
}
dsn := fmt.Sprintf("%s?_pragma=foreign_keys(1)&_pragma=busy_timeout=%d", file, (60 * time.Second).Milliseconds())
migr, err := migrate.New("../../../migrations", "sqlite", "sqlite://"+dsn)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if err := migr.Up(); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
db, err := sql.Open("sqlite", dsn)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
repo := NewAgentRepository(db)
testsuite.TestAgentRepository(t, repo)
}

View File

@ -0,0 +1 @@
*.sqlite*

View File

@ -0,0 +1,14 @@
package testsuite
import (
"testing"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
)
func TestAgentRepository(t *testing.T, repo datastore.AgentRepository) {
t.Run("Cases", func(t *testing.T) {
t.Parallel()
runAgentRepositoryTests(t, repo)
})
}

View File

@ -0,0 +1,129 @@
package testsuite
import (
"context"
"testing"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/mdns/spec"
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
"github.com/pkg/errors"
)
type agentRepositoryTestCase struct {
Name string
Skip bool
Run func(ctx context.Context, repo datastore.AgentRepository) error
}
var agentRepositoryTestCases = []agentRepositoryTestCase{
{
Name: "Create a new agent",
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
thumbprint := "foo"
keySet := jwk.NewSet()
var metadata map[string]any
agent, err := repo.Create(ctx, thumbprint, keySet, metadata)
if err != nil {
return errors.WithStack(err)
}
if agent.CreatedAt.IsZero() {
return errors.Errorf("agent.CreatedAt should not be zero time")
}
if agent.UpdatedAt.IsZero() {
return errors.Errorf("agent.UpdatedAt should not be zero time")
}
if e, g := datastore.AgentStatusPending, agent.Status; e != g {
return errors.Errorf("agent.Status: expected '%v', got '%v'", e, g)
}
return nil
},
},
{
Name: "Try to update spec for an unexistant agent",
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
var unexistantAgentID datastore.AgentID = 9999
var specData map[string]any
agent, err := repo.UpdateSpec(ctx, unexistantAgentID, string(spec.Name), 0, specData)
if err == nil {
return errors.New("error should not be nil")
}
if !errors.Is(err, datastore.ErrNotFound) {
return errors.Errorf("error should be datastore.ErrNotFound, got '%+v'", err)
}
if agent != nil {
return errors.New("agent should be nil")
}
return nil
},
},
{
Name: "Try to delete spec of an unexistant agent",
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
var unexistantAgentID datastore.AgentID = 9999
err := repo.DeleteSpec(ctx, unexistantAgentID, string(spec.Name))
if err == nil {
return errors.New("error should not be nil")
}
if !errors.Is(err, datastore.ErrNotFound) {
return errors.Errorf("error should be datastore.ErrNotFound, got '%+v'", err)
}
return nil
},
},
{
Name: "Try to get specs of an unexistant agent",
Run: func(ctx context.Context, repo datastore.AgentRepository) error {
var unexistantAgentID datastore.AgentID = 9999
specs, err := repo.GetSpecs(ctx, unexistantAgentID)
if err == nil {
return errors.New("error should not be nil")
}
if !errors.Is(err, datastore.ErrNotFound) {
return errors.Errorf("error should be datastore.ErrNotFound, got '%+v'", err)
}
if specs != nil {
return errors.Errorf("specs should be nil, got '%+v'", err)
}
return nil
},
},
}
func runAgentRepositoryTests(t *testing.T, repo datastore.AgentRepository) {
for _, tc := range agentRepositoryTestCases {
func(tc agentRepositoryTestCase) {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
if tc.Skip {
t.SkipNow()
return
}
ctx := context.Background()
if err := tc.Run(ctx, repo); err != nil {
t.Errorf("%+v", errors.WithStack(err))
}
})
}(tc)
}
}

View File

@ -0,0 +1,6 @@
package format
import (
_ "forge.cadoles.com/Cadoles/emissary/internal/format/json"
_ "forge.cadoles.com/Cadoles/emissary/internal/format/table"
)

View File

@ -0,0 +1,6 @@
package passwd
import (
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/argon2id"
_ "forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd/plain"
)

View File

@ -0,0 +1,9 @@
package spec
import (
_ "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/app/spec"
_ "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/mdns/spec"
_ "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/openwrt/spec/sysupgrade"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"
)

View File

@ -0,0 +1,6 @@
package sql
import (
_ "github.com/jackc/pgx/v5/stdlib"
_ "modernc.org/sqlite"
)

View File

@ -23,6 +23,13 @@ type (
ParseOption = jwk.ParseOption
)
var (
FromRaw = jwk.FromRaw
NewSet = jwk.NewSet
)
const AlgorithmKey = jwk.AlgorithmKey
func Parse(src []byte, options ...jwk.ParseOption) (Set, error) {
return jwk.Parse(src, options...)
}

View File

@ -2,7 +2,6 @@ package migrate
import (
"fmt"
"log"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
@ -23,8 +22,6 @@ func New(migrationDir, driver, dsn string) (*migrate.Migrate, error) {
fmt.Sprintf("file://%s/%s", migrationDir, driver),
dsn,
)
log.Println(migrationDir, driver, dsn)
if err != nil {
return nil, errors.WithStack(err)
}

View File

@ -0,0 +1,29 @@
package proxy
import (
"net/http"
"forge.cadoles.com/Cadoles/emissary/internal/proxy/wildcard"
)
func FilterHosts(allowedHostPatterns ...string) Middleware {
return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if matches := wildcard.MatchAny(r.Host, allowedHostPatterns...); !matches {
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
func WithAllowedHosts(allowedHostPatterns ...string) OptionFunc {
return func(o *Options) {
o.Middlewares = append(o.Middlewares, FilterHosts(allowedHostPatterns...))
}
}

View File

@ -0,0 +1,66 @@
package proxy
import (
"net/http"
"net/url"
"sort"
"forge.cadoles.com/Cadoles/emissary/internal/proxy/wildcard"
"gitlab.com/wpetit/goweb/logger"
)
func RewriteHosts(mappings map[string]*url.URL) Middleware {
patterns := make([]string, len(mappings))
for p := range mappings {
patterns = append(patterns, p)
}
sort.Strings(patterns)
reverse(patterns)
return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var match *url.URL
for _, p := range patterns {
logger.Debug(ctx, "matching host to pattern", logger.F("host", r.Host), logger.F("pattern", p))
if matches := wildcard.Match(r.Host, p); !matches {
continue
}
match = mappings[p]
break
}
if match == nil {
h.ServeHTTP(w, r)
return
}
ctx = logger.With(ctx, logger.F("originalHost", r.Host))
r = r.WithContext(ctx)
originalURL := r.URL.String()
r.URL.Host = match.Host
r.URL.Scheme = match.Scheme
logger.Debug(ctx, "rewriting url", logger.F("from", originalURL), logger.F("to", r.URL.String()))
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
func WithRewriteHosts(mappings map[string]*url.URL) OptionFunc {
return func(o *Options) {
o.Middlewares = append(o.Middlewares, RewriteHosts(mappings))
}
}

View File

@ -0,0 +1,33 @@
package proxy
import "net/http"
type Middleware func(h http.Handler) http.Handler
type ProxyResponseTransformer interface {
TransformResponse(*http.Response) error
}
type defaultProxyResponseTransformer struct{}
// TransformResponse implements ProxyResponseTransformer
func (*defaultProxyResponseTransformer) TransformResponse(*http.Response) error {
return nil
}
var _ ProxyResponseTransformer = &defaultProxyResponseTransformer{}
type ProxyResponseMiddleware func(ProxyResponseTransformer) ProxyResponseTransformer
type ProxyRequestTransformer interface {
TransformRequest(*http.Request)
}
type ProxyRequestMiddleware func(ProxyRequestTransformer) ProxyRequestTransformer
type defaultProxyRequestTransformer struct{}
// TransformRequest implements ProxyRequestTransformer
func (*defaultProxyRequestTransformer) TransformRequest(*http.Request) {}
var _ ProxyRequestTransformer = &defaultProxyRequestTransformer{}

29
internal/proxy/options.go Normal file
View File

@ -0,0 +1,29 @@
package proxy
type Options struct {
Middlewares []Middleware
ProxyRequestMiddlewares []ProxyRequestMiddleware
ProxyResponseMiddlewares []ProxyResponseMiddleware
}
func defaultOptions() *Options {
return &Options{
Middlewares: make([]Middleware, 0),
ProxyRequestMiddlewares: make([]ProxyRequestMiddleware, 0),
ProxyResponseMiddlewares: make([]ProxyResponseMiddleware, 0),
}
}
type OptionFunc func(*Options)
func WithProxyRequestMiddlewares(middlewares ...ProxyRequestMiddleware) OptionFunc {
return func(o *Options) {
o.ProxyRequestMiddlewares = middlewares
}
}
func WithproxyResponseMiddlewares(middlewares ...ProxyResponseMiddleware) OptionFunc {
return func(o *Options) {
o.ProxyResponseMiddlewares = middlewares
}
}

131
internal/proxy/proxy.go Normal file
View File

@ -0,0 +1,131 @@
package proxy
import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"sync"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type Proxy struct {
reversers sync.Map
handler http.Handler
proxyResponseTransformer ProxyResponseTransformer
proxyRequestTransformer ProxyRequestTransformer
}
// ServeHTTP implements http.Handler
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
p.handler.ServeHTTP(w, r)
}
func (p *Proxy) proxyRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reverser *httputil.ReverseProxy
key := fmt.Sprintf("%s://%s", r.URL.Scheme, r.URL.Host)
createAndStore := func() {
target := &url.URL{
Scheme: r.URL.Scheme,
Host: r.URL.Host,
}
reverser = httputil.NewSingleHostReverseProxy(target)
originalDirector := reverser.Director
if p.proxyRequestTransformer != nil {
reverser.Director = func(r *http.Request) {
originalURL := r.URL.String()
originalDirector(r)
p.proxyRequestTransformer.TransformRequest(r)
logger.Debug(ctx, "proxying request", logger.F("targetURL", r.URL.String()), logger.F("originalURL", originalURL))
}
}
if p.proxyResponseTransformer != nil {
reverser.ModifyResponse = func(r *http.Response) error {
if err := p.proxyResponseTransformer.TransformResponse(r); err != nil {
return errors.WithStack(err)
}
return nil
}
}
p.reversers.Store(key, reverser)
}
raw, exists := p.reversers.Load(key)
if !exists {
createAndStore()
}
reverser, ok := raw.(*httputil.ReverseProxy)
if !ok {
createAndStore()
}
reverser.ServeHTTP(w, r)
}
func New(funcs ...OptionFunc) *Proxy {
opts := defaultOptions()
for _, fn := range funcs {
fn(opts)
}
proxy := &Proxy{}
handler := http.HandlerFunc(proxy.proxyRequest)
proxy.handler = createMiddlewareChain(handler, opts.Middlewares)
proxy.proxyRequestTransformer = createProxyRequestChain(&defaultProxyRequestTransformer{}, opts.ProxyRequestMiddlewares)
proxy.proxyResponseTransformer = createProxyResponseChain(&defaultProxyResponseTransformer{}, opts.ProxyResponseMiddlewares)
return proxy
}
var _ http.Handler = &Proxy{}
func createMiddlewareChain(handler http.Handler, middlewares []Middleware) http.Handler {
reverse(middlewares)
for _, m := range middlewares {
handler = m(handler)
}
return handler
}
func createProxyResponseChain(transformer ProxyResponseTransformer, middlewares []ProxyResponseMiddleware) ProxyResponseTransformer {
reverse(middlewares)
for _, m := range middlewares {
transformer = m(transformer)
}
return transformer
}
func createProxyRequestChain(transformer ProxyRequestTransformer, middlewares []ProxyRequestMiddleware) ProxyRequestTransformer {
reverse(middlewares)
for _, m := range middlewares {
transformer = m(transformer)
}
return transformer
}
func reverse[S ~[]E, E any](s S) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}

View File

@ -0,0 +1,44 @@
package wildcard
const wildcard = '*'
func Match(str, pattern string) bool {
if pattern == "" {
return str == pattern
}
if pattern == string(wildcard) {
return true
}
return deepMatchRune([]rune(str), []rune(pattern))
}
func MatchAny(str string, patterns ...string) bool {
for _, p := range patterns {
if matches := Match(str, p); matches {
return matches
}
}
return false
}
func deepMatchRune(str, pattern []rune) bool {
for len(pattern) > 0 {
switch pattern[0] {
default:
if len(str) == 0 || str[0] != pattern[0] {
return false
}
case wildcard:
return deepMatchRune(str, pattern[1:]) ||
(len(str) > 0 && deepMatchRune(str[1:], pattern))
}
str = str[1:]
pattern = pattern[1:]
}
return len(str) == 0 && len(pattern) == 0
}

View File

@ -145,6 +145,7 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
type updateAgentRequest struct {
Status *datastore.AgentStatus `json:"status" validate:"omitempty,oneof=0 1 2 3"`
Label *string `json:"label" validate:"omitempty"`
}
func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
@ -166,6 +167,10 @@ func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
options = append(options, datastore.WithAgentUpdateStatus(*updateAgentReq.Status))
}
if updateAgentReq.Label != nil {
options = append(options, datastore.WithAgentUpdateLabel(*updateAgentReq.Label))
}
agent, err := s.agentRepo.Update(
ctx,
datastore.AgentID(agentID),

View File

@ -105,8 +105,8 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
r.Group(func(r chi.Router) {
r.Use(auth.Middleware(
thirdparty.NewAuthenticator(keys, string(s.conf.Issuer)),
agent.NewAuthenticator(s.agentRepo),
thirdparty.NewAuthenticator(keys, string(s.conf.Issuer), thirdparty.DefaultAcceptableSkew),
agent.NewAuthenticator(s.agentRepo, agent.DefaultAcceptableSkew),
))
r.Route("/agents", func(r chi.Router) {

View File

@ -10,11 +10,6 @@ import (
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger"
// Import specs
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/app"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/proxy"
_ "forge.cadoles.com/Cadoles/emissary/internal/spec/uci"
)
const (

View File

@ -1,87 +0,0 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://app.edge.emissary.cadoles.com/spec.json",
"title": "AppSpec",
"description": "Emissary 'App' specification",
"type": "object",
"properties": {
"apps": {
"type": "object",
"patternProperties": {
".*": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"sha256sum": {
"type": "string"
},
"address": {
"type": "string"
},
"format": {
"type": "string",
"enum": [
"zip",
"tar.gz"
]
}
},
"required": [
"url",
"sha256sum",
"address",
"format"
],
"additionalProperties": false
}
}
},
"auth": {
"type": "object",
"properties": {
"local": {
"type": "object",
"properties": {
"key": {
"type": ["object", "string"]
},
"accounts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"algo": {
"type": "string"
},
"claims": {
"type": "object"
}
},
"required": [
"username",
"password",
"algo"
]
}
}
},
"required": [
"key"
]
}
}
}
},
"required": [
"apps"
],
"additionalProperties": false
}

View File

@ -1,53 +0,0 @@
package app
import (
"forge.cadoles.com/Cadoles/emissary/internal/spec"
edgeAuth "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
)
const NameApp spec.Name = "app.emissary.cadoles.com"
type Spec struct {
Revision int `json:"revision"`
Apps map[string]AppEntry `json:"apps"`
Auth *Auth `json:"auth"`
}
type AppEntry struct {
URL string `json:"url"`
SHA256Sum string `json:"sha256sum"`
Address string `json:"address"`
Format string `json:"format"`
}
type Auth struct {
Local *LocalAuth `json:"local,omitempty"`
}
type LocalAuth struct {
Key any `json:"key"`
Accounts []edgeAuth.LocalAccount `json:"accounts"`
}
func (s *Spec) SpecName() spec.Name {
return NameApp
}
func (s *Spec) SpecRevision() int {
return s.Revision
}
func (s *Spec) SpecData() map[string]any {
return map[string]any{
"apps": s.Apps,
"auth": s.Auth,
}
}
func NewSpec() *Spec {
return &Spec{
Revision: -1,
}
}
var _ spec.Spec = &Spec{}

View File

@ -1,42 +0,0 @@
{
"name": "app.emissary.cadoles.com",
"data": {
"apps": {
"edge.sdk.client.test": {
"url": "http://example.com/edge.sdk.client.test_0.0.0.zip",
"sha256sum": "58019192dacdae17755707719707db007e26dac856102280583fbd18427dd352",
"address": ":8081",
"format": "zip"
}
},
"auth": {
"local": {
"key": {
"d": "YOre0WZefGfUGFvDg42oL5Oad5Zsb1N_hqPyLVM5ajpTZzcHpB3wT6In9tFO_VshB6lxVtPA9ckPkpMTFY7ygt1Yomc1HkoOKRtmIaqdr4VgNQifU-4yiLiJkSbdYSeMV-KkkN8mGR1keJpJeS34W1X0W6CkU2nw7F5VueBCJfWJA0funRfuWdI68MTUgT9kRZFp-SfvptvRL6jVYHV_5hqxzHCvgEdBSF6QKwx4M6P6QBMt7ft6uMLmFx9abKFw2V51hX3PkxiSepVB3w5CYg4HtS3AHX6bILL4m0R2pdTIkap7i3tkH_xAOuKWt8D6JhadI8X1rEAwXmCS5KrRgQ",
"dp": "U0HfvBC6hk-SCpuotGIv3vbHCVt1aF3SHK0y32EYCOe8e_9G6YCEILfcvEJ5fiOCc2kvx6TasHQu4qj1uWRKenZlK1sJ6KDybGCkZL1D3jYnbeLZYBuWBL__YbZiST3ewbxzj_EDMWiZ8sUltahza_1weSgg8auSzTHS2LJBHIE",
"dq": "hVom4ScDxgqhCsQNVpZlN7M3v0tgWjl_gTOHjOyzKCHQJeC0QmJJaMKkQZPWJ8jjLqy7VwVpqC2nZU7QDuX1Cq5eJDQcXi9XtaAfIBico9WcYDre6mDyhL588YHpekyRke8HnZ810iesr0G3gU1h0QvZVVuW-pXTJOXhZTt6nFc",
"e": "AQAB",
"kty": "RSA",
"n": "vPnpkE3-HfNgJSru_K40LstkjiG2Bq_Tt-m0d_yUBBSbirFxF3qH4EXi7WrtZdeDahg2iV2BvpbVVj9GlmGo9OLol6jc7AP2yvZrkbABiiJhCbuPdkYbNpx6B7Itl8RT_bUSYAMZhmux5lpsn4weQ01fzjICi1rA-bIJpOfotdOjP4_lol-LxGZOGJQv9kndP8bgmssJb3Y_2s4gPtkmXySLrhpr5So-_6dVksyuBD9aLcnsMLDbywusjEMCdhqzQbvOjryomnmEXwyz_Ewb5HFK2PfgFtoHkdjqDz-mrEs3tw5g4TdYhCftzJxgbyNAEq4aEiOQrAncYyrXlotP_w",
"p": "8TNMF0WUe7CEeNVUTsuEcBAAXRguNtpvVifIjlwzFRGOYVGIpKuHsqQPKlZL07I9gPr9LifQnyQus3oEmTOrVs6LB9sfbukbg43ZRKoGVM40JYF5Xjs7R3mEZhgU0WaYOVe3iLtBGMfXNWFwlbfQP-zEb-dPCBX1jWT3LdgNBcE",
"q": "yJJLNc9w6O4y2icME8k99FugV9E7ObwUxF3v5JN3y1cmAT0h2njyE3iAGqaDZwcY1_jGCisjwoqX6i5E8xqhxX3Gcy3J7SmUAf8fhY8wU3zv9DK7skg2IdvanDb8Y1OM6GchbYZAOVPEg2IvVio8zI-Ih3DDwDk8Df0ufzoHRb8",
"qi": "zOE-4R3cjPesm3MX-4PdwmsaF9QZLUVRUvvHJ08pKs6kAXP18hzjctAoOjhQDxlTYqNYNePfKzKwost3OJoPgRIc9w9qwUCK1gNOS4Z_xozCIaXgMddNFhkoAfZ4JaKjNCiinzjGfqG99Lf-yzmmREuuhRv7SdS3ST4VQjiJQew"
},
"accounts": [
{
"username": "foo",
"algo": "plain",
"password": "bar",
"claims": {
"arcad_role": "user",
"arcad_tenant": "dev.cli",
"preferred_username": "Foo",
"sub": "foo"
}
}
]
}
}
},
"revision": 0
}

View File

@ -0,0 +1 @@
ALTER TABLE agents DROP COLUMN label;

View File

@ -0,0 +1 @@
ALTER TABLE agents ADD COLUMN label TEXT DEFAULT "";

View File

@ -0,0 +1 @@
ALTER TABLE agents DROP COLUMN contacted_at;

View File

@ -0,0 +1 @@
ALTER TABLE agents ADD COLUMN contacted_at datetime;

24
misc/jenkins/Dockerfile Normal file
View File

@ -0,0 +1,24 @@
FROM reg.cadoles.com/proxy_cache/library/ubuntu:22.04
ARG HTTP_PROXY=
ARG HTTPS_PROXY=
ARG http_proxy=
ARG https_proxy=
ARG GO_VERSION=1.19.2
# Install dev environment dependencies
RUN export DEBIAN_FRONTEND=noninteractive &&\
apt-get update -y &&\
apt-get install -y --no-install-recommends curl ca-certificates build-essential wget unzip tar git jq
# Install Go
RUN mkdir -p /tmp \
&& wget -O /tmp/go${GO_VERSION}.linux-amd64.tar.gz https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz \
&& rm -rf /usr/local/go \
&& mkdir -p /usr/local \
&& tar -C /usr/local -xzf /tmp/go${GO_VERSION}.linux-amd64.tar.gz
ENV PATH="${PATH}:/usr/local/go/bin"
# Add LetsEncrypt certificates
RUN curl -k https://forge.cadoles.com/Cadoles/Jenkins/raw/branch/master/resources/com/cadoles/common/add-letsencrypt-ca.sh | bash

View File

@ -21,6 +21,18 @@ agent:
enabled: true
dataDir: /var/lib/emissary/apps/data
downloadDir: /var/lib/emissary/apps/bundles
sysupgrade:
enabled: true
sysupgradeCommand:
- sysupgrade
- --force
- -u
- -v
- '%FIRMWARE%'
firmwareVersionCommand:
- sh
- -c
- source /etc/openwrt_release && echo "$DISTRIB_ID-$DISTRIB_RELEASE-$DISTRIB_REVISION"
collectors:
- name: uname
command: uname

View File

@ -9,7 +9,7 @@ server:
port: 3000
database:
driver: sqlite
dsn: sqlite:///var/lib/emissary/data.sqlite
dsn: sqlite:///var/lib/emissary/data.sqlite?_pragma=foreign_keys(1)&_pragma=busy_timeout=60000
cors:
allowedOrigins: []
allowCredentials: true

View File

@ -1,10 +0,0 @@
{
"apps": {
"edge.sdk.client.test": {
"url": "http://localhost:3001/edge.sdk.client.test_0.0.0.zip",
"sha256sum": "58019192dacdae17755707719707db007e26dac856102280583fbd18427dd352",
"format": "zip",
"address": "127.0.0.1:8081"
}
}
}

View File

@ -1,65 +0,0 @@
@baseUrl = http://localhost:3000
### Get agents
# @name getAgents
GET {{ baseUrl }}/api/v1/agents
Content-Type: application/json
@agentId = {{ getAgents.response.body.Data.Agents.0.ID }}
### Update an agent (accept it)
PUT {{ baseUrl }}/api/v1/agents/{{ agentId }}
Content-Type: application/json
{
"Status": 1
}
### Get an agent
GET {{ baseUrl }}/api/v1/agents/{{ agentId }}
Content-Type: application/json
### Get an agent specs
# @name getSpecs
GET {{ baseUrl }}/api/v1/agents/{{ agentId }}/specs
Content-Type: application/json
@specName = {{ getSpecs.response.body.Data.Specs.0.Name }}
### Update an agent specs
POST {{ baseUrl }}/api/v1/agents/{{ agentId }}/specs
Content-Type: application/json
{
"Name": "gateway.emissary.cadoles.com",
"Revision": 2,
"Data": {
"gateways": {
"cadoles.com":{
"address":":3003",
"target":"https://www.cadoles.com"
}
}
}
}
### Delete an agent spec
DELETE {{ baseUrl }}/api/v1/agents/{{ agentId }}/specs
Content-Type: application/json
{
"Name": "gateway.emissary.cadoles.com"
}
### Update UCI spec with uhttpd config
POST {{ baseUrl }}/api/v1/agents/2/specs
Content-Type: application/json
< ./uci-spec.payload.json

View File

@ -1,163 +0,0 @@
{
"Name": "uci.emissary.cadoles.com",
"Revision": 6,
"Data": {
"config": {
"packages": [
{
"name": "uhttpd",
"configs": [
{
"name": "uhttpd",
"section": "main",
"options": [
{
"type": "list",
"name": "listen_http",
"value": "0.0.0.0:8080"
},
{
"type": "list",
"name": "listen_http",
"value": "[::]:8080"
},
{
"type": "list",
"name": "listen_https",
"value": "0.0.0.0:8443"
},
{
"type": "list",
"name": "listen_https",
"value": "[::]:8443"
},
{
"type": "option",
"name": "redirect_https",
"value": "0"
},
{
"type": "option",
"name": "home",
"value": "/www"
},
{
"type": "option",
"name": "rfc1918_filter",
"value": "1"
},
{
"type": "option",
"name": "max_requests",
"value": "3"
},
{
"type": "option",
"name": "max_connections",
"value": "100"
},
{
"type": "option",
"name": "cert",
"value": "/etc/uhttpd.crt"
},
{
"type": "option",
"name": "key",
"value": "/etc/uhttpd.key"
},
{
"type": "option",
"name": "cgi_prefix",
"value": "/cgi-bin"
},
{
"type": "list",
"name": "lua_prefix",
"value": "/cgi-bin/luci=/usr/lib/lua/luci/sgi/uhttpd.lua"
},
{
"type": "option",
"name": "script_timeout",
"value": "60"
},
{
"type": "option",
"name": "network_timeout",
"value": "30"
},
{
"type": "option",
"name": "http_keepalive",
"value": "20"
},
{
"type": "option",
"name": "tcp_keepalive",
"value": "1"
},
{
"type": "option",
"name": "ubus_prefix",
"value": "/ubus"
}
]
},
{
"name": "cert",
"section": "defaults",
"options": [
{
"type": "option",
"name": "days",
"value": "730"
},
{
"type": "option",
"name": "key_type",
"value": "ec"
},
{
"type": "option",
"name": "bits",
"value": "2048"
},
{
"type": "option",
"name": "ec_curve",
"value": "P-256"
},
{
"type": "option",
"name": "country",
"value": "ZZ"
},
{
"type": "option",
"name": "state",
"value": "Somewhere"
},
{
"type": "option",
"name": "location",
"value": "Unknown"
},
{
"type": "option",
"name": "commonname",
"value": "OpenWrt"
}
]
}
]
}
]
},
"postImportCommands": [
{
"command": "reload_config",
"args": []
}
]
}
}

View File

@ -0,0 +1,61 @@
{
"apps": {
"edge.portal": {
"url": "https://emissary.cadol.es/files/apps/edge.portal_v2023.4.9-41c100d.zip",
"sha256sum": "b73a6741654f3e24281e354b3b506b109dac6ada8a9698452f52b03a53299a7d",
"address": ":8082",
"format": "zip"
},
"app.arcad.edge.hextris": {
"url": "https://emissary.cadol.es/files/apps/app.arcad.edge.hextris_v2023.4.11-81fb4c4.zip",
"sha256sum": "6d70f65971b3dd288da32d8d004ab8fbca030398b5c12e3c052ef98c53a6b81a",
"address": ":8083",
"format": "zip"
},
"edge.sdk.client.test": {
"url": "https://emissary.cadol.es/files/apps/edge.sdk.client.test_v2023.4.11-f5283b8.zip",
"sha256sum": "785d9f8d427900e1bb27ab85a33e8b1cbd1b6a1f8b2eab6366dc215a69655ade",
"address": ":8084",
"format": "zip"
},
"arcad.diffusion": {
"url": "https://emissary.cadol.es/files/apps/arcad.diffusion_v2023.4.9-81046a2.zip",
"sha256sum": "b8770adfaaf60e6d3e7776e0a090e6e7a0b31f3f9425b91168b42144d0346513",
"address": ":8085",
"format": "zip"
}
},
"config": {
"appUrlResolving": {
"ifaceMappings": {
"lo": "http://{{ .DeviceIP }}:{{ .AppPort }}",
"wlp4s0": "http://{{ .DeviceIP }}:{{ .AppPort }}",
"enp0s31f6": "http://{{ .DeviceIP }}:{{ .AppPort }}"
},
"defaultUrlTemplate": "http://{{ last ( splitList \".\" ( toString .Manifest.ID ) ) }}.localhost.arcad.lan:8080"
},
"unexpectedHostRedirect": {
"acceptedHostPatterns": ["arcad.lan", "*.arcad.lan", "arcad-*.local", "*.*.*.*"],
"hostTarget": "localhost.arcad.lan"
},
"auth": {
"local": {
"key": "absolutlynotsecret",
"cookieDuration": "1h",
"accounts": [
{
"username": "admin",
"algo": "plain",
"password": "admin",
"claims": {
"arcad_role": "admin",
"arcad_tenant": "x86",
"preferred_username": "Admin",
"sub": "admin"
}
}
]
}
}
}
}

View File

@ -0,0 +1,29 @@
{
"services": {
"arcad": {
"type": "_http._tcp",
"port": 8080,
"host": "arcad"
},
"portal": {
"type": "_http._tcp",
"port": 8080,
"host": "arcad-portal"
},
"hextris": {
"type": "_http._tcp",
"port": 8080,
"host": "arcad-hextris"
},
"test": {
"type": "_http._tcp",
"port": 8080,
"host": "arcad-test"
},
"diffusion": {
"type": "_http._tcp",
"port": 8080,
"host": "arcad-diffusion"
}
}
}

View File

@ -0,0 +1,45 @@
{
"proxies": {
"main": {
"address": ":8080",
"mappings": [
{
"hostPattern": "portal.localhost.arcad.lan:*",
"target": "http://localhost:8082"
},
{
"hostPattern": "hextris.localhost.arcad.lan:*",
"target": "http://localhost:8083"
},
{
"hostPattern": "test.localhost.arcad.lan:*",
"target": "http://localhost:8084"
},
{
"hostPattern": "diffusion.localhost.arcad.lan:*",
"target": "http://localhost:8085"
},
{
"hostPattern": "arcad-portal.local:*",
"target": "http://localhost:8082"
},
{
"hostPattern": "arcad-hextris.local:*",
"target": "http://localhost:8083"
},
{
"hostPattern": "arcad-test.local:*",
"target": "http://localhost:8084"
},
{
"hostPattern": "arcad-diffusion.local:*",
"target": "http://localhost:8085"
},
{
"hostPattern": "*",
"target": "http://localhost:8082"
}
]
}
}
}

View File

@ -1,4 +1,5 @@
**/*.go
internal/**/*.json
modd.conf
tmp/config.yml
.env {