diff --git a/.claude/skills/release_strix/SKILL.md b/.claude/skills/release_strix/SKILL.md index 27a6c07..0d7a0e8 100644 --- a/.claude/skills/release_strix/SKILL.md +++ b/.claude/skills/release_strix/SKILL.md @@ -121,7 +121,7 @@ Verify the new version tag exists and both amd64 and arm64 platforms are present ```bash docker run --rm -d --name strix-smoke-test -p 14567:4567 eduard256/strix:$VERSION sleep 5 -curl -s http://localhost:14567/api/v1/health | jq '.version' +curl -s http://localhost:14567/api | jq '.version' docker stop strix-smoke-test ``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cfb72ac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM golang:1.26-alpine AS builder + +RUN apk add --no-cache gcc musl-dev + +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +ARG VERSION=dev +RUN CGO_ENABLED=1 go build -ldflags "-s -w -X main.version=${VERSION}" -o /strix . + +FROM alpine:latest + +RUN apk add --no-cache ffmpeg ca-certificates + +COPY --from=builder /strix /usr/local/bin/strix + +WORKDIR /app +COPY cameras.db . + +EXPOSE 4567 + +HEALTHCHECK --interval=30s --timeout=3s CMD wget -q --spider http://localhost:4567/api/health || exit 1 + +USER nobody +ENTRYPOINT ["strix"] diff --git a/internal/api/api.go b/internal/api/api.go index 601f0b4..cf86300 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -108,7 +108,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) { func apiHealth(w http.ResponseWriter, r *http.Request) { ResponseJSON(w, map[string]any{ "version": app.Version, - "uptime": time.Since(app.StartTime).String(), + "uptime": time.Since(app.StartTime).Truncate(time.Second).String(), }) } diff --git a/internal/probe/probe.go b/internal/probe/probe.go index f56e679..109b789 100644 --- a/internal/probe/probe.go +++ b/internal/probe/probe.go @@ -29,7 +29,7 @@ func Init() { log = app.GetLogger("probe") var err error - db, err = sql.Open("sqlite3", app.DB+"?mode=ro") + db, err = sql.Open("sqlite3", "file:"+app.DB+"?mode=ro&immutable=1") if err != nil { log.Error().Err(err).Msg("[probe] db open") } diff --git a/internal/search/search.go b/internal/search/search.go index 00a8d70..732dd18 100644 --- a/internal/search/search.go +++ b/internal/search/search.go @@ -21,7 +21,7 @@ func Init() { log = app.GetLogger("search") var err error - db, err = sql.Open("sqlite3", app.DB+"?mode=ro") + db, err = sql.Open("sqlite3", "file:"+app.DB+"?mode=ro&immutable=1") if err != nil { log.Fatal().Err(err).Msg("[search] db open") } diff --git a/pkg/camdb/streams.go b/pkg/camdb/streams.go index 251adb3..7207008 100644 --- a/pkg/camdb/streams.go +++ b/pkg/camdb/streams.go @@ -3,8 +3,8 @@ package camdb import ( "database/sql" "encoding/base64" - "errors" "fmt" + "net/url" "strconv" "strings" ) @@ -125,26 +125,6 @@ func BuildStreams(db *sql.DB, p *StreamParams) ([]string, error) { return streams, nil } -// ValidateID checks if id format is valid -func ValidateID(id string) error { - switch { - case strings.HasPrefix(id, "b:"): - if len(id) < 3 { - return errors.New("camdb: empty brand id") - } - case strings.HasPrefix(id, "m:"): - if strings.Count(id, ":") < 2 { - return fmt.Errorf("camdb: invalid model id: %s", id) - } - case strings.HasPrefix(id, "p:"): - if len(id) < 3 { - return errors.New("camdb: empty preset id") - } - default: - return fmt.Errorf("camdb: unknown prefix: %s", id) - } - return nil -} // internals @@ -153,7 +133,7 @@ func buildURL(protocol, path, ip string, port int, user, pass string, channel in var auth string if user != "" { - auth = user + ":" + pass + "@" + auth = url.PathEscape(user) + ":" + url.PathEscape(pass) + "@" } host := ip @@ -174,6 +154,10 @@ func replacePlaceholders(s, ip string, port int, user, pass string, channel int) auth = base64.StdEncoding.EncodeToString([]byte(user + ":" + pass)) } + // URL-encode credentials for safe use in query parameters + encUser := url.QueryEscape(user) + encPass := url.QueryEscape(pass) + pairs := []string{ "[CHANNEL]", strconv.Itoa(channel), "[channel]", strconv.Itoa(channel), @@ -183,12 +167,12 @@ func replacePlaceholders(s, ip string, port int, user, pass string, channel int) "[channel+1]", strconv.Itoa(channel + 1), "{CHANNEL+1}", strconv.Itoa(channel + 1), "{channel+1}", strconv.Itoa(channel + 1), - "[USERNAME]", user, "[username]", user, - "[USER]", user, "[user]", user, - "[PASSWORD]", pass, "[password]", pass, - "[PASWORD]", pass, "[pasword]", pass, - "[PASS]", pass, "[pass]", pass, - "[PWD]", pass, "[pwd]", pass, + "[USERNAME]", encUser, "[username]", encUser, + "[USER]", encUser, "[user]", encUser, + "[PASSWORD]", encPass, "[password]", encPass, + "[PASWORD]", encPass, "[pasword]", encPass, + "[PASS]", encPass, "[pass]", encPass, + "[PWD]", encPass, "[pwd]", encPass, "[WIDTH]", "640", "[width]", "640", "[HEIGHT]", "480", "[height]", "480", "[IP]", ip, "[ip]", ip,