Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c655abae2 | ||
|
|
b87cc6fb62 | ||
|
|
96f551be80 | ||
|
|
73ab6405aa | ||
|
|
8fa62f9b91 | ||
|
|
c4e513f99c | ||
|
|
941d4fb10d |
No files matched your search
@@ -0,0 +1,68 @@
|
|||||||
|
# Builds and publishes a release when a version tag (v0.2.0, v1.0.0-rc.1)
|
||||||
|
# is pushed:
|
||||||
|
# * a Gitea release with the CHANGELOG notes, the linux tarballs, the
|
||||||
|
# .debs and SHA256SUMS;
|
||||||
|
# * the .debs (amd64, arm64) in the owner's Debian package registry:
|
||||||
|
# distribution "stable", or "testing" for a pre-release (a version
|
||||||
|
# with a hyphen), which Gitea also marks as a pre-release.
|
||||||
|
#
|
||||||
|
# Needs a repository secret RELEASE_TOKEN: an access token of a user who
|
||||||
|
# can write to the repository, with the write:repository and write:package
|
||||||
|
# scopes. docs/development.md ("Releasing") has the steps.
|
||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["v*"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Check the tag against VERSION and CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
version=$(cat VERSION)
|
||||||
|
if [ "$GITHUB_REF_NAME" != "v$version" ]; then
|
||||||
|
echo "::error::tag $GITHUB_REF_NAME does not match VERSION ($version)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# The release notes (a pre-release may use its final version's
|
||||||
|
# section or Unreleased; see scripts/gitea-release.sh).
|
||||||
|
if ! scripts/gitea-release.sh notes > /dev/null; then
|
||||||
|
echo "::error::no release notes for $version in CHANGELOG.md"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Tools
|
||||||
|
run: |
|
||||||
|
missing=
|
||||||
|
for t in jq dpkg-deb curl; do command -v $t >/dev/null || missing="$missing $t"; done
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
sudo=; [ "$(id -u)" = 0 ] || sudo=sudo
|
||||||
|
$sudo apt-get update -qq && $sudo apt-get install -y -qq jq dpkg curl
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: make test
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
# Checked against VERSION above; don't depend on the checkout
|
||||||
|
# having the tag.
|
||||||
|
RELEASE_TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
export DEB_MAINTAINER="$(git log -1 --format='%an <%ae>')"
|
||||||
|
make dist
|
||||||
|
make deb DEB_ARCH=amd64
|
||||||
|
make deb DEB_ARCH=arm64
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
run: scripts/gitea-release.sh
|
||||||
@@ -51,7 +51,8 @@ make e2e # with ../housecarl's lab: needs Docker (DOCKER_HOST=unix:///var/r
|
|||||||
|
|
||||||
As in Housecarl: commit only when asked, never push; branches
|
As in Housecarl: commit only when asked, never push; branches
|
||||||
`dev-X.Y.Z`; a release is a commit "Release X" setting `VERSION`, with
|
`dev-X.Y.Z`; a release is a commit "Release X" setting `VERSION`, with
|
||||||
the annotated tag `vX` ("Guard X").
|
the annotated tag `vX` ("Guard X"). Pushing the tag runs
|
||||||
|
`.gitea/workflows/release.yml` (docs/development.md, "Releasing").
|
||||||
|
|
||||||
## Shell hygiene
|
## Shell hygiene
|
||||||
|
|
||||||
|
|||||||
+23
-1
@@ -25,4 +25,26 @@ All notable changes to Guard are documented here. The format follows
|
|||||||
`test` (a watch's patterns over a log), `status` (through a local
|
`test` (a watch's patterns over a log), `status` (through a local
|
||||||
socket), `bans`, `ban`, `unban`, `version`.
|
socket), `bans`, `ban`, `unban`, `version`.
|
||||||
- A Debian package with a sandboxed systemd unit, as the `guard` user in
|
- A Debian package with a sandboxed systemd unit, as the `guard` user in
|
||||||
the `adm` and `systemd-journal` groups.
|
the `adm` and `systemd-journal` groups, published with each release to
|
||||||
|
the Gitea Debian registry (`stable`, or `testing` for a release
|
||||||
|
candidate) along with tarballs for Linux amd64 and arm64.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The package's `guard` user is now in the `adm` and `systemd-journal`
|
||||||
|
groups itself, as the documentation said, not only through its unit's
|
||||||
|
`SupplementaryGroups`. `sudo -u guard guard test` and `guard check`
|
||||||
|
could read neither `/var/log`'s files (`permission denied`) nor the
|
||||||
|
journal. Upgrading adds the groups.
|
||||||
|
- Shorter, plainer command output. Tables name a journal source by its
|
||||||
|
first two units or identifiers (`journal postfix/smtpd,
|
||||||
|
postfix/submission/smtpd +1`), so `presets`, `check` and `status`
|
||||||
|
fit a normal terminal. `guard test` prints the first 20 matching
|
||||||
|
lines (not 200) and the first 20 addresses it would ban (`--all` for
|
||||||
|
every one), single addresses without `/32`, and counts the matches
|
||||||
|
from `never_ban` addresses. When Housecarl doesn't answer, `guard check`
|
||||||
|
says so once ("Housecarl at URL doesn't answer: connection refused")
|
||||||
|
instead of once per ban list with the request's URL, and only asks for
|
||||||
|
`log:read` when Housecarl refused the log. `guard status` tells "not
|
||||||
|
running" from "no permission". `guard bans` says when there are none;
|
||||||
|
`guard unban` says "1 ban lifted".
|
||||||
@@ -6,6 +6,11 @@
|
|||||||
# sudo make install install the binary, completion, systemd unit and sysusers file,
|
# sudo make install install the binary, completion, systemd unit and sysusers file,
|
||||||
# and an example /etc/guard/guard.yaml
|
# and an example /etc/guard/guard.yaml
|
||||||
# make deb the Debian package in dist/ (DEB_ARCH=arm64 for another architecture)
|
# make deb the Debian package in dist/ (DEB_ARCH=arm64 for another architecture)
|
||||||
|
# make dist static binaries for linux/amd64 and linux/arm64 in dist/
|
||||||
|
# (guard-<version>-linux-<arch>.tar.gz)
|
||||||
|
#
|
||||||
|
# Guard is Linux only (it reads the journal), so there is no cli-dist as in
|
||||||
|
# Housecarl and Steward.
|
||||||
|
|
||||||
VERSION ?= $(shell cat VERSION 2>/dev/null || echo 0.0.0-dev)
|
VERSION ?= $(shell cat VERSION 2>/dev/null || echo 0.0.0-dev)
|
||||||
RELEASE_TAG ?= $(shell t=v$(VERSION); git tag --points-at HEAD 2>/dev/null | grep -qx "$$t" && git diff --quiet HEAD 2>/dev/null && echo $$t)
|
RELEASE_TAG ?= $(shell t=v$(VERSION); git tag --points-at HEAD 2>/dev/null | grep -qx "$$t" && git diff --quiet HEAD 2>/dev/null && echo $$t)
|
||||||
@@ -30,7 +35,7 @@ DEB_DEV_VERSION := $(shell [ -n '$(DESCRIBE)' ] || exit 0; \
|
|||||||
echo '$(DESCRIBE)' | sed -E 's/^v(.*)-([0-9]+)-g([0-9a-f]{7})[0-9a-f]*$$/\1+git\2.g\3/' | tr - '~' | sed "s/$$/$$m/")
|
echo '$(DESCRIBE)' | sed -E 's/^v(.*)-([0-9]+)-g([0-9a-f]{7})[0-9a-f]*$$/\1+git\2.g\3/' | tr - '~' | sed "s/$$/$$m/")
|
||||||
DEB_VERSION ?= $(if $(RELEASE_TAG),$(subst -,~,$(VERSION)),$(or $(DEB_DEV_VERSION),$(subst -,~,$(VERSION))))
|
DEB_VERSION ?= $(if $(RELEASE_TAG),$(subst -,~,$(VERSION)),$(or $(DEB_DEV_VERSION),$(subst -,~,$(VERSION))))
|
||||||
|
|
||||||
.PHONY: all build completion test e2e install install-files uninstall deb clean
|
.PHONY: all build completion test e2e install install-files uninstall deb dist clean
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@@ -81,5 +86,11 @@ uninstall:
|
|||||||
deb: build
|
deb: build
|
||||||
DEB_ARCH=$(DEB_ARCH) DEB_VERSION=$(DEB_VERSION) VERSION=$(VERSION) LDFLAGS="$(LDFLAGS)" GOFLAGS="$(GOFLAGS)" scripts/mkdeb.sh
|
DEB_ARCH=$(DEB_ARCH) DEB_VERSION=$(DEB_VERSION) VERSION=$(VERSION) LDFLAGS="$(LDFLAGS)" GOFLAGS="$(GOFLAGS)" scripts/mkdeb.sh
|
||||||
|
|
||||||
|
dist: build
|
||||||
|
for arch in amd64 arm64; do \
|
||||||
|
GOOS=linux GOARCH=$$arch go build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o dist/guard-$(VERSION)-linux-$$arch/guard ./cmd/guard && \
|
||||||
|
tar -C dist -czf dist/guard-$(VERSION)-linux-$$arch.tar.gz guard-$(VERSION)-linux-$$arch || exit 1; \
|
||||||
|
done
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf bin dist
|
rm -rf bin dist
|
||||||
@@ -58,10 +58,11 @@ watch:
|
|||||||
* [configuration.md](docs/configuration.md): `/etc/guard/guard.yaml`
|
* [configuration.md](docs/configuration.md): `/etc/guard/guard.yaml`
|
||||||
* [presets.md](docs/presets.md): the presets, and writing your own patterns
|
* [presets.md](docs/presets.md): the presets, and writing your own patterns
|
||||||
* [cli.md](docs/cli.md): the `guard` command
|
* [cli.md](docs/cli.md): the `guard` command
|
||||||
|
* [development.md](docs/development.md): building, testing, releasing
|
||||||
* [CHANGELOG.md](CHANGELOG.md)
|
* [CHANGELOG.md](CHANGELOG.md)
|
||||||
|
|
||||||
Housecarl's side: its [docs/bans.md](https://gitea.adbraun.lol/adam/housecarl/src/branch/main/docs/bans.md).
|
Housecarl's side: its [docs/bans.md](https://gitea.adbraun.lol/adam/housecarl/src/branch/main/docs/bans.md).
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
0.1.0 in development. Go, `CGO_ENABLED=0`, Linux. MIT licence.
|
Guard 0.1.0 is in testing (0.1.0-rc.1+git7-b87cc6f). Go, `CGO_ENABLED=0`, Linux. MIT licence.
|
||||||
+100
-29
@@ -14,6 +14,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -161,7 +162,7 @@ func checkCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
lists[x.Ban.List] = true
|
lists[x.Ban.List] = true
|
||||||
drops = drops || x.Kind() == config.SourceHousecarlDrops
|
drops = drops || x.Kind() == config.SourceHousecarlDrops
|
||||||
fmt.Fprintf(w, "%s\t%s\t%d\t%d in %s\t%s\t%s\n", x.Name, daemon.Describe(x), len(x.Match), x.Hits, x.Within, x.Ban.List, ports(x.Ban.Protocol, x.Ban.Port))
|
fmt.Fprintf(w, "%s\t%s\t%d\t%d in %s\t%s\t%s\n", x.Name, daemon.Short(x), len(x.Match), x.Hits, x.Within, x.Ban.List, ports(x.Ban.Protocol, x.Ban.Port))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
hc, err := client(cfg)
|
hc, err := client(cfg)
|
||||||
@@ -169,9 +170,17 @@ func checkCmd() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
bad := false
|
bad := false
|
||||||
|
names := make([]string, 0, len(lists))
|
||||||
for l := range lists {
|
for l := range lists {
|
||||||
|
names = append(names, l)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
for _, l := range names {
|
||||||
bl, err := hc.BanList(ctx(), l)
|
bl, err := hc.BanList(ctx(), l)
|
||||||
|
var down *housecarl.UnreachableError
|
||||||
switch {
|
switch {
|
||||||
|
case errors.As(err, &down):
|
||||||
|
return err
|
||||||
case err != nil:
|
case err != nil:
|
||||||
fmt.Printf("ban list %s: %v\n", l, err)
|
fmt.Printf("ban list %s: %v\n", l, err)
|
||||||
bad = true
|
bad = true
|
||||||
@@ -183,7 +192,11 @@ func checkCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
if drops {
|
if drops {
|
||||||
if _, err := hc.Log(ctx(), time.Now()); err != nil {
|
if _, err := hc.Log(ctx(), time.Now()); err != nil {
|
||||||
fmt.Printf("Housecarl's log (housecarl-drops): %v (the token needs log:read)\n", err)
|
hint := ""
|
||||||
|
if housecarl.Refused(err) {
|
||||||
|
hint = " (the token needs log:read)"
|
||||||
|
}
|
||||||
|
fmt.Printf("Housecarl's log (housecarl-drops): %v%s\n", err, hint)
|
||||||
bad = true
|
bad = true
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Housecarl's log (housecarl-drops): ok")
|
fmt.Println("Housecarl's log (housecarl-drops): ok")
|
||||||
@@ -266,7 +279,7 @@ func presetsCmd() *cobra.Command {
|
|||||||
fmt.Fprintln(w, "PRESET\tSOURCE\tWHAT")
|
fmt.Fprintln(w, "PRESET\tSOURCE\tWHAT")
|
||||||
for _, n := range names {
|
for _, n := range names {
|
||||||
p := ps[n]
|
p := ps[n]
|
||||||
fmt.Fprintf(w, "%s\t%s\t%s\n", n, daemon.Describe(&config.Watch{File: p.File, Journal: p.Journal, Source: p.Source, Protocol: "tcp"}), p.Description)
|
fmt.Fprintf(w, "%s\t%s\t%s\n", n, daemon.Short(&config.Watch{File: p.File, Journal: p.Journal, Source: p.Source, Protocol: "tcp"}), p.Description)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
@@ -331,10 +344,14 @@ the watch's window (within) is used.`,
|
|||||||
count int
|
count int
|
||||||
ban bool
|
ban bool
|
||||||
}
|
}
|
||||||
|
type found struct {
|
||||||
|
addr, text, why string
|
||||||
|
}
|
||||||
hits := map[string]*hit{}
|
hits := map[string]*hit{}
|
||||||
order := []string{}
|
order := []string{}
|
||||||
tr := map[string][]time.Time{}
|
tr := map[string][]time.Time{}
|
||||||
matched := 0
|
var shown []found
|
||||||
|
matched, spared := 0, 0
|
||||||
for _, l := range in {
|
for _, l := range in {
|
||||||
a, ok := m.Match(l.Text)
|
a, ok := m.Match(l.Text)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -342,17 +359,14 @@ the watch's window (within) is used.`,
|
|||||||
}
|
}
|
||||||
matched++
|
matched++
|
||||||
why := never.Why(a)
|
why := never.Why(a)
|
||||||
if all || matched <= 200 {
|
if all || len(shown) < testShown {
|
||||||
note := ""
|
shown = append(shown, found{a.String(), l.Text, why})
|
||||||
if why != "" {
|
|
||||||
note = " (never banned: " + why + ")"
|
|
||||||
}
|
|
||||||
fmt.Printf("%-39s %s%s\n", a, clip(l.Text, 160), note)
|
|
||||||
}
|
}
|
||||||
if why != "" {
|
if why != "" {
|
||||||
|
spared++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
g := match.Group(a, 64).String()
|
g := address(match.Group(a, 64))
|
||||||
h := hits[g]
|
h := hits[g]
|
||||||
if h == nil {
|
if h == nil {
|
||||||
h = &hit{}
|
h = &hit{}
|
||||||
@@ -371,39 +385,72 @@ the watch's window (within) is used.`,
|
|||||||
h.ban = h.ban || h.count >= w.Hits
|
h.ban = h.ban || h.count >= w.Hits
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if matched > 200 && !all {
|
width := 0
|
||||||
fmt.Printf("… %d more matching lines (--all shows them)\n", matched-200)
|
for _, f := range shown {
|
||||||
|
width = max(width, len(f.addr))
|
||||||
}
|
}
|
||||||
fmt.Printf("\n%d lines, %d matched, %d addresses (IPv6 by /64)\n", len(in), matched, len(order))
|
for _, f := range shown {
|
||||||
|
note := ""
|
||||||
|
if f.why != "" {
|
||||||
|
note = " (never banned: " + f.why + ")"
|
||||||
|
}
|
||||||
|
fmt.Printf("%-*s %s%s\n", width, f.addr, clip(f.text, 120), note)
|
||||||
|
}
|
||||||
|
if matched > len(shown) {
|
||||||
|
fmt.Printf("… and %d more matching lines (--all shows them)\n", matched-len(shown))
|
||||||
|
}
|
||||||
|
fmt.Printf("\n%d lines, %d matched, %s (IPv6 by /64)", len(in), matched, plural(len(order), "address"))
|
||||||
|
if spared > 0 {
|
||||||
|
fmt.Printf("; %d matches from never_ban addresses", spared)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
sort.SliceStable(order, func(i, j int) bool { return hits[order[i]].count > hits[order[j]].count })
|
sort.SliceStable(order, func(i, j int) bool { return hits[order[i]].count > hits[order[j]].count })
|
||||||
banned := 0
|
var banned []string
|
||||||
for _, g := range order {
|
for _, g := range order {
|
||||||
if hits[g].ban {
|
if hits[g].ban {
|
||||||
banned++
|
banned = append(banned, g)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if banned == 0 {
|
rule := fmt.Sprintf("%d hits within %s", w.Hits, w.Within)
|
||||||
fmt.Printf("nobody would be banned (%d hits within %s)\n", w.Hits, w.Within)
|
if !timed {
|
||||||
|
rule = fmt.Sprintf("%d hits; a file's lines have no times, so all of them count", w.Hits)
|
||||||
|
}
|
||||||
|
if len(banned) == 0 {
|
||||||
|
fmt.Printf("Nobody would be banned (%s)\n", rule)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
window := "within " + w.Within.String()
|
fmt.Printf("Would ban %d (%s):\n", len(banned), rule)
|
||||||
if !timed {
|
width = 0
|
||||||
window = "in all (a file's lines have no time)"
|
for _, g := range banned {
|
||||||
|
width = max(width, len(g))
|
||||||
}
|
}
|
||||||
fmt.Printf("would ban %d (%d hits %s):\n", banned, w.Hits, window)
|
for i, g := range banned {
|
||||||
for _, g := range order {
|
if i == testShown && !all {
|
||||||
if hits[g].ban {
|
fmt.Printf(" … and %d more (--all shows them)\n", len(banned)-i)
|
||||||
fmt.Printf(" %-39s %d matches\n", g, hits[g].count)
|
break
|
||||||
}
|
}
|
||||||
|
fmt.Printf(" %-*s %s\n", width, g, plural(hits[g].count, "match"))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().IntVarP(&lines, "lines", "n", 5000, "a journal watch without FILE: the last this many entries")
|
cmd.Flags().IntVarP(&lines, "lines", "n", 5000, "a journal watch without FILE: the last this many entries")
|
||||||
cmd.Flags().BoolVar(&all, "all", false, "print every matching line, not the first 200")
|
cmd.Flags().BoolVar(&all, "all", false, "print every matching line and address, not the first 20")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testShown is how many matching lines, and addresses to ban, guard
|
||||||
|
// test prints without --all.
|
||||||
|
const testShown = 20
|
||||||
|
|
||||||
|
// address is a ban's address: a single address without its /32 or /128.
|
||||||
|
func address(p netip.Prefix) string {
|
||||||
|
if p.IsSingleIP() {
|
||||||
|
return p.Addr().String()
|
||||||
|
}
|
||||||
|
return p.String()
|
||||||
|
}
|
||||||
|
|
||||||
func readLines(path string) ([]source.Line, error) {
|
func readLines(path string) ([]source.Line, error) {
|
||||||
var r io.Reader = os.Stdin
|
var r io.Reader = os.Stdin
|
||||||
if path != "-" {
|
if path != "-" {
|
||||||
@@ -473,8 +520,17 @@ func statusCmd() *cobra.Command {
|
|||||||
return d.DialContext(c, "unix", cfg.Socket)
|
return d.DialContext(c, "unix", cfg.Socket)
|
||||||
}}}
|
}}}
|
||||||
resp, err := hc.Get("http://guard/status")
|
resp, err := hc.Get("http://guard/status")
|
||||||
if err != nil {
|
switch {
|
||||||
return fmt.Errorf("Guard isn't running, or %s can't be reached (as root, or the guard user): %w", cfg.Socket, err)
|
case errors.Is(err, os.ErrNotExist), errors.Is(err, syscall.ECONNREFUSED):
|
||||||
|
return fmt.Errorf("Guard isn't running (nothing answers on %s)", cfg.Socket)
|
||||||
|
case errors.Is(err, os.ErrPermission):
|
||||||
|
return fmt.Errorf("no permission for %s: run this as root or the guard user", cfg.Socket)
|
||||||
|
case err != nil:
|
||||||
|
var ue *url.Error
|
||||||
|
if errors.As(err, &ue) {
|
||||||
|
err = ue.Err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Guard's socket %s: %w", cfg.Socket, err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
var s daemon.Status
|
var s daemon.Status
|
||||||
@@ -519,6 +575,17 @@ func statusCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// plural is "1 ban", "2 bans", "2 matches".
|
||||||
|
func plural(n int, what string) string {
|
||||||
|
if n == 1 {
|
||||||
|
return "1 " + what
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(what, "s") || strings.HasSuffix(what, "ch") {
|
||||||
|
return strconv.Itoa(n) + " " + what + "es"
|
||||||
|
}
|
||||||
|
return strconv.Itoa(n) + " " + what + "s"
|
||||||
|
}
|
||||||
|
|
||||||
func splitComma(s string) []string {
|
func splitComma(s string) []string {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -562,6 +629,10 @@ func bansCmd() *cobra.Command {
|
|||||||
if flagJSON {
|
if flagJSON {
|
||||||
return printJSON(bans)
|
return printJSON(bans)
|
||||||
}
|
}
|
||||||
|
if len(bans) == 0 {
|
||||||
|
fmt.Printf("No active bans in %s\n", l)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
table(func(w io.Writer) {
|
table(func(w io.Writer) {
|
||||||
fmt.Fprintln(w, "ID\tADDRESS\tPORT\tENDS IN\tBY\tSOURCE")
|
fmt.Fprintln(w, "ID\tADDRESS\tPORT\tENDS IN\tBY\tSOURCE")
|
||||||
for _, b := range bans {
|
for _, b := range bans {
|
||||||
@@ -668,7 +739,7 @@ func unbanCmd() *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("%d bans lifted\n", n)
|
fmt.Printf("%s lifted\n", plural(n, "ban"))
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
# Guard's system user (systemd-sysusers): it reads logs and talks to
|
# Guard's system user (systemd-sysusers): it reads logs and talks to
|
||||||
# Housecarl, nothing more.
|
# Housecarl, nothing more. adm reads /var/log's files, systemd-journal
|
||||||
|
# the journal, for the daemon (its unit adds them too) and for
|
||||||
|
# 'sudo -u guard guard test'.
|
||||||
u guard - "Guard log watcher" /var/lib/guard /usr/sbin/nologin
|
u guard - "Guard log watcher" /var/lib/guard /usr/sbin/nologin
|
||||||
|
m guard adm
|
||||||
|
m guard systemd-journal
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Developing Guard
|
||||||
|
|
||||||
|
[AGENTS.md](../AGENTS.md) has the layout and the ground rules.
|
||||||
|
|
||||||
|
## Building and testing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make # bin/guard and its bash completion
|
||||||
|
make test # go vet, gofmt check, go test ./...
|
||||||
|
make e2e # scripts/e2e.sh: needs Docker and ../housecarl
|
||||||
|
make deb # dist/guard_<version>_<arch>.deb
|
||||||
|
make dist # dist/guard-<version>-linux-<arch>.tar.gz (amd64, arm64)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Unit tests** need neither Housecarl nor a journal: the daemon tests
|
||||||
|
ban into a fake Housecarl, the journal test runs a fake journalctl
|
||||||
|
(`GUARD_JOURNALCTL`), and `TestPresets` runs each preset over its sample
|
||||||
|
lines in `testdata/presets/`.
|
||||||
|
|
||||||
|
**The e2e test** borrows Housecarl's lab: a router container (Debian 13,
|
||||||
|
nftables 1.1.3, the `housecarl-nft` image that Housecarl's `make e2e`
|
||||||
|
makes) runs housecarld built from `../housecarl` (or `HOUSECARL_SRC`),
|
||||||
|
a LAN server runs Guard over a log file, and a host on the internet
|
||||||
|
misbehaves. It checks a ban at the router (one port, for that server
|
||||||
|
alone), `never_ban`, bans waiting while Housecarl is down, and Guard on
|
||||||
|
the router banning scanners from Housecarl's log of dropped packets.
|
||||||
|
`KEEP=1` leaves the lab running; `DOCKER_HOST` points at another Docker
|
||||||
|
daemon.
|
||||||
|
|
||||||
|
The systemd sandbox is only tested by installing the package in a
|
||||||
|
container that runs systemd (`--privileged --cgroupns=host /sbin/init`).
|
||||||
|
|
||||||
|
## Releasing
|
||||||
|
|
||||||
|
As for Jarl, Steward and Housecarl:
|
||||||
|
|
||||||
|
1. Move "Unreleased" in `CHANGELOG.md` under `## [<version>] - <date>`.
|
||||||
|
A release candidate may leave it there.
|
||||||
|
2. Put the version in `VERSION`, and update the README's status line.
|
||||||
|
3. Run `make test && make e2e`.
|
||||||
|
4. Commit as "Release X", tag it `git tag -a vX -m "Guard X"`, and push.
|
||||||
|
|
||||||
|
The tag starts `.gitea/workflows/release.yml`, which checks the tag
|
||||||
|
against `VERSION` and the CHANGELOG, runs the tests, builds the tarballs
|
||||||
|
and the `.deb`s (amd64, arm64), and publishes them
|
||||||
|
(`scripts/gitea-release.sh`): a Gitea release with the notes and the
|
||||||
|
files, and the `.deb`s in the owner's Debian registry, `stable` for a
|
||||||
|
release and `testing` for a release candidate. It needs the repository
|
||||||
|
secret `RELEASE_TOKEN` (`write:repository`, `write:package`).
|
||||||
+17
-2
@@ -28,11 +28,26 @@ A watch that reads Housecarl's own log (`housecarl-drops`) also needs
|
|||||||
|
|
||||||
## 2. The package
|
## 2. The package
|
||||||
|
|
||||||
|
Guard's releases publish its Debian package to the same Gitea registry
|
||||||
|
as Jarl's, Steward's and Housecarl's: `stable` for releases, `testing`
|
||||||
|
for release candidates. If apt already has it, `sudo apt install guard`
|
||||||
|
is all it takes. Otherwise add it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo apt install ./guard_0.1.0_amd64.deb
|
sudo install -d /etc/apt/keyrings
|
||||||
|
sudo curl -fsSL https://gitea.adbraun.lol/api/packages/adam/debian/repository.key -o /etc/apt/keyrings/gitea-adam.asc
|
||||||
|
echo "deb [signed-by=/etc/apt/keyrings/gitea-adam.asc] https://gitea.adbraun.lol/api/packages/adam/debian stable main" \
|
||||||
|
| sudo tee /etc/apt/sources.list.d/gitea-adam.list
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install guard
|
||||||
```
|
```
|
||||||
|
|
||||||
It makes the `guard` user (in the `adm` and `systemd-journal` groups,
|
For release candidates too, add the `testing` distribution: the same
|
||||||
|
`deb` line with `testing` instead of `stable`. The release page also has
|
||||||
|
the `.deb` files (`sudo apt install ./guard_0.1.0_amd64.deb`) and
|
||||||
|
tarballs for Linux amd64 and arm64.
|
||||||
|
|
||||||
|
The package makes the `guard` user (in the `adm` and `systemd-journal` groups,
|
||||||
to read logs), installs `guard.service` without starting it, and writes
|
to read logs), installs `guard.service` without starting it, and writes
|
||||||
an example `/etc/guard/guard.yaml`.
|
an example `/etc/guard/guard.yaml`.
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,28 @@ func Describe(w *config.Watch) string {
|
|||||||
return "file " + w.File
|
return "file " + w.File
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Short describes a watch's source in a table's few columns: at most
|
||||||
|
// two journal units or identifiers, then how many more.
|
||||||
|
func Short(w *config.Watch) string {
|
||||||
|
switch w.Kind() {
|
||||||
|
case "journal":
|
||||||
|
names := w.Journal.Unit
|
||||||
|
if len(names) == 0 {
|
||||||
|
names = w.Journal.Identifier
|
||||||
|
} else {
|
||||||
|
names = append(append([]string(nil), names...), w.Journal.Identifier...)
|
||||||
|
}
|
||||||
|
s := "journal " + strings.Join(names[:min(len(names), 2)], ", ")
|
||||||
|
if len(names) > 2 {
|
||||||
|
s += fmt.Sprintf(" +%d", len(names)-2)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
case config.SourceHousecarlDrops:
|
||||||
|
return "Housecarl drops (" + w.Protocol + ")"
|
||||||
|
}
|
||||||
|
return "file " + w.File
|
||||||
|
}
|
||||||
|
|
||||||
// loadGroups asks Housecarl how each ban list groups IPv6 addresses, so
|
// loadGroups asks Housecarl how each ban list groups IPv6 addresses, so
|
||||||
// bounties are counted as the list bans. A list that can't be read now
|
// bounties are counted as the list bans. A list that can't be read now
|
||||||
// counts by /64, its default.
|
// counts by /64, its default.
|
||||||
@@ -409,7 +431,7 @@ type WatchStatus struct {
|
|||||||
func (d *Daemon) Status() Status {
|
func (d *Daemon) Status() Status {
|
||||||
s := Status{Started: d.started}
|
s := Status{Started: d.started}
|
||||||
for _, w := range d.watches {
|
for _, w := range d.watches {
|
||||||
s.Watches = append(s.Watches, WatchStatus{Name: w.Cfg.Name, Source: Describe(w.Cfg), List: w.Cfg.Ban.List, Hits: w.Cfg.Hits,
|
s.Watches = append(s.Watches, WatchStatus{Name: w.Cfg.Name, Source: Short(w.Cfg), List: w.Cfg.Ban.List, Hits: w.Cfg.Hits,
|
||||||
Within: w.Cfg.Within.String(), Lines: w.lines.Load(), Matches: w.matches.Load(), Bans: w.bans.Load(), Top: w.Tracker.Top(10, d.Now())})
|
Within: w.Cfg.Within.String(), Lines: w.lines.Load(), Matches: w.matches.Load(), Bans: w.bans.Load(), Top: w.Tracker.Top(10, d.Now())})
|
||||||
}
|
}
|
||||||
s.Recent, s.Pending = d.state.Snapshot()
|
s.Recent, s.Pending = d.state.Snapshot()
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ const Prefix = "/api/v1"
|
|||||||
|
|
||||||
// Client talks to one Housecarl.
|
// Client talks to one Housecarl.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
base string
|
base string
|
||||||
token string
|
socket string
|
||||||
http *http.Client
|
token string
|
||||||
|
http *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options say how to reach Housecarl.
|
// Options say how to reach Housecarl.
|
||||||
@@ -72,7 +73,7 @@ func New(o Options) (*Client, error) {
|
|||||||
}
|
}
|
||||||
tr.TLSClientConfig = cfg
|
tr.TLSClientConfig = cfg
|
||||||
}
|
}
|
||||||
return &Client{base: base, token: strings.TrimSpace(o.Token), http: &http.Client{Transport: tr, Timeout: timeout}}, nil
|
return &Client{base: base, socket: o.Socket, token: strings.TrimSpace(o.Token), http: &http.Client{Transport: tr, Timeout: timeout}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error is an error answer from Housecarl.
|
// Error is an error answer from Housecarl.
|
||||||
@@ -89,6 +90,32 @@ func (e *Error) Error() string {
|
|||||||
return e.Message
|
return e.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnreachableError is a request Housecarl never answered: no
|
||||||
|
// connection, a TLS failure, a timeout.
|
||||||
|
type UnreachableError struct {
|
||||||
|
Where string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UnreachableError) Error() string {
|
||||||
|
err := e.Err
|
||||||
|
var ue *url.Error
|
||||||
|
if errors.As(err, &ue) {
|
||||||
|
err = ue.Err // without the method and the URL with its query
|
||||||
|
}
|
||||||
|
return "Housecarl at " + e.Where + " doesn't answer: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *UnreachableError) Unwrap() error { return e.Err }
|
||||||
|
|
||||||
|
// where is where the client reaches Housecarl, for messages.
|
||||||
|
func (c *Client) where() string {
|
||||||
|
if c.socket != "" {
|
||||||
|
return c.socket
|
||||||
|
}
|
||||||
|
return c.base
|
||||||
|
}
|
||||||
|
|
||||||
// Refused reports whether Housecarl refused a request for good: asking
|
// Refused reports whether Housecarl refused a request for good: asking
|
||||||
// again won't help (a bad request, no permission, an address never
|
// again won't help (a bad request, no permission, an address never
|
||||||
// banned). Anything else (no answer, a server error) is worth retrying.
|
// banned). Anything else (no answer, a server error) is worth retrying.
|
||||||
@@ -123,7 +150,7 @@ func (c *Client) do(ctx context.Context, method, path string, query url.Values,
|
|||||||
}
|
}
|
||||||
resp, err := c.http.Do(req)
|
resp, err := c.http.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return &UnreachableError{Where: c.where(), Err: err}
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
data, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
|
data, _ := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
|
||||||
|
|||||||
+2
-2
@@ -142,10 +142,10 @@ not_contains "$NET2"
|
|||||||
run "ssh from it still works" 0 connect 22 $NET2
|
run "ssh from it still works" 0 connect 22 $NET2
|
||||||
contains "^ssh$"
|
contains "^ssh$"
|
||||||
run "guard test over the log" 0 docker exec ${P}-srv /w/guard -c /w/guard.yaml test ssh /w/auth.log
|
run "guard test over the log" 0 docker exec ${P}-srv /w/guard -c /w/guard.yaml test ssh /w/auth.log
|
||||||
contains "would ban 1"
|
contains "Would ban 1"
|
||||||
contains "never banned: in never_ban $NET2/32"
|
contains "never banned: in never_ban $NET2/32"
|
||||||
run "guard unban" 0 docker exec ${P}-srv /w/guard -c /w/guard.yaml unban $NET
|
run "guard unban" 0 docker exec ${P}-srv /w/guard -c /w/guard.yaml unban $NET
|
||||||
contains "1 bans lifted"
|
contains "1 ban lifted"
|
||||||
run "ssh again" 0 connect 22
|
run "ssh again" 0 connect 22
|
||||||
contains "^ssh$"
|
contains "^ssh$"
|
||||||
|
|
||||||
|
|||||||
Executable
+151
@@ -0,0 +1,151 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Publishes a built release (make dist, make deb for each
|
||||||
|
# architecture) to Gitea:
|
||||||
|
#
|
||||||
|
# * a release for tag v<VERSION> with the CHANGELOG section for VERSION as
|
||||||
|
# its notes, and every file in dist/ (tarballs, .debs)
|
||||||
|
# plus a SHA256SUMS of them attached;
|
||||||
|
# * the .debs in the owner's Debian package registry, so servers can
|
||||||
|
# 'apt install guard' and get updates with apt (docs/install.md).
|
||||||
|
#
|
||||||
|
# Run by .gitea/workflows/release.yml when a v* tag is pushed. Running it
|
||||||
|
# again for the same version replaces the attachments and packages.
|
||||||
|
#
|
||||||
|
# A version with a hyphen (0.2.0-rc.1) is a pre-release: Gitea marks the
|
||||||
|
# release so and doesn't count it as the latest, and its packages go to
|
||||||
|
# the "testing" distribution, so servers that follow "stable" never get
|
||||||
|
# a pre-release from apt.
|
||||||
|
#
|
||||||
|
# Release notes: the CHANGELOG section "## [VERSION]". A pre-release
|
||||||
|
# without one of its own uses the section of the version it leads up to
|
||||||
|
# ("## [0.2.0]"), or else "## [Unreleased]".
|
||||||
|
#
|
||||||
|
# 'gitea-release.sh notes' only prints the notes, failing if there are
|
||||||
|
# none (the workflow checks this before building).
|
||||||
|
#
|
||||||
|
# Variables:
|
||||||
|
# GITEA_URL e.g. https://gitea.example.com (in Actions:
|
||||||
|
# GITHUB_SERVER_URL)
|
||||||
|
# GITEA_REPO owner/name (in Actions: GITHUB_REPOSITORY)
|
||||||
|
# GITEA_TOKEN a token with write:repository and write:package
|
||||||
|
# VERSION default: the VERSION file
|
||||||
|
# DEB_DISTRIBUTION the apt distribution (default: stable, testing for
|
||||||
|
# a pre-release)
|
||||||
|
# DEB_COMPONENT the apt component (default: main)
|
||||||
|
#
|
||||||
|
# Needs curl and jq.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
VERSION=${VERSION:-$(cat VERSION)}
|
||||||
|
prerelease=false
|
||||||
|
case $VERSION in *-*) prerelease=true ;; esac
|
||||||
|
|
||||||
|
# section prints the CHANGELOG section headed "## [$1]" (without the
|
||||||
|
# heading); nothing if there is none.
|
||||||
|
section() {
|
||||||
|
awk -v v="$1" '
|
||||||
|
/^## \[/ { if (on) exit; on = index($0, "## [" v "]") == 1; next }
|
||||||
|
on' CHANGELOG.md
|
||||||
|
}
|
||||||
|
|
||||||
|
# The release notes (see above). A section with only blank lines counts
|
||||||
|
# as missing.
|
||||||
|
notes=$(section "$VERSION")
|
||||||
|
from="[$VERSION]"
|
||||||
|
if [ "$prerelease" = true ]; then
|
||||||
|
for s in "${VERSION%%-*}" Unreleased; do
|
||||||
|
[ -z "$(printf '%s' "$notes" | tr -d '[:space:]')" ] || break
|
||||||
|
notes=$(section "$s")
|
||||||
|
from="[$s]"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [ -z "$(printf '%s' "$notes" | tr -d '[:space:]')" ]; then
|
||||||
|
if [ "$prerelease" = true ]; then
|
||||||
|
echo "gitea-release: CHANGELOG.md has no '## [$VERSION]', '## [${VERSION%%-*}]' or '## [Unreleased]' section with notes" >&2
|
||||||
|
else
|
||||||
|
echo "gitea-release: CHANGELOG.md has no '## [$VERSION]' section with notes" >&2
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "${1:-}" = notes ]; then
|
||||||
|
echo "gitea-release: notes for $VERSION from CHANGELOG.md $from" >&2
|
||||||
|
printf '%s\n' "$notes"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
: "${GITEA_URL:=${GITHUB_SERVER_URL:-}}" "${GITEA_REPO:=${GITHUB_REPOSITORY:-}}"
|
||||||
|
: "${GITEA_URL:?set GITEA_URL}" "${GITEA_REPO:?set GITEA_REPO}" "${GITEA_TOKEN:?set GITEA_TOKEN}"
|
||||||
|
if [ "$prerelease" = true ]; then
|
||||||
|
DEB_DISTRIBUTION=${DEB_DISTRIBUTION:-testing}
|
||||||
|
else
|
||||||
|
DEB_DISTRIBUTION=${DEB_DISTRIBUTION:-stable}
|
||||||
|
fi
|
||||||
|
DEB_COMPONENT=${DEB_COMPONENT:-main}
|
||||||
|
GITEA_URL=${GITEA_URL%/}
|
||||||
|
owner=${GITEA_REPO%%/*}
|
||||||
|
tag=v$VERSION
|
||||||
|
api=$GITEA_URL/api/v1/repos/$GITEA_REPO
|
||||||
|
|
||||||
|
# curl with the token; fails on HTTP errors except the codes in $ok.
|
||||||
|
call() {
|
||||||
|
ok=$1
|
||||||
|
shift
|
||||||
|
out=$(mktemp)
|
||||||
|
code=$(curl -sS -o "$out" -w '%{http_code}' -H "Authorization: token $GITEA_TOKEN" "$@")
|
||||||
|
case " 200 201 204 $ok " in
|
||||||
|
*" $code "*) cat "$out"; rm -f "$out"; return 0 ;;
|
||||||
|
esac
|
||||||
|
echo "gitea-release: HTTP $code from $*" >&2
|
||||||
|
cat "$out" >&2
|
||||||
|
echo >&2
|
||||||
|
rm -f "$out"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
files=$(find dist -maxdepth 1 -type f ! -name SHA256SUMS | sort)
|
||||||
|
[ -n "$files" ] || { echo "gitea-release: nothing in dist/" >&2; exit 1; }
|
||||||
|
# (Debian package versions have ~ for -: guard_0.2.0~rc.1_amd64.deb.)
|
||||||
|
debversion=$(printf '%s' "$VERSION" | tr - '~')
|
||||||
|
for f in $files; do
|
||||||
|
case $f in *"$VERSION"* | *"$debversion"*) ;; *) echo "gitea-release: $f is not version $VERSION" >&2; exit 1 ;; esac
|
||||||
|
done
|
||||||
|
# shellcheck disable=SC2046 # file names have no spaces
|
||||||
|
(cd dist && sha256sum $(printf '%s\n' "$files" | sed 's|^dist/||')) > dist/SHA256SUMS
|
||||||
|
files="$files dist/SHA256SUMS"
|
||||||
|
|
||||||
|
# The release (created, or found when publishing again).
|
||||||
|
release=$(call 404 "$api/releases/tags/$(printf '%s' "$tag" | sed 's/+/%2B/g')")
|
||||||
|
id=$(printf '%s\n' "$release" | jq -r '.id // empty')
|
||||||
|
if [ -z "$id" ]; then
|
||||||
|
body=$(jq -n --arg tag "$tag" --arg notes "$notes" --argjson pre "$prerelease" \
|
||||||
|
'{tag_name: $tag, name: ("Guard " + $tag), body: $notes, prerelease: $pre}')
|
||||||
|
release=$(call "" -X POST -H 'Content-Type: application/json' -d "$body" "$api/releases")
|
||||||
|
id=$(printf '%s\n' "$release" | jq -r .id)
|
||||||
|
echo "gitea-release: created release $tag"
|
||||||
|
fi
|
||||||
|
for f in $files; do
|
||||||
|
name=${f#dist/}
|
||||||
|
old=$(printf '%s\n' "$release" | jq -r --arg n "$name" '.assets[]? | select(.name == $n) | .id')
|
||||||
|
[ -z "$old" ] || call "" -X DELETE "$api/releases/$id/assets/$old" >/dev/null
|
||||||
|
# A + (build metadata, 0.1.0-rc.1+git4-8fa62f9) would be a space in a query.
|
||||||
|
call "" -X POST -F "attachment=@$f" "$api/releases/$id/assets?name=$(printf '%s' "$name" | sed 's/+/%2B/g')" >/dev/null
|
||||||
|
echo "gitea-release: attached $name"
|
||||||
|
done
|
||||||
|
|
||||||
|
# The Debian packages. Gitea refuses a package version it already has
|
||||||
|
# (409), so publishing again deletes it first.
|
||||||
|
pool=$GITEA_URL/api/packages/$owner/debian/pool/$DEB_DISTRIBUTION/$DEB_COMPONENT
|
||||||
|
for f in dist/*.deb; do
|
||||||
|
[ -e "$f" ] || continue
|
||||||
|
# shellcheck disable=SC2016,SC2046 # dpkg-deb's fields; split into "$@"
|
||||||
|
set -- $(dpkg-deb --showformat '${Package} ${Version} ${Architecture}' --show "$f")
|
||||||
|
if ! call 409 --upload-file "$f" "$pool/upload" >/dev/null; then
|
||||||
|
echo "gitea-release: upload of $f failed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$code" = 409 ]; then
|
||||||
|
call "" -X DELETE "$pool/$1/$2/$3" >/dev/null
|
||||||
|
call "" --upload-file "$f" "$pool/upload" >/dev/null
|
||||||
|
fi
|
||||||
|
echo "gitea-release: uploaded $1 $2 $3 to $DEB_DISTRIBUTION/$DEB_COMPONENT"
|
||||||
|
done
|
||||||
Reference in new issue
Block a user