Compare commits

..

7 Commits

Author SHA1 Message Date
Deluan
759214cfbc chore(deps): update Go version to 1.26 in Dockerfile and go.mod
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-24 21:49:19 -05:00
Deluan
14343d91b0 chore(deps): update goose to 3.27.0
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-24 21:44:04 -05:00
Deluan
fc36f1daa6 chore(deps): update go-taglib dependency to latest version (mka fix)
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-24 21:19:11 -05:00
Deluan Quintão
652c27690b feat(plugins): add HTTP host service (#5095)
* feat(httpclient): implement HttpClient service for outbound HTTP requests in plugins

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(httpclient): enhance SSRF protection by validating host requests against private IPs

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(httpclient): support DELETE requests with body in HttpClient service

Signed-off-by: Deluan <deluan@navidrome.org>

* feat(httpclient): refactor HTTP client initialization and enhance redirect handling

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor(http): standardize naming conventions for HTTP types and methods

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor example plugin to use host.HTTPSend for improved error management

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(plugins): fix IPv6 SSRF bypass and wildcard host matching

Fix two bugs in the plugin HTTP/WebSocket host validation:

1. extractHostname now strips IPv6 brackets when no port is present
(e.g. "[::1]" → "::1"). Previously, net.SplitHostPort failed for
bracketed IPv6 without a port, leaving brackets intact. This caused
net.ParseIP to return nil, bypassing the private/loopback SSRF guard.

2. matchHostPattern now treats "*" as an allow-all pattern. Previously,
a bare "*" only matched via exact equality, so plugins declaring
requiredHosts: ["*"] (like webhook-rs) had all requests rejected.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-24 14:28:36 -05:00
Deluan Quintão
2bb13e5ff1 feat(server): add ExtAuth logout URL configuration (#5074)
* feat(server): add ExtAuth logout URL configuration (#4467)

When external authentication (reverse proxy auth) is active, the Logout
button is hidden because authentication is managed externally. Many
external auth services (Authelia, Authentik, Keycloak) provide a logout
URL that can terminate the session.

Add `ExtAuth.LogoutURL` config option that, when set, shows the Logout
button in the UI and redirects the user to the external auth provider's
logout endpoint instead of the Navidrome login page.

* feat(server): add validation for ExtAuth logout URL configuration

* feat(server): refactor ExtAuth logout URL validation to a reusable function

* fix(configuration): rename URL validation functions for consistency

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(configuration): rename URL validation functions for consistency

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-23 20:28:38 -05:00
dependabot[bot]
d1c5e6a2f2 chore(deps): bump goreleaser/goreleaser-action in /.github/workflows (#5089)
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 6 to 7.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 19:06:45 -05:00
Deluan
0c3cc86535 fix(subsonic): restore public attribute for playlists in XML responses
This was causing issues with DSub and DSub2000

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-23 18:17:44 -05:00
41 changed files with 1342 additions and 4016 deletions

View File

@@ -4,7 +4,7 @@
"dockerfile": "Dockerfile",
"args": {
// Update the VARIANT arg to pick a version of Go: 1, 1.15, 1.14
"VARIANT": "1.25",
"VARIANT": "1.26",
// Options
"INSTALL_NODE": "true",
"NODE_VERSION": "v24",

View File

@@ -117,7 +117,7 @@ jobs:
- name: Test
run: |
pkg-config --define-prefix --cflags --libs taglib # for debugging
go test -shuffle=on -tags netgo,sqlite_fts5,sqlite_spellfix -race ./... -v
go test -shuffle=on -tags netgo,sqlite_fts5 -race ./... -v
- name: Test ndpgen
run: |
@@ -424,7 +424,7 @@ jobs:
run: echo 'RELEASE_FLAGS=--skip=publish --snapshot' >> $GITHUB_ENV
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
version: '~> v2'
args: "release --clean -f release/goreleaser.yml ${{ env.RELEASE_FLAGS }}"

View File

@@ -3,7 +3,6 @@ run:
build-tags:
- netgo
- sqlite_fts5
- sqlite_spellfix
linters:
enable:
- asasalint

View File

@@ -63,7 +63,7 @@ COPY --from=ui /build /build
########################################################################################################################
### Build Navidrome binary
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:1.25-trixie AS base
FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/golang:1.26-trixie AS base
RUN apt-get update && apt-get install -y clang lld
COPY --from=xx / /
WORKDIR /workspace
@@ -109,7 +109,7 @@ RUN --mount=type=bind,source=. \
export EXT=".exe"
fi
go build -tags=netgo,sqlite_fts5,sqlite_spellfix -ldflags="${LD_EXTRA} -w -s \
go build -tags=netgo,sqlite_fts5 -ldflags="${LD_EXTRA} -w -s \
-X github.com/navidrome/navidrome/consts.gitSha=${GIT_SHA} \
-X github.com/navidrome/navidrome/consts.gitTag=${GIT_TAG}" \
-o /out/navidrome${EXT} .

View File

@@ -1,6 +1,6 @@
GO_VERSION=$(shell grep "^go " go.mod | cut -f 2 -d ' ')
NODE_VERSION=$(shell cat .nvmrc)
GO_BUILD_TAGS=netgo,sqlite_fts5,sqlite_spellfix
GO_BUILD_TAGS=netgo,sqlite_fts5
# Set global environment variables, required for most targets
export CGO_CFLAGS_ALLOW=--define-prefix

View File

@@ -1,8 +0,0 @@
//go:build sqlite_spellfix
package buildtags
// SPELLFIX is required for the spellfix1 virtual table, used for fuzzy/approximate
// string matching. Without this tag, the SQLite driver won't include spellfix1 support.
var SQLITE_SPELLFIX = true

View File

@@ -250,6 +250,7 @@ type pluginsOptions struct {
type extAuthOptions struct {
TrustedSources string
UserHeader string
LogoutURL string
}
type searchOptions struct {
@@ -345,6 +346,7 @@ func Load(noConfigDump bool) {
validateBackupSchedule,
validatePlaylistsPath,
validatePurgeMissingOption,
validateURL("ExtAuth.LogoutURL", Server.ExtAuth.LogoutURL),
)
if err != nil {
os.Exit(1)
@@ -548,6 +550,33 @@ func validateSchedule(schedule, field string) (string, error) {
return schedule, err
}
// validateURL checks if the provided URL is valid and has either http or https scheme.
// It returns a function that can be used as a hook to validate URLs in the config.
func validateURL(optionName, optionURL string) func() error {
return func() error {
if optionURL == "" {
return nil
}
u, err := url.Parse(optionURL)
if err != nil {
log.Error(fmt.Sprintf("Invalid %s: it could not be parsed", optionName), "url", optionURL, "err", err)
return err
}
if u.Scheme != "http" && u.Scheme != "https" {
err := fmt.Errorf("invalid scheme for %s: '%s'. Only 'http' and 'https' are allowed", optionName, u.Scheme)
log.Error(err.Error())
return err
}
// Require an absolute URL with a non-empty host and no opaque component.
if u.Host == "" || u.Opaque != "" {
err := fmt.Errorf("invalid %s: '%s'. A full http(s) URL with a non-empty host is required", optionName, optionURL)
log.Error(err.Error())
return err
}
return nil
}
}
func normalizeSearchBackend(value string) string {
v := strings.ToLower(strings.TrimSpace(value))
switch v {
@@ -641,6 +670,7 @@ func setViperDefaults() {
viper.SetDefault("passwordencryptionkey", "")
viper.SetDefault("extauth.userheader", "Remote-User")
viper.SetDefault("extauth.trustedsources", "")
viper.SetDefault("extauth.logouturl", "")
viper.SetDefault("prometheus.enabled", false)
viper.SetDefault("prometheus.metricspath", consts.PrometheusDefaultPath)
viper.SetDefault("prometheus.password", "")

View File

@@ -52,6 +52,48 @@ var _ = Describe("Configuration", func() {
})
})
Describe("ValidateURL", func() {
It("accepts a valid http URL", func() {
fn := conf.ValidateURL("TestOption", "http://example.com/path")
Expect(fn()).To(Succeed())
})
It("accepts a valid https URL", func() {
fn := conf.ValidateURL("TestOption", "https://example.com/path")
Expect(fn()).To(Succeed())
})
It("rejects a URL with no scheme", func() {
fn := conf.ValidateURL("TestOption", "example.com/path")
Expect(fn()).To(MatchError(ContainSubstring("invalid scheme")))
})
It("rejects a URL with an unsupported scheme", func() {
fn := conf.ValidateURL("TestOption", "javascript://example.com/path")
Expect(fn()).To(MatchError(ContainSubstring("invalid scheme")))
})
It("accepts an empty URL (optional config)", func() {
fn := conf.ValidateURL("TestOption", "")
Expect(fn()).To(Succeed())
})
It("includes the option name in the error message", func() {
fn := conf.ValidateURL("MyOption", "ftp://example.com")
Expect(fn()).To(MatchError(ContainSubstring("MyOption")))
})
It("rejects a URL that cannot be parsed", func() {
fn := conf.ValidateURL("TestOption", "://invalid")
Expect(fn()).To(HaveOccurred())
})
It("rejects a URL without a host", func() {
fn := conf.ValidateURL("TestOption", "http:///path")
Expect(fn()).To(MatchError(ContainSubstring("non-empty host is required")))
})
})
DescribeTable("NormalizeSearchBackend",
func(input, expected string) {
Expect(conf.NormalizeSearchBackend(input)).To(Equal(expected))

View File

@@ -8,4 +8,6 @@ var SetViperDefaults = setViperDefaults
var ParseLanguages = parseLanguages
var ValidateURL = validateURL
var NormalizeSearchBackend = normalizeSearchBackend

View File

@@ -10,7 +10,6 @@ import (
"github.com/mattn/go-sqlite3"
"github.com/navidrome/navidrome/conf"
_ "github.com/navidrome/navidrome/db/migrations"
_ "github.com/navidrome/navidrome/db/spellfix"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils/hasher"
"github.com/navidrome/navidrome/utils/singleton"

View File

@@ -1,24 +0,0 @@
# spellfix1 SQLite Extension
This package statically compiles the [spellfix1](https://sqlite.org/spellfix1.html) SQLite extension
into the Navidrome binary. It is registered via `sqlite3_auto_extension` so that every new SQLite
connection has `spellfix1` available without loading a shared library.
## Vendored Files
The C source files are vendored because cgo cannot reference headers from other Go modules:
- **`spellfix.c`** — from the SQLite source tree: [`ext/misc/spellfix.c`](https://github.com/sqlite/sqlite/blob/master/ext/misc/spellfix.c)
- **`sqlite3ext.h`** — from the SQLite source tree: [`src/sqlite3ext.h`](https://github.com/sqlite/sqlite/blob/master/src/sqlite3ext.h)
## Updating
When upgrading `github.com/mattn/go-sqlite3`, run the update script to download
the matching spellfix1 source files for the bundled SQLite version:
```bash
./db/spellfix/update.sh
```
The script reads the SQLite version from go-sqlite3's `sqlite3-binding.h` and
downloads the corresponding files from the [SQLite GitHub mirror](https://github.com/sqlite/sqlite).

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +0,0 @@
//go:build sqlite_spellfix
package spellfix
/*
#cgo CFLAGS: -I${SRCDIR} -Wno-deprecated-declarations
// Avoid duplicate symbol conflicts with go-sqlite3.
// Rename the api pointer and entry point to unique names for this compilation unit.
#define sqlite3_api sqlite3_api_spellfix
#define sqlite3_spellfix_init sqlite3_spellfix_init_local
// Compile the extension into this binary.
// spellfix.c includes sqlite3ext.h and declares SQLITE_EXTENSION_INIT1.
#include "spellfix.c"
// sqlite3ext.h redefines sqlite3_auto_extension as a macro through the api
// struct. Undo that so we can call the real C function directly.
#undef sqlite3_auto_extension
// Provided by the SQLite library linked via go-sqlite3.
extern int sqlite3_auto_extension(void(*)(void));
// Register spellfix so it is available on every new sqlite3_open() connection.
static void register_spellfix(void) {
sqlite3_auto_extension((void(*)(void))sqlite3_spellfix_init_local);
}
*/
import "C"
func init() {
C.register_spellfix()
}

View File

@@ -1,57 +0,0 @@
//go:build sqlite_spellfix
package spellfix_test
import (
"database/sql"
"testing"
_ "github.com/mattn/go-sqlite3"
_ "github.com/navidrome/navidrome/db/spellfix"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestSpellfix(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Spellfix Suite")
}
var _ = Describe("spellfix1", func() {
var db *sql.DB
BeforeEach(func() {
var err error
db, err = sql.Open("sqlite3", ":memory:")
Expect(err).ToNot(HaveOccurred())
})
AfterEach(func() {
_ = db.Close()
})
It("creates a spellfix1 virtual table", func() {
_, err := db.Exec("CREATE VIRTUAL TABLE demo USING spellfix1")
Expect(err).ToNot(HaveOccurred())
})
It("returns fuzzy matches", func() {
_, err := db.Exec("CREATE VIRTUAL TABLE demo USING spellfix1")
Expect(err).ToNot(HaveOccurred())
_, err = db.Exec("INSERT INTO demo(word) VALUES ('hello'), ('world'), ('help')")
Expect(err).ToNot(HaveOccurred())
rows, err := db.Query("SELECT word FROM demo WHERE word MATCH 'helo' AND top=3")
Expect(err).ToNot(HaveOccurred())
defer rows.Close()
var words []string
for rows.Next() {
var word string
Expect(rows.Scan(&word)).To(Succeed())
words = append(words, word)
}
Expect(words).To(ContainElement("hello"))
})
})

View File

@@ -1,730 +0,0 @@
/*
** 2006 June 7
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
**
** May you do good and not evil.
** May you find forgiveness for yourself and forgive others.
** May you share freely, never taking more than you give.
**
*************************************************************************
** This header file defines the SQLite interface for use by
** shared libraries that want to be imported as extensions into
** an SQLite instance. Shared libraries that intend to be loaded
** as extensions by SQLite should #include this file instead of
** sqlite3.h.
*/
#ifndef SQLITE3EXT_H
#define SQLITE3EXT_H
#include "sqlite3.h"
/*
** The following structure holds pointers to all of the SQLite API
** routines.
**
** WARNING: In order to maintain backwards compatibility, add new
** interfaces to the end of this structure only. If you insert new
** interfaces in the middle of this structure, then older different
** versions of SQLite will not be able to load each other's shared
** libraries!
*/
struct sqlite3_api_routines {
void * (*aggregate_context)(sqlite3_context*,int nBytes);
int (*aggregate_count)(sqlite3_context*);
int (*bind_blob)(sqlite3_stmt*,int,const void*,int n,void(*)(void*));
int (*bind_double)(sqlite3_stmt*,int,double);
int (*bind_int)(sqlite3_stmt*,int,int);
int (*bind_int64)(sqlite3_stmt*,int,sqlite_int64);
int (*bind_null)(sqlite3_stmt*,int);
int (*bind_parameter_count)(sqlite3_stmt*);
int (*bind_parameter_index)(sqlite3_stmt*,const char*zName);
const char * (*bind_parameter_name)(sqlite3_stmt*,int);
int (*bind_text)(sqlite3_stmt*,int,const char*,int n,void(*)(void*));
int (*bind_text16)(sqlite3_stmt*,int,const void*,int,void(*)(void*));
int (*bind_value)(sqlite3_stmt*,int,const sqlite3_value*);
int (*busy_handler)(sqlite3*,int(*)(void*,int),void*);
int (*busy_timeout)(sqlite3*,int ms);
int (*changes)(sqlite3*);
int (*close)(sqlite3*);
int (*collation_needed)(sqlite3*,void*,void(*)(void*,sqlite3*,
int eTextRep,const char*));
int (*collation_needed16)(sqlite3*,void*,void(*)(void*,sqlite3*,
int eTextRep,const void*));
const void * (*column_blob)(sqlite3_stmt*,int iCol);
int (*column_bytes)(sqlite3_stmt*,int iCol);
int (*column_bytes16)(sqlite3_stmt*,int iCol);
int (*column_count)(sqlite3_stmt*pStmt);
const char * (*column_database_name)(sqlite3_stmt*,int);
const void * (*column_database_name16)(sqlite3_stmt*,int);
const char * (*column_decltype)(sqlite3_stmt*,int i);
const void * (*column_decltype16)(sqlite3_stmt*,int);
double (*column_double)(sqlite3_stmt*,int iCol);
int (*column_int)(sqlite3_stmt*,int iCol);
sqlite_int64 (*column_int64)(sqlite3_stmt*,int iCol);
const char * (*column_name)(sqlite3_stmt*,int);
const void * (*column_name16)(sqlite3_stmt*,int);
const char * (*column_origin_name)(sqlite3_stmt*,int);
const void * (*column_origin_name16)(sqlite3_stmt*,int);
const char * (*column_table_name)(sqlite3_stmt*,int);
const void * (*column_table_name16)(sqlite3_stmt*,int);
const unsigned char * (*column_text)(sqlite3_stmt*,int iCol);
const void * (*column_text16)(sqlite3_stmt*,int iCol);
int (*column_type)(sqlite3_stmt*,int iCol);
sqlite3_value* (*column_value)(sqlite3_stmt*,int iCol);
void * (*commit_hook)(sqlite3*,int(*)(void*),void*);
int (*complete)(const char*sql);
int (*complete16)(const void*sql);
int (*create_collation)(sqlite3*,const char*,int,void*,
int(*)(void*,int,const void*,int,const void*));
int (*create_collation16)(sqlite3*,const void*,int,void*,
int(*)(void*,int,const void*,int,const void*));
int (*create_function)(sqlite3*,const char*,int,int,void*,
void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
void (*xFinal)(sqlite3_context*));
int (*create_function16)(sqlite3*,const void*,int,int,void*,
void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
void (*xFinal)(sqlite3_context*));
int (*create_module)(sqlite3*,const char*,const sqlite3_module*,void*);
int (*data_count)(sqlite3_stmt*pStmt);
sqlite3 * (*db_handle)(sqlite3_stmt*);
int (*declare_vtab)(sqlite3*,const char*);
int (*enable_shared_cache)(int);
int (*errcode)(sqlite3*db);
const char * (*errmsg)(sqlite3*);
const void * (*errmsg16)(sqlite3*);
int (*exec)(sqlite3*,const char*,sqlite3_callback,void*,char**);
int (*expired)(sqlite3_stmt*);
int (*finalize)(sqlite3_stmt*pStmt);
void (*free)(void*);
void (*free_table)(char**result);
int (*get_autocommit)(sqlite3*);
void * (*get_auxdata)(sqlite3_context*,int);
int (*get_table)(sqlite3*,const char*,char***,int*,int*,char**);
int (*global_recover)(void);
void (*interruptx)(sqlite3*);
sqlite_int64 (*last_insert_rowid)(sqlite3*);
const char * (*libversion)(void);
int (*libversion_number)(void);
void *(*malloc)(int);
char * (*mprintf)(const char*,...);
int (*open)(const char*,sqlite3**);
int (*open16)(const void*,sqlite3**);
int (*prepare)(sqlite3*,const char*,int,sqlite3_stmt**,const char**);
int (*prepare16)(sqlite3*,const void*,int,sqlite3_stmt**,const void**);
void * (*profile)(sqlite3*,void(*)(void*,const char*,sqlite_uint64),void*);
void (*progress_handler)(sqlite3*,int,int(*)(void*),void*);
void *(*realloc)(void*,int);
int (*reset)(sqlite3_stmt*pStmt);
void (*result_blob)(sqlite3_context*,const void*,int,void(*)(void*));
void (*result_double)(sqlite3_context*,double);
void (*result_error)(sqlite3_context*,const char*,int);
void (*result_error16)(sqlite3_context*,const void*,int);
void (*result_int)(sqlite3_context*,int);
void (*result_int64)(sqlite3_context*,sqlite_int64);
void (*result_null)(sqlite3_context*);
void (*result_text)(sqlite3_context*,const char*,int,void(*)(void*));
void (*result_text16)(sqlite3_context*,const void*,int,void(*)(void*));
void (*result_text16be)(sqlite3_context*,const void*,int,void(*)(void*));
void (*result_text16le)(sqlite3_context*,const void*,int,void(*)(void*));
void (*result_value)(sqlite3_context*,sqlite3_value*);
void * (*rollback_hook)(sqlite3*,void(*)(void*),void*);
int (*set_authorizer)(sqlite3*,int(*)(void*,int,const char*,const char*,
const char*,const char*),void*);
void (*set_auxdata)(sqlite3_context*,int,void*,void (*)(void*));
char * (*xsnprintf)(int,char*,const char*,...);
int (*step)(sqlite3_stmt*);
int (*table_column_metadata)(sqlite3*,const char*,const char*,const char*,
char const**,char const**,int*,int*,int*);
void (*thread_cleanup)(void);
int (*total_changes)(sqlite3*);
void * (*trace)(sqlite3*,void(*xTrace)(void*,const char*),void*);
int (*transfer_bindings)(sqlite3_stmt*,sqlite3_stmt*);
void * (*update_hook)(sqlite3*,void(*)(void*,int ,char const*,char const*,
sqlite_int64),void*);
void * (*user_data)(sqlite3_context*);
const void * (*value_blob)(sqlite3_value*);
int (*value_bytes)(sqlite3_value*);
int (*value_bytes16)(sqlite3_value*);
double (*value_double)(sqlite3_value*);
int (*value_int)(sqlite3_value*);
sqlite_int64 (*value_int64)(sqlite3_value*);
int (*value_numeric_type)(sqlite3_value*);
const unsigned char * (*value_text)(sqlite3_value*);
const void * (*value_text16)(sqlite3_value*);
const void * (*value_text16be)(sqlite3_value*);
const void * (*value_text16le)(sqlite3_value*);
int (*value_type)(sqlite3_value*);
char *(*vmprintf)(const char*,va_list);
/* Added ??? */
int (*overload_function)(sqlite3*, const char *zFuncName, int nArg);
/* Added by 3.3.13 */
int (*prepare_v2)(sqlite3*,const char*,int,sqlite3_stmt**,const char**);
int (*prepare16_v2)(sqlite3*,const void*,int,sqlite3_stmt**,const void**);
int (*clear_bindings)(sqlite3_stmt*);
/* Added by 3.4.1 */
int (*create_module_v2)(sqlite3*,const char*,const sqlite3_module*,void*,
void (*xDestroy)(void *));
/* Added by 3.5.0 */
int (*bind_zeroblob)(sqlite3_stmt*,int,int);
int (*blob_bytes)(sqlite3_blob*);
int (*blob_close)(sqlite3_blob*);
int (*blob_open)(sqlite3*,const char*,const char*,const char*,sqlite3_int64,
int,sqlite3_blob**);
int (*blob_read)(sqlite3_blob*,void*,int,int);
int (*blob_write)(sqlite3_blob*,const void*,int,int);
int (*create_collation_v2)(sqlite3*,const char*,int,void*,
int(*)(void*,int,const void*,int,const void*),
void(*)(void*));
int (*file_control)(sqlite3*,const char*,int,void*);
sqlite3_int64 (*memory_highwater)(int);
sqlite3_int64 (*memory_used)(void);
sqlite3_mutex *(*mutex_alloc)(int);
void (*mutex_enter)(sqlite3_mutex*);
void (*mutex_free)(sqlite3_mutex*);
void (*mutex_leave)(sqlite3_mutex*);
int (*mutex_try)(sqlite3_mutex*);
int (*open_v2)(const char*,sqlite3**,int,const char*);
int (*release_memory)(int);
void (*result_error_nomem)(sqlite3_context*);
void (*result_error_toobig)(sqlite3_context*);
int (*sleep)(int);
void (*soft_heap_limit)(int);
sqlite3_vfs *(*vfs_find)(const char*);
int (*vfs_register)(sqlite3_vfs*,int);
int (*vfs_unregister)(sqlite3_vfs*);
int (*xthreadsafe)(void);
void (*result_zeroblob)(sqlite3_context*,int);
void (*result_error_code)(sqlite3_context*,int);
int (*test_control)(int, ...);
void (*randomness)(int,void*);
sqlite3 *(*context_db_handle)(sqlite3_context*);
int (*extended_result_codes)(sqlite3*,int);
int (*limit)(sqlite3*,int,int);
sqlite3_stmt *(*next_stmt)(sqlite3*,sqlite3_stmt*);
const char *(*sql)(sqlite3_stmt*);
int (*status)(int,int*,int*,int);
int (*backup_finish)(sqlite3_backup*);
sqlite3_backup *(*backup_init)(sqlite3*,const char*,sqlite3*,const char*);
int (*backup_pagecount)(sqlite3_backup*);
int (*backup_remaining)(sqlite3_backup*);
int (*backup_step)(sqlite3_backup*,int);
const char *(*compileoption_get)(int);
int (*compileoption_used)(const char*);
int (*create_function_v2)(sqlite3*,const char*,int,int,void*,
void (*xFunc)(sqlite3_context*,int,sqlite3_value**),
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
void (*xFinal)(sqlite3_context*),
void(*xDestroy)(void*));
int (*db_config)(sqlite3*,int,...);
sqlite3_mutex *(*db_mutex)(sqlite3*);
int (*db_status)(sqlite3*,int,int*,int*,int);
int (*extended_errcode)(sqlite3*);
void (*log)(int,const char*,...);
sqlite3_int64 (*soft_heap_limit64)(sqlite3_int64);
const char *(*sourceid)(void);
int (*stmt_status)(sqlite3_stmt*,int,int);
int (*strnicmp)(const char*,const char*,int);
int (*unlock_notify)(sqlite3*,void(*)(void**,int),void*);
int (*wal_autocheckpoint)(sqlite3*,int);
int (*wal_checkpoint)(sqlite3*,const char*);
void *(*wal_hook)(sqlite3*,int(*)(void*,sqlite3*,const char*,int),void*);
int (*blob_reopen)(sqlite3_blob*,sqlite3_int64);
int (*vtab_config)(sqlite3*,int op,...);
int (*vtab_on_conflict)(sqlite3*);
/* Version 3.7.16 and later */
int (*close_v2)(sqlite3*);
const char *(*db_filename)(sqlite3*,const char*);
int (*db_readonly)(sqlite3*,const char*);
int (*db_release_memory)(sqlite3*);
const char *(*errstr)(int);
int (*stmt_busy)(sqlite3_stmt*);
int (*stmt_readonly)(sqlite3_stmt*);
int (*stricmp)(const char*,const char*);
int (*uri_boolean)(const char*,const char*,int);
sqlite3_int64 (*uri_int64)(const char*,const char*,sqlite3_int64);
const char *(*uri_parameter)(const char*,const char*);
char *(*xvsnprintf)(int,char*,const char*,va_list);
int (*wal_checkpoint_v2)(sqlite3*,const char*,int,int*,int*);
/* Version 3.8.7 and later */
int (*auto_extension)(void(*)(void));
int (*bind_blob64)(sqlite3_stmt*,int,const void*,sqlite3_uint64,
void(*)(void*));
int (*bind_text64)(sqlite3_stmt*,int,const char*,sqlite3_uint64,
void(*)(void*),unsigned char);
int (*cancel_auto_extension)(void(*)(void));
int (*load_extension)(sqlite3*,const char*,const char*,char**);
void *(*malloc64)(sqlite3_uint64);
sqlite3_uint64 (*msize)(void*);
void *(*realloc64)(void*,sqlite3_uint64);
void (*reset_auto_extension)(void);
void (*result_blob64)(sqlite3_context*,const void*,sqlite3_uint64,
void(*)(void*));
void (*result_text64)(sqlite3_context*,const char*,sqlite3_uint64,
void(*)(void*), unsigned char);
int (*strglob)(const char*,const char*);
/* Version 3.8.11 and later */
sqlite3_value *(*value_dup)(const sqlite3_value*);
void (*value_free)(sqlite3_value*);
int (*result_zeroblob64)(sqlite3_context*,sqlite3_uint64);
int (*bind_zeroblob64)(sqlite3_stmt*, int, sqlite3_uint64);
/* Version 3.9.0 and later */
unsigned int (*value_subtype)(sqlite3_value*);
void (*result_subtype)(sqlite3_context*,unsigned int);
/* Version 3.10.0 and later */
int (*status64)(int,sqlite3_int64*,sqlite3_int64*,int);
int (*strlike)(const char*,const char*,unsigned int);
int (*db_cacheflush)(sqlite3*);
/* Version 3.12.0 and later */
int (*system_errno)(sqlite3*);
/* Version 3.14.0 and later */
int (*trace_v2)(sqlite3*,unsigned,int(*)(unsigned,void*,void*,void*),void*);
char *(*expanded_sql)(sqlite3_stmt*);
/* Version 3.18.0 and later */
void (*set_last_insert_rowid)(sqlite3*,sqlite3_int64);
/* Version 3.20.0 and later */
int (*prepare_v3)(sqlite3*,const char*,int,unsigned int,
sqlite3_stmt**,const char**);
int (*prepare16_v3)(sqlite3*,const void*,int,unsigned int,
sqlite3_stmt**,const void**);
int (*bind_pointer)(sqlite3_stmt*,int,void*,const char*,void(*)(void*));
void (*result_pointer)(sqlite3_context*,void*,const char*,void(*)(void*));
void *(*value_pointer)(sqlite3_value*,const char*);
int (*vtab_nochange)(sqlite3_context*);
int (*value_nochange)(sqlite3_value*);
const char *(*vtab_collation)(sqlite3_index_info*,int);
/* Version 3.24.0 and later */
int (*keyword_count)(void);
int (*keyword_name)(int,const char**,int*);
int (*keyword_check)(const char*,int);
sqlite3_str *(*str_new)(sqlite3*);
char *(*str_finish)(sqlite3_str*);
void (*str_appendf)(sqlite3_str*, const char *zFormat, ...);
void (*str_vappendf)(sqlite3_str*, const char *zFormat, va_list);
void (*str_append)(sqlite3_str*, const char *zIn, int N);
void (*str_appendall)(sqlite3_str*, const char *zIn);
void (*str_appendchar)(sqlite3_str*, int N, char C);
void (*str_reset)(sqlite3_str*);
int (*str_errcode)(sqlite3_str*);
int (*str_length)(sqlite3_str*);
char *(*str_value)(sqlite3_str*);
/* Version 3.25.0 and later */
int (*create_window_function)(sqlite3*,const char*,int,int,void*,
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
void (*xFinal)(sqlite3_context*),
void (*xValue)(sqlite3_context*),
void (*xInv)(sqlite3_context*,int,sqlite3_value**),
void(*xDestroy)(void*));
/* Version 3.26.0 and later */
const char *(*normalized_sql)(sqlite3_stmt*);
/* Version 3.28.0 and later */
int (*stmt_isexplain)(sqlite3_stmt*);
int (*value_frombind)(sqlite3_value*);
/* Version 3.30.0 and later */
int (*drop_modules)(sqlite3*,const char**);
/* Version 3.31.0 and later */
sqlite3_int64 (*hard_heap_limit64)(sqlite3_int64);
const char *(*uri_key)(const char*,int);
const char *(*filename_database)(const char*);
const char *(*filename_journal)(const char*);
const char *(*filename_wal)(const char*);
/* Version 3.32.0 and later */
const char *(*create_filename)(const char*,const char*,const char*,
int,const char**);
void (*free_filename)(const char*);
sqlite3_file *(*database_file_object)(const char*);
/* Version 3.34.0 and later */
int (*txn_state)(sqlite3*,const char*);
/* Version 3.36.1 and later */
sqlite3_int64 (*changes64)(sqlite3*);
sqlite3_int64 (*total_changes64)(sqlite3*);
/* Version 3.37.0 and later */
int (*autovacuum_pages)(sqlite3*,
unsigned int(*)(void*,const char*,unsigned int,unsigned int,unsigned int),
void*, void(*)(void*));
/* Version 3.38.0 and later */
int (*error_offset)(sqlite3*);
int (*vtab_rhs_value)(sqlite3_index_info*,int,sqlite3_value**);
int (*vtab_distinct)(sqlite3_index_info*);
int (*vtab_in)(sqlite3_index_info*,int,int);
int (*vtab_in_first)(sqlite3_value*,sqlite3_value**);
int (*vtab_in_next)(sqlite3_value*,sqlite3_value**);
/* Version 3.39.0 and later */
int (*deserialize)(sqlite3*,const char*,unsigned char*,
sqlite3_int64,sqlite3_int64,unsigned);
unsigned char *(*serialize)(sqlite3*,const char *,sqlite3_int64*,
unsigned int);
const char *(*db_name)(sqlite3*,int);
/* Version 3.40.0 and later */
int (*value_encoding)(sqlite3_value*);
/* Version 3.41.0 and later */
int (*is_interrupted)(sqlite3*);
/* Version 3.43.0 and later */
int (*stmt_explain)(sqlite3_stmt*,int);
/* Version 3.44.0 and later */
void *(*get_clientdata)(sqlite3*,const char*);
int (*set_clientdata)(sqlite3*, const char*, void*, void(*)(void*));
/* Version 3.50.0 and later */
int (*setlk_timeout)(sqlite3*,int,int);
/* Version 3.51.0 and later */
int (*set_errmsg)(sqlite3*,int,const char*);
int (*db_status64)(sqlite3*,int,sqlite3_int64*,sqlite3_int64*,int);
};
/*
** This is the function signature used for all extension entry points. It
** is also defined in the file "loadext.c".
*/
typedef int (*sqlite3_loadext_entry)(
sqlite3 *db, /* Handle to the database. */
char **pzErrMsg, /* Used to set error string on failure. */
const sqlite3_api_routines *pThunk /* Extension API function pointers. */
);
/*
** The following macros redefine the API routines so that they are
** redirected through the global sqlite3_api structure.
**
** This header file is also used by the loadext.c source file
** (part of the main SQLite library - not an extension) so that
** it can get access to the sqlite3_api_routines structure
** definition. But the main library does not want to redefine
** the API. So the redefinition macros are only valid if the
** SQLITE_CORE macros is undefined.
*/
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)
#define sqlite3_aggregate_context sqlite3_api->aggregate_context
#ifndef SQLITE_OMIT_DEPRECATED
#define sqlite3_aggregate_count sqlite3_api->aggregate_count
#endif
#define sqlite3_bind_blob sqlite3_api->bind_blob
#define sqlite3_bind_double sqlite3_api->bind_double
#define sqlite3_bind_int sqlite3_api->bind_int
#define sqlite3_bind_int64 sqlite3_api->bind_int64
#define sqlite3_bind_null sqlite3_api->bind_null
#define sqlite3_bind_parameter_count sqlite3_api->bind_parameter_count
#define sqlite3_bind_parameter_index sqlite3_api->bind_parameter_index
#define sqlite3_bind_parameter_name sqlite3_api->bind_parameter_name
#define sqlite3_bind_text sqlite3_api->bind_text
#define sqlite3_bind_text16 sqlite3_api->bind_text16
#define sqlite3_bind_value sqlite3_api->bind_value
#define sqlite3_busy_handler sqlite3_api->busy_handler
#define sqlite3_busy_timeout sqlite3_api->busy_timeout
#define sqlite3_changes sqlite3_api->changes
#define sqlite3_close sqlite3_api->close
#define sqlite3_collation_needed sqlite3_api->collation_needed
#define sqlite3_collation_needed16 sqlite3_api->collation_needed16
#define sqlite3_column_blob sqlite3_api->column_blob
#define sqlite3_column_bytes sqlite3_api->column_bytes
#define sqlite3_column_bytes16 sqlite3_api->column_bytes16
#define sqlite3_column_count sqlite3_api->column_count
#define sqlite3_column_database_name sqlite3_api->column_database_name
#define sqlite3_column_database_name16 sqlite3_api->column_database_name16
#define sqlite3_column_decltype sqlite3_api->column_decltype
#define sqlite3_column_decltype16 sqlite3_api->column_decltype16
#define sqlite3_column_double sqlite3_api->column_double
#define sqlite3_column_int sqlite3_api->column_int
#define sqlite3_column_int64 sqlite3_api->column_int64
#define sqlite3_column_name sqlite3_api->column_name
#define sqlite3_column_name16 sqlite3_api->column_name16
#define sqlite3_column_origin_name sqlite3_api->column_origin_name
#define sqlite3_column_origin_name16 sqlite3_api->column_origin_name16
#define sqlite3_column_table_name sqlite3_api->column_table_name
#define sqlite3_column_table_name16 sqlite3_api->column_table_name16
#define sqlite3_column_text sqlite3_api->column_text
#define sqlite3_column_text16 sqlite3_api->column_text16
#define sqlite3_column_type sqlite3_api->column_type
#define sqlite3_column_value sqlite3_api->column_value
#define sqlite3_commit_hook sqlite3_api->commit_hook
#define sqlite3_complete sqlite3_api->complete
#define sqlite3_complete16 sqlite3_api->complete16
#define sqlite3_create_collation sqlite3_api->create_collation
#define sqlite3_create_collation16 sqlite3_api->create_collation16
#define sqlite3_create_function sqlite3_api->create_function
#define sqlite3_create_function16 sqlite3_api->create_function16
#define sqlite3_create_module sqlite3_api->create_module
#define sqlite3_create_module_v2 sqlite3_api->create_module_v2
#define sqlite3_data_count sqlite3_api->data_count
#define sqlite3_db_handle sqlite3_api->db_handle
#define sqlite3_declare_vtab sqlite3_api->declare_vtab
#define sqlite3_enable_shared_cache sqlite3_api->enable_shared_cache
#define sqlite3_errcode sqlite3_api->errcode
#define sqlite3_errmsg sqlite3_api->errmsg
#define sqlite3_errmsg16 sqlite3_api->errmsg16
#define sqlite3_exec sqlite3_api->exec
#ifndef SQLITE_OMIT_DEPRECATED
#define sqlite3_expired sqlite3_api->expired
#endif
#define sqlite3_finalize sqlite3_api->finalize
#define sqlite3_free sqlite3_api->free
#define sqlite3_free_table sqlite3_api->free_table
#define sqlite3_get_autocommit sqlite3_api->get_autocommit
#define sqlite3_get_auxdata sqlite3_api->get_auxdata
#define sqlite3_get_table sqlite3_api->get_table
#ifndef SQLITE_OMIT_DEPRECATED
#define sqlite3_global_recover sqlite3_api->global_recover
#endif
#define sqlite3_interrupt sqlite3_api->interruptx
#define sqlite3_last_insert_rowid sqlite3_api->last_insert_rowid
#define sqlite3_libversion sqlite3_api->libversion
#define sqlite3_libversion_number sqlite3_api->libversion_number
#define sqlite3_malloc sqlite3_api->malloc
#define sqlite3_mprintf sqlite3_api->mprintf
#define sqlite3_open sqlite3_api->open
#define sqlite3_open16 sqlite3_api->open16
#define sqlite3_prepare sqlite3_api->prepare
#define sqlite3_prepare16 sqlite3_api->prepare16
#define sqlite3_prepare_v2 sqlite3_api->prepare_v2
#define sqlite3_prepare16_v2 sqlite3_api->prepare16_v2
#define sqlite3_profile sqlite3_api->profile
#define sqlite3_progress_handler sqlite3_api->progress_handler
#define sqlite3_realloc sqlite3_api->realloc
#define sqlite3_reset sqlite3_api->reset
#define sqlite3_result_blob sqlite3_api->result_blob
#define sqlite3_result_double sqlite3_api->result_double
#define sqlite3_result_error sqlite3_api->result_error
#define sqlite3_result_error16 sqlite3_api->result_error16
#define sqlite3_result_int sqlite3_api->result_int
#define sqlite3_result_int64 sqlite3_api->result_int64
#define sqlite3_result_null sqlite3_api->result_null
#define sqlite3_result_text sqlite3_api->result_text
#define sqlite3_result_text16 sqlite3_api->result_text16
#define sqlite3_result_text16be sqlite3_api->result_text16be
#define sqlite3_result_text16le sqlite3_api->result_text16le
#define sqlite3_result_value sqlite3_api->result_value
#define sqlite3_rollback_hook sqlite3_api->rollback_hook
#define sqlite3_set_authorizer sqlite3_api->set_authorizer
#define sqlite3_set_auxdata sqlite3_api->set_auxdata
#define sqlite3_snprintf sqlite3_api->xsnprintf
#define sqlite3_step sqlite3_api->step
#define sqlite3_table_column_metadata sqlite3_api->table_column_metadata
#define sqlite3_thread_cleanup sqlite3_api->thread_cleanup
#define sqlite3_total_changes sqlite3_api->total_changes
#define sqlite3_trace sqlite3_api->trace
#ifndef SQLITE_OMIT_DEPRECATED
#define sqlite3_transfer_bindings sqlite3_api->transfer_bindings
#endif
#define sqlite3_update_hook sqlite3_api->update_hook
#define sqlite3_user_data sqlite3_api->user_data
#define sqlite3_value_blob sqlite3_api->value_blob
#define sqlite3_value_bytes sqlite3_api->value_bytes
#define sqlite3_value_bytes16 sqlite3_api->value_bytes16
#define sqlite3_value_double sqlite3_api->value_double
#define sqlite3_value_int sqlite3_api->value_int
#define sqlite3_value_int64 sqlite3_api->value_int64
#define sqlite3_value_numeric_type sqlite3_api->value_numeric_type
#define sqlite3_value_text sqlite3_api->value_text
#define sqlite3_value_text16 sqlite3_api->value_text16
#define sqlite3_value_text16be sqlite3_api->value_text16be
#define sqlite3_value_text16le sqlite3_api->value_text16le
#define sqlite3_value_type sqlite3_api->value_type
#define sqlite3_vmprintf sqlite3_api->vmprintf
#define sqlite3_vsnprintf sqlite3_api->xvsnprintf
#define sqlite3_overload_function sqlite3_api->overload_function
#define sqlite3_prepare_v2 sqlite3_api->prepare_v2
#define sqlite3_prepare16_v2 sqlite3_api->prepare16_v2
#define sqlite3_clear_bindings sqlite3_api->clear_bindings
#define sqlite3_bind_zeroblob sqlite3_api->bind_zeroblob
#define sqlite3_blob_bytes sqlite3_api->blob_bytes
#define sqlite3_blob_close sqlite3_api->blob_close
#define sqlite3_blob_open sqlite3_api->blob_open
#define sqlite3_blob_read sqlite3_api->blob_read
#define sqlite3_blob_write sqlite3_api->blob_write
#define sqlite3_create_collation_v2 sqlite3_api->create_collation_v2
#define sqlite3_file_control sqlite3_api->file_control
#define sqlite3_memory_highwater sqlite3_api->memory_highwater
#define sqlite3_memory_used sqlite3_api->memory_used
#define sqlite3_mutex_alloc sqlite3_api->mutex_alloc
#define sqlite3_mutex_enter sqlite3_api->mutex_enter
#define sqlite3_mutex_free sqlite3_api->mutex_free
#define sqlite3_mutex_leave sqlite3_api->mutex_leave
#define sqlite3_mutex_try sqlite3_api->mutex_try
#define sqlite3_open_v2 sqlite3_api->open_v2
#define sqlite3_release_memory sqlite3_api->release_memory
#define sqlite3_result_error_nomem sqlite3_api->result_error_nomem
#define sqlite3_result_error_toobig sqlite3_api->result_error_toobig
#define sqlite3_sleep sqlite3_api->sleep
#define sqlite3_soft_heap_limit sqlite3_api->soft_heap_limit
#define sqlite3_vfs_find sqlite3_api->vfs_find
#define sqlite3_vfs_register sqlite3_api->vfs_register
#define sqlite3_vfs_unregister sqlite3_api->vfs_unregister
#define sqlite3_threadsafe sqlite3_api->xthreadsafe
#define sqlite3_result_zeroblob sqlite3_api->result_zeroblob
#define sqlite3_result_error_code sqlite3_api->result_error_code
#define sqlite3_test_control sqlite3_api->test_control
#define sqlite3_randomness sqlite3_api->randomness
#define sqlite3_context_db_handle sqlite3_api->context_db_handle
#define sqlite3_extended_result_codes sqlite3_api->extended_result_codes
#define sqlite3_limit sqlite3_api->limit
#define sqlite3_next_stmt sqlite3_api->next_stmt
#define sqlite3_sql sqlite3_api->sql
#define sqlite3_status sqlite3_api->status
#define sqlite3_backup_finish sqlite3_api->backup_finish
#define sqlite3_backup_init sqlite3_api->backup_init
#define sqlite3_backup_pagecount sqlite3_api->backup_pagecount
#define sqlite3_backup_remaining sqlite3_api->backup_remaining
#define sqlite3_backup_step sqlite3_api->backup_step
#define sqlite3_compileoption_get sqlite3_api->compileoption_get
#define sqlite3_compileoption_used sqlite3_api->compileoption_used
#define sqlite3_create_function_v2 sqlite3_api->create_function_v2
#define sqlite3_db_config sqlite3_api->db_config
#define sqlite3_db_mutex sqlite3_api->db_mutex
#define sqlite3_db_status sqlite3_api->db_status
#define sqlite3_extended_errcode sqlite3_api->extended_errcode
#define sqlite3_log sqlite3_api->log
#define sqlite3_soft_heap_limit64 sqlite3_api->soft_heap_limit64
#define sqlite3_sourceid sqlite3_api->sourceid
#define sqlite3_stmt_status sqlite3_api->stmt_status
#define sqlite3_strnicmp sqlite3_api->strnicmp
#define sqlite3_unlock_notify sqlite3_api->unlock_notify
#define sqlite3_wal_autocheckpoint sqlite3_api->wal_autocheckpoint
#define sqlite3_wal_checkpoint sqlite3_api->wal_checkpoint
#define sqlite3_wal_hook sqlite3_api->wal_hook
#define sqlite3_blob_reopen sqlite3_api->blob_reopen
#define sqlite3_vtab_config sqlite3_api->vtab_config
#define sqlite3_vtab_on_conflict sqlite3_api->vtab_on_conflict
/* Version 3.7.16 and later */
#define sqlite3_close_v2 sqlite3_api->close_v2
#define sqlite3_db_filename sqlite3_api->db_filename
#define sqlite3_db_readonly sqlite3_api->db_readonly
#define sqlite3_db_release_memory sqlite3_api->db_release_memory
#define sqlite3_errstr sqlite3_api->errstr
#define sqlite3_stmt_busy sqlite3_api->stmt_busy
#define sqlite3_stmt_readonly sqlite3_api->stmt_readonly
#define sqlite3_stricmp sqlite3_api->stricmp
#define sqlite3_uri_boolean sqlite3_api->uri_boolean
#define sqlite3_uri_int64 sqlite3_api->uri_int64
#define sqlite3_uri_parameter sqlite3_api->uri_parameter
#define sqlite3_uri_vsnprintf sqlite3_api->xvsnprintf
#define sqlite3_wal_checkpoint_v2 sqlite3_api->wal_checkpoint_v2
/* Version 3.8.7 and later */
#define sqlite3_auto_extension sqlite3_api->auto_extension
#define sqlite3_bind_blob64 sqlite3_api->bind_blob64
#define sqlite3_bind_text64 sqlite3_api->bind_text64
#define sqlite3_cancel_auto_extension sqlite3_api->cancel_auto_extension
#define sqlite3_load_extension sqlite3_api->load_extension
#define sqlite3_malloc64 sqlite3_api->malloc64
#define sqlite3_msize sqlite3_api->msize
#define sqlite3_realloc64 sqlite3_api->realloc64
#define sqlite3_reset_auto_extension sqlite3_api->reset_auto_extension
#define sqlite3_result_blob64 sqlite3_api->result_blob64
#define sqlite3_result_text64 sqlite3_api->result_text64
#define sqlite3_strglob sqlite3_api->strglob
/* Version 3.8.11 and later */
#define sqlite3_value_dup sqlite3_api->value_dup
#define sqlite3_value_free sqlite3_api->value_free
#define sqlite3_result_zeroblob64 sqlite3_api->result_zeroblob64
#define sqlite3_bind_zeroblob64 sqlite3_api->bind_zeroblob64
/* Version 3.9.0 and later */
#define sqlite3_value_subtype sqlite3_api->value_subtype
#define sqlite3_result_subtype sqlite3_api->result_subtype
/* Version 3.10.0 and later */
#define sqlite3_status64 sqlite3_api->status64
#define sqlite3_strlike sqlite3_api->strlike
#define sqlite3_db_cacheflush sqlite3_api->db_cacheflush
/* Version 3.12.0 and later */
#define sqlite3_system_errno sqlite3_api->system_errno
/* Version 3.14.0 and later */
#define sqlite3_trace_v2 sqlite3_api->trace_v2
#define sqlite3_expanded_sql sqlite3_api->expanded_sql
/* Version 3.18.0 and later */
#define sqlite3_set_last_insert_rowid sqlite3_api->set_last_insert_rowid
/* Version 3.20.0 and later */
#define sqlite3_prepare_v3 sqlite3_api->prepare_v3
#define sqlite3_prepare16_v3 sqlite3_api->prepare16_v3
#define sqlite3_bind_pointer sqlite3_api->bind_pointer
#define sqlite3_result_pointer sqlite3_api->result_pointer
#define sqlite3_value_pointer sqlite3_api->value_pointer
/* Version 3.22.0 and later */
#define sqlite3_vtab_nochange sqlite3_api->vtab_nochange
#define sqlite3_value_nochange sqlite3_api->value_nochange
#define sqlite3_vtab_collation sqlite3_api->vtab_collation
/* Version 3.24.0 and later */
#define sqlite3_keyword_count sqlite3_api->keyword_count
#define sqlite3_keyword_name sqlite3_api->keyword_name
#define sqlite3_keyword_check sqlite3_api->keyword_check
#define sqlite3_str_new sqlite3_api->str_new
#define sqlite3_str_finish sqlite3_api->str_finish
#define sqlite3_str_appendf sqlite3_api->str_appendf
#define sqlite3_str_vappendf sqlite3_api->str_vappendf
#define sqlite3_str_append sqlite3_api->str_append
#define sqlite3_str_appendall sqlite3_api->str_appendall
#define sqlite3_str_appendchar sqlite3_api->str_appendchar
#define sqlite3_str_reset sqlite3_api->str_reset
#define sqlite3_str_errcode sqlite3_api->str_errcode
#define sqlite3_str_length sqlite3_api->str_length
#define sqlite3_str_value sqlite3_api->str_value
/* Version 3.25.0 and later */
#define sqlite3_create_window_function sqlite3_api->create_window_function
/* Version 3.26.0 and later */
#define sqlite3_normalized_sql sqlite3_api->normalized_sql
/* Version 3.28.0 and later */
#define sqlite3_stmt_isexplain sqlite3_api->stmt_isexplain
#define sqlite3_value_frombind sqlite3_api->value_frombind
/* Version 3.30.0 and later */
#define sqlite3_drop_modules sqlite3_api->drop_modules
/* Version 3.31.0 and later */
#define sqlite3_hard_heap_limit64 sqlite3_api->hard_heap_limit64
#define sqlite3_uri_key sqlite3_api->uri_key
#define sqlite3_filename_database sqlite3_api->filename_database
#define sqlite3_filename_journal sqlite3_api->filename_journal
#define sqlite3_filename_wal sqlite3_api->filename_wal
/* Version 3.32.0 and later */
#define sqlite3_create_filename sqlite3_api->create_filename
#define sqlite3_free_filename sqlite3_api->free_filename
#define sqlite3_database_file_object sqlite3_api->database_file_object
/* Version 3.34.0 and later */
#define sqlite3_txn_state sqlite3_api->txn_state
/* Version 3.36.1 and later */
#define sqlite3_changes64 sqlite3_api->changes64
#define sqlite3_total_changes64 sqlite3_api->total_changes64
/* Version 3.37.0 and later */
#define sqlite3_autovacuum_pages sqlite3_api->autovacuum_pages
/* Version 3.38.0 and later */
#define sqlite3_error_offset sqlite3_api->error_offset
#define sqlite3_vtab_rhs_value sqlite3_api->vtab_rhs_value
#define sqlite3_vtab_distinct sqlite3_api->vtab_distinct
#define sqlite3_vtab_in sqlite3_api->vtab_in
#define sqlite3_vtab_in_first sqlite3_api->vtab_in_first
#define sqlite3_vtab_in_next sqlite3_api->vtab_in_next
/* Version 3.39.0 and later */
#ifndef SQLITE_OMIT_DESERIALIZE
#define sqlite3_deserialize sqlite3_api->deserialize
#define sqlite3_serialize sqlite3_api->serialize
#endif
#define sqlite3_db_name sqlite3_api->db_name
/* Version 3.40.0 and later */
#define sqlite3_value_encoding sqlite3_api->value_encoding
/* Version 3.41.0 and later */
#define sqlite3_is_interrupted sqlite3_api->is_interrupted
/* Version 3.43.0 and later */
#define sqlite3_stmt_explain sqlite3_api->stmt_explain
/* Version 3.44.0 and later */
#define sqlite3_get_clientdata sqlite3_api->get_clientdata
#define sqlite3_set_clientdata sqlite3_api->set_clientdata
/* Version 3.50.0 and later */
#define sqlite3_setlk_timeout sqlite3_api->setlk_timeout
/* Version 3.51.0 and later */
#define sqlite3_set_errmsg sqlite3_api->set_errmsg
#define sqlite3_db_status64 sqlite3_api->db_status64
#endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)
/* This case when the file really is being compiled as a loadable
** extension */
# define SQLITE_EXTENSION_INIT1 const sqlite3_api_routines *sqlite3_api=0;
# define SQLITE_EXTENSION_INIT2(v) sqlite3_api=v;
# define SQLITE_EXTENSION_INIT3 \
extern const sqlite3_api_routines *sqlite3_api;
#else
/* This case when the file is being statically linked into the
** application */
# define SQLITE_EXTENSION_INIT1 /*no-op*/
# define SQLITE_EXTENSION_INIT2(v) (void)v; /* unused parameter */
# define SQLITE_EXTENSION_INIT3 /*no-op*/
#endif
#endif /* SQLITE3EXT_H */

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env bash
#
# Updates the vendored spellfix1 source files to match the SQLite version
# bundled with the current go-sqlite3 dependency.
#
set -euo pipefail
cd "$(dirname "$0")"
SQLITE_VERSION=$(grep '#define SQLITE_VERSION ' \
"$(go env GOMODCACHE)/$(go list -m -f '{{.Path}}@{{.Version}}' github.com/mattn/go-sqlite3)/sqlite3-binding.h" \
| awk '{gsub(/"/, "", $3); print $3}')
if [ -z "$SQLITE_VERSION" ]; then
echo "ERROR: Could not determine SQLite version from go-sqlite3" >&2
exit 1
fi
TAG="version-${SQLITE_VERSION}"
BASE_URL="https://raw.githubusercontent.com/sqlite/sqlite/${TAG}"
echo "SQLite version from go-sqlite3: ${SQLITE_VERSION}"
echo "Downloading from tag: ${TAG}"
curl -sfL "${BASE_URL}/ext/misc/spellfix.c" -o spellfix.c
echo " Updated spellfix.c"
curl -sfL "${BASE_URL}/src/sqlite3ext.h" -o sqlite3ext.h
echo " Updated sqlite3ext.h"
echo "Done."

9
go.mod
View File

@@ -1,13 +1,13 @@
module github.com/navidrome/navidrome
go 1.25
go 1.26
replace (
// Fork to fix https://github.com/navidrome/navidrome/issues/3254
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 => github.com/deluan/tag v0.0.0-20241002021117-dfe5e6ea396d
// Fork to implement raw tags support
go.senan.xyz/taglib => github.com/deluan/go-taglib v0.0.0-20260221220301-2fab4903f48e
go.senan.xyz/taglib => github.com/deluan/go-taglib v0.0.0-20260225021432-1699562530f1
)
require (
@@ -53,7 +53,7 @@ require (
github.com/onsi/gomega v1.39.1
github.com/pelletier/go-toml/v2 v2.2.4
github.com/pocketbase/dbx v1.12.0
github.com/pressly/goose/v3 v3.26.0
github.com/pressly/goose/v3 v3.27.0
github.com/prometheus/client_golang v1.23.2
github.com/rjeczalik/notify v0.9.3
github.com/robfig/cron/v3 v3.0.1
@@ -88,7 +88,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/creack/pty v1.1.24 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
@@ -140,7 +140,6 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect
golang.org/x/tools v0.42.0 // indirect

36
go.sum
View File

@@ -1,7 +1,7 @@
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
@@ -34,10 +34,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/deluan/go-taglib v0.0.0-20260221220301-2fab4903f48e h1:yQF3eOcI2dMMtxqdKXm3cgfYZlDcq9SUDDv90bsMj2I=
github.com/deluan/go-taglib v0.0.0-20260221220301-2fab4903f48e/go.mod h1:sKDN0U4qXDlq6LFK+aOAkDH4Me5nDV1V/A4B+B69xBA=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/deluan/go-taglib v0.0.0-20260225021432-1699562530f1 h1:seWJmkPAb+M1ysRNGzTGS7FfdrUe9wQTHhB9p2fxDWg=
github.com/deluan/go-taglib v0.0.0-20260225021432-1699562530f1/go.mod h1:sKDN0U4qXDlq6LFK+aOAkDH4Me5nDV1V/A4B+B69xBA=
github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf h1:tb246l2Zmpt/GpF9EcHCKTtwzrd0HGfEmoODFA/qnk4=
github.com/deluan/rest v0.0.0-20211102003136-6260bc399cbf/go.mod h1:tSgDythFsl0QgS/PFWfIZqcJKnkADWneY80jaVRlqK8=
github.com/deluan/sanitize v0.0.0-20241120162836-fdfd8fdfaa55 h1:wSCnggTs2f2ji6nFwQmfwgINcmSMj0xF0oHnoyRSPe4=
@@ -143,8 +143,8 @@ github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2Og
github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -193,8 +193,8 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/ogier/pflag v0.0.1 h1:RW6JSWSu/RkSatfcLtogGfFgpim5p7ARQ10ECk5O750=
github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g=
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
@@ -212,8 +212,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pocketbase/dbx v1.12.0 h1:/oLErM+A0b4xI0PWTGPqSDVjzix48PqI/bng2l0PzoA=
github.com/pocketbase/dbx v1.12.0/go.mod h1:xXRCIAKTHMgUCyCKZm55pUOdvFziJjQfXaWKhu2vhMs=
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
github.com/pressly/goose/v3 v3.27.0 h1:/D30gVTuQhu0WsNZYbJi4DMOsx1lNq+6SkLe+Wp59BM=
github.com/pressly/goose/v3 v3.27.0/go.mod h1:3ZBeCXqzkgIRvrEMDkYh1guvtoJTU5oMMuDdkutoM78=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
@@ -321,8 +321,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
@@ -423,11 +423,11 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=

View File

@@ -15,7 +15,6 @@ func main() {
// To avoid these kind of errors, you should use `make build` to compile the project.
_ = buildtags.NETGO
_ = buildtags.SQLITE_FTS5
_ = buildtags.SQLITE_SPELLFIX
cmd.Execute()
}

View File

@@ -1,6 +1,6 @@
module github.com/navidrome/navidrome/plugins/cmd/ndpgen
go 1.25
go 1.26
require (
github.com/extism/go-pdk v1.1.3

View File

@@ -14,6 +14,7 @@ import (
"net/url"
"strings"
"github.com/navidrome/navidrome/plugins/pdk/go/host"
"github.com/navidrome/navidrome/plugins/pdk/go/metadata"
"github.com/navidrome/navidrome/plugins/pdk/go/pdk"
)
@@ -77,21 +78,28 @@ func sparqlQuery(endpoint, query string) (*SPARQLResult, error) {
form := url.Values{}
form.Set("query", query)
req := pdk.NewHTTPRequest(pdk.MethodPost, endpoint)
req.SetHeader("Accept", "application/sparql-results+json")
req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
req.SetHeader("User-Agent", "NavidromeWikimediaPlugin/1.0")
req.SetBody([]byte(form.Encode()))
pdk.Log(pdk.LogDebug, fmt.Sprintf("SPARQL query to %s: %s", endpoint, query))
resp := req.Send()
if resp.Status() != 200 {
return nil, fmt.Errorf("SPARQL HTTP error: status %d", resp.Status())
resp, err := host.HTTPSend(host.HTTPRequest{
Method: "POST",
URL: endpoint,
Headers: map[string]string{
"Accept": "application/sparql-results+json",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "NavidromeWikimediaPlugin/1.0",
},
Body: []byte(form.Encode()),
TimeoutMs: 10000,
})
if err != nil {
return nil, fmt.Errorf("SPARQL HTTP error: %w", err)
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("SPARQL HTTP error: status %d", resp.StatusCode)
}
var result SPARQLResult
if err := json.Unmarshal(resp.Body(), &result); err != nil {
if err := json.Unmarshal(resp.Body, &result); err != nil {
return nil, fmt.Errorf("failed to parse SPARQL response: %w", err)
}
if len(result.Results.Bindings) == 0 {
@@ -104,15 +112,22 @@ func sparqlQuery(endpoint, query string) (*SPARQLResult, error) {
func mediawikiQuery(params url.Values) ([]byte, error) {
apiURL := fmt.Sprintf("%s?%s", mediawikiAPIEndpoint, params.Encode())
req := pdk.NewHTTPRequest(pdk.MethodGet, apiURL)
req.SetHeader("Accept", "application/json")
req.SetHeader("User-Agent", "NavidromeWikimediaPlugin/1.0")
resp := req.Send()
if resp.Status() != 200 {
return nil, fmt.Errorf("MediaWiki HTTP error: status %d", resp.Status())
resp, err := host.HTTPSend(host.HTTPRequest{
Method: "GET",
URL: apiURL,
Headers: map[string]string{
"Accept": "application/json",
"User-Agent": "NavidromeWikimediaPlugin/1.0",
},
TimeoutMs: 10000,
})
if err != nil {
return nil, fmt.Errorf("MediaWiki HTTP error: %w", err)
}
return resp.Body(), nil
if resp.StatusCode != 200 {
return nil, fmt.Errorf("MediaWiki HTTP error: status %d", resp.StatusCode)
}
return resp.Body, nil
}
// getWikidataWikipediaURL fetches the Wikipedia URL from Wikidata using MBID or name

View File

@@ -0,0 +1,40 @@
package host
import "context"
// HTTPRequest represents an outbound HTTP request from a plugin.
type HTTPRequest struct {
Method string `json:"method"`
URL string `json:"url"`
Headers map[string]string `json:"headers,omitempty"`
Body []byte `json:"body,omitempty"`
TimeoutMs int32 `json:"timeoutMs,omitempty"`
}
// HTTPResponse represents the response from an outbound HTTP request.
type HTTPResponse struct {
StatusCode int32 `json:"statusCode"`
Headers map[string]string `json:"headers,omitempty"`
Body []byte `json:"body,omitempty"`
}
// HTTPService provides outbound HTTP request capabilities for plugins.
//
// This service allows plugins to make HTTP requests to external services.
// Requests are validated against the plugin's declared requiredHosts patterns
// from the http permission in the manifest. Redirects are followed but each
// redirect destination is also validated against the allowed hosts.
//
//nd:hostservice name=HTTP permission=http
type HTTPService interface {
// Send executes an HTTP request and returns the response.
//
// Parameters:
// - request: The HTTP request to execute, including method, URL, headers, body, and timeout
//
// Returns the HTTP response with status code, headers, and body.
// Network errors, timeouts, and permission failures are returned as Go errors.
// Successful HTTP calls (including 4xx/5xx status codes) return a non-nil response with nil error.
//nd:hostfunc
Send(ctx context.Context, request HTTPRequest) (*HTTPResponse, error)
}

View File

@@ -0,0 +1,88 @@
// Code generated by ndpgen. DO NOT EDIT.
package host
import (
"context"
"encoding/json"
extism "github.com/extism/go-sdk"
)
// HTTPSendRequest is the request type for HTTP.Send.
type HTTPSendRequest struct {
Request HTTPRequest `json:"request"`
}
// HTTPSendResponse is the response type for HTTP.Send.
type HTTPSendResponse struct {
Result *HTTPResponse `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// RegisterHTTPHostFunctions registers HTTP service host functions.
// The returned host functions should be added to the plugin's configuration.
func RegisterHTTPHostFunctions(service HTTPService) []extism.HostFunction {
return []extism.HostFunction{
newHTTPSendHostFunction(service),
}
}
func newHTTPSendHostFunction(service HTTPService) extism.HostFunction {
return extism.NewHostFunctionWithStack(
"http_send",
func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {
// Read JSON request from plugin memory
reqBytes, err := p.ReadBytes(stack[0])
if err != nil {
httpWriteError(p, stack, err)
return
}
var req HTTPSendRequest
if err := json.Unmarshal(reqBytes, &req); err != nil {
httpWriteError(p, stack, err)
return
}
// Call the service method
result, svcErr := service.Send(ctx, req.Request)
if svcErr != nil {
httpWriteError(p, stack, svcErr)
return
}
// Write JSON response to plugin memory
resp := HTTPSendResponse{
Result: result,
}
httpWriteResponse(p, stack, resp)
},
[]extism.ValueType{extism.ValueTypePTR},
[]extism.ValueType{extism.ValueTypePTR},
)
}
// httpWriteResponse writes a JSON response to plugin memory.
func httpWriteResponse(p *extism.CurrentPlugin, stack []uint64, resp any) {
respBytes, err := json.Marshal(resp)
if err != nil {
httpWriteError(p, stack, err)
return
}
respPtr, err := p.WriteBytes(respBytes)
if err != nil {
stack[0] = 0
return
}
stack[0] = respPtr
}
// httpWriteError writes an error response to plugin memory.
func httpWriteError(p *extism.CurrentPlugin, stack []uint64, err error) {
errResp := struct {
Error string `json:"error"`
}{Error: err.Error()}
respBytes, _ := json.Marshal(errResp)
respPtr, _ := p.WriteBytes(respBytes)
stack[0] = respPtr
}

190
plugins/host_httpclient.go Normal file
View File

@@ -0,0 +1,190 @@
package plugins
import (
"bytes"
"cmp"
"context"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/plugins/host"
)
const (
httpClientDefaultTimeout = 10 * time.Second
httpClientMaxRedirects = 5
httpClientMaxResponseBodyLen = 10 * 1024 * 1024 // 10 MB
)
// httpServiceImpl implements host.HTTPService.
type httpServiceImpl struct {
pluginName string
requiredHosts []string
client *http.Client
}
// newHTTPService creates a new HTTPService for a plugin.
func newHTTPService(pluginName string, permission *HTTPPermission) *httpServiceImpl {
var requiredHosts []string
if permission != nil {
requiredHosts = permission.RequiredHosts
}
svc := &httpServiceImpl{
pluginName: pluginName,
requiredHosts: requiredHosts,
}
svc.client = &http.Client{
Transport: http.DefaultTransport,
// Timeout is set per-request via context deadline, not here.
// CheckRedirect validates hosts and enforces redirect limits.
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= httpClientMaxRedirects {
log.Warn(req.Context(), "HTTP redirect limit exceeded", "plugin", svc.pluginName, "url", req.URL.String(), "redirectCount", len(via))
return http.ErrUseLastResponse
}
if err := svc.validateHost(req.Context(), req.URL.Host); err != nil {
log.Warn(req.Context(), "HTTP redirect blocked", "plugin", svc.pluginName, "url", req.URL.String(), "err", err)
return err
}
return nil
},
}
return svc
}
func (s *httpServiceImpl) Send(ctx context.Context, request host.HTTPRequest) (*host.HTTPResponse, error) {
// Parse and validate URL
parsedURL, err := url.Parse(request.URL)
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
// Validate URL scheme
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
return nil, fmt.Errorf("invalid URL scheme %q: must be http or https", parsedURL.Scheme)
}
// Validate host against allowed hosts and private IP restrictions
if err := s.validateHost(ctx, parsedURL.Host); err != nil {
return nil, err
}
// Apply per-request timeout via context deadline
timeout := cmp.Or(time.Duration(request.TimeoutMs)*time.Millisecond, httpClientDefaultTimeout)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// Build request body
method := strings.ToUpper(request.Method)
var body io.Reader
if len(request.Body) > 0 {
body = bytes.NewReader(request.Body)
}
// Create HTTP request
httpReq, err := http.NewRequestWithContext(ctx, method, request.URL, body)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
for k, v := range request.Headers {
httpReq.Header.Set(k, v)
}
// Execute request
resp, err := s.client.Do(httpReq) //nolint:gosec // URL is validated against requiredHosts
if err != nil {
return nil, err
}
defer resp.Body.Close()
log.Trace(ctx, "HTTP request", "plugin", s.pluginName, "method", method, "url", request.URL, "status", resp.StatusCode)
// Read response body (with size limit to prevent memory exhaustion)
respBody, err := io.ReadAll(io.LimitReader(resp.Body, httpClientMaxResponseBodyLen))
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}
// Flatten response headers (first value only)
headers := make(map[string]string, len(resp.Header))
for k, v := range resp.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
return &host.HTTPResponse{
StatusCode: int32(resp.StatusCode),
Headers: headers,
Body: respBody,
}, nil
}
// validateHost checks whether a request to the given host is permitted.
// When requiredHosts is set, it checks against the allowlist.
// When requiredHosts is empty, it blocks private/loopback IPs to prevent SSRF.
func (s *httpServiceImpl) validateHost(ctx context.Context, hostStr string) error {
hostname := extractHostname(hostStr)
if len(s.requiredHosts) > 0 {
if !s.isHostAllowed(hostname) {
return fmt.Errorf("host %q is not allowed", hostStr)
}
return nil
}
// No explicit allowlist: block private/loopback IPs
if isPrivateOrLoopback(hostname) {
log.Warn(ctx, "HTTP request to private/loopback address blocked", "plugin", s.pluginName, "host", hostStr)
return fmt.Errorf("host %q is not allowed: private/loopback addresses require explicit requiredHosts in manifest", hostStr)
}
return nil
}
func (s *httpServiceImpl) isHostAllowed(hostname string) bool {
for _, pattern := range s.requiredHosts {
if matchHostPattern(pattern, hostname) {
return true
}
}
return false
}
// extractHostname returns the hostname portion of a host string, stripping
// any port number and IPv6 brackets. It handles IPv6 addresses correctly
// (e.g. "[::1]:8080" → "::1", "[::1]" → "::1").
func extractHostname(hostStr string) string {
if h, _, err := net.SplitHostPort(hostStr); err == nil {
return h
}
// Strip IPv6 brackets when no port is present (e.g. "[::1]" → "::1")
if strings.HasPrefix(hostStr, "[") && strings.HasSuffix(hostStr, "]") {
return hostStr[1 : len(hostStr)-1]
}
return hostStr
}
// isPrivateOrLoopback returns true if the given hostname resolves to or is
// a private, loopback, or link-local IP address. This includes:
// IPv4: 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16
// IPv6: ::1, fc00::/7, fe80::/10
// It also blocks "localhost" by name.
func isPrivateOrLoopback(hostname string) bool {
if strings.EqualFold(hostname, "localhost") {
return true
}
ip := net.ParseIP(hostname)
if ip == nil {
return false
}
return ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()
}
// Verify interface implementation
var _ host.HTTPService = (*httpServiceImpl)(nil)

View File

@@ -0,0 +1,565 @@
//go:build !windows
package plugins
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"time"
"github.com/navidrome/navidrome/plugins/host"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("httpServiceImpl", func() {
var (
svc *httpServiceImpl
ts *httptest.Server
)
AfterEach(func() {
if ts != nil {
ts.Close()
}
})
Context("without host restrictions (default SSRF protection)", func() {
BeforeEach(func() {
svc = newHTTPService("test-plugin", nil)
})
It("should block requests to loopback IPs", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: ts.URL,
TimeoutMs: 1000,
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("private/loopback"))
})
It("should block requests to localhost by name", func() {
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: "http://localhost:12345/test",
TimeoutMs: 1000,
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("private/loopback"))
})
It("should block requests to private IPs (10.x)", func() {
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: "http://10.0.0.1/test",
TimeoutMs: 1000,
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("private/loopback"))
})
It("should block requests to private IPs (192.168.x)", func() {
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: "http://192.168.1.1/test",
TimeoutMs: 1000,
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("private/loopback"))
})
It("should block requests to private IPs (172.16.x)", func() {
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: "http://172.16.0.1/test",
TimeoutMs: 1000,
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("private/loopback"))
})
It("should block requests to link-local IPs (169.254.x)", func() {
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: "http://169.254.169.254/latest/meta-data/",
TimeoutMs: 1000,
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("private/loopback"))
})
It("should block requests to IPv6 loopback with port", func() {
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: "http://[::1]:8080/test",
TimeoutMs: 1000,
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("private/loopback"))
})
It("should block requests to IPv6 loopback without port", func() {
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: "http://[::1]/test",
TimeoutMs: 1000,
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("private/loopback"))
})
It("should allow requests to public hostnames", func() {
// This will fail at the network level (connection refused or DNS),
// but it should NOT fail with a "private/loopback" error
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: "http://203.0.113.1:1/test", // TEST-NET-3, non-routable but not private
TimeoutMs: 100,
})
// Should get a network error, not a permission error
if err != nil {
Expect(err.Error()).ToNot(ContainSubstring("private/loopback"))
}
})
It("should return error for invalid URL", func() {
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: "://bad-url",
})
Expect(err).To(HaveOccurred())
})
It("should reject non-http/https URL schemes", func() {
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: "ftp://example.com/file",
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("must be http or https"))
})
})
Context("with explicit requiredHosts allowing loopback", func() {
BeforeEach(func() {
svc = newHTTPService("test-plugin", &HTTPPermission{
RequiredHosts: []string{"127.0.0.1"},
})
})
It("should handle GET requests", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expect(r.Method).To(Equal("GET"))
w.Header().Set("X-Test", "ok")
w.WriteHeader(201)
_, _ = w.Write([]byte("hello"))
}))
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: ts.URL,
Headers: map[string]string{"Accept": "text/plain"},
TimeoutMs: 1000,
})
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(Equal(int32(201)))
Expect(string(resp.Body)).To(Equal("hello"))
Expect(resp.Headers["X-Test"]).To(Equal("ok"))
})
It("should handle POST requests with body", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expect(r.Method).To(Equal("POST"))
b, _ := io.ReadAll(r.Body)
_, _ = w.Write([]byte("got:" + string(b)))
}))
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "POST",
URL: ts.URL,
Body: []byte("abc"),
TimeoutMs: 1000,
})
Expect(err).ToNot(HaveOccurred())
Expect(string(resp.Body)).To(Equal("got:abc"))
})
It("should handle PUT requests with body", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expect(r.Method).To(Equal("PUT"))
b, _ := io.ReadAll(r.Body)
_, _ = w.Write([]byte("put:" + string(b)))
}))
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "PUT",
URL: ts.URL,
Body: []byte("xyz"),
TimeoutMs: 1000,
})
Expect(err).ToNot(HaveOccurred())
Expect(string(resp.Body)).To(Equal("put:xyz"))
})
It("should handle DELETE requests", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expect(r.Method).To(Equal("DELETE"))
w.WriteHeader(204)
}))
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "DELETE",
URL: ts.URL,
TimeoutMs: 1000,
})
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(Equal(int32(204)))
})
It("should handle DELETE requests with body", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expect(r.Method).To(Equal("DELETE"))
b, _ := io.ReadAll(r.Body)
_, _ = w.Write([]byte("del:" + string(b)))
}))
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "DELETE",
URL: ts.URL,
Body: []byte(`{"id":"123"}`),
TimeoutMs: 1000,
})
Expect(err).ToNot(HaveOccurred())
Expect(string(resp.Body)).To(Equal(`del:{"id":"123"}`))
})
It("should handle PATCH requests with body", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expect(r.Method).To(Equal("PATCH"))
b, _ := io.ReadAll(r.Body)
_, _ = w.Write([]byte("patch:" + string(b)))
}))
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "PATCH",
URL: ts.URL,
Body: []byte("data"),
TimeoutMs: 1000,
})
Expect(err).ToNot(HaveOccurred())
Expect(string(resp.Body)).To(Equal("patch:data"))
})
It("should handle HEAD requests", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expect(r.Method).To(Equal("HEAD"))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
}))
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "HEAD",
URL: ts.URL,
TimeoutMs: 1000,
})
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(Equal(int32(200)))
Expect(resp.Headers["Content-Type"]).To(Equal("application/json"))
Expect(resp.Body).To(BeEmpty())
})
It("should use default timeout when TimeoutMs is 0", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: ts.URL,
})
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(Equal(int32(200)))
})
It("should return error on timeout", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(50 * time.Millisecond)
}))
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: ts.URL,
TimeoutMs: 1,
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("deadline exceeded"))
})
It("should return error on context cancellation", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(50 * time.Millisecond)
}))
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(1 * time.Millisecond)
cancel()
}()
_, err := svc.Send(ctx, host.HTTPRequest{
Method: "GET",
URL: ts.URL,
TimeoutMs: 5000,
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("context canceled"))
})
It("should send request headers", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(r.Header.Get("X-Custom")))
}))
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: ts.URL,
Headers: map[string]string{"X-Custom": "myvalue"},
TimeoutMs: 1000,
})
Expect(err).ToNot(HaveOccurred())
Expect(string(resp.Body)).To(Equal("myvalue"))
})
})
Context("with host restrictions", func() {
BeforeEach(func() {
svc = newHTTPService("test-plugin", &HTTPPermission{
RequiredHosts: []string{"allowed.example.com", "*.allowed.org"},
})
})
It("should block requests to non-allowed hosts", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
// httptest server is on 127.0.0.1 which is not in requiredHosts
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: ts.URL,
TimeoutMs: 1000,
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not allowed"))
})
It("should follow redirects to allowed hosts", func() {
// Create a destination server
dest := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("final"))
}))
defer dest.Close()
// Create a redirect server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, dest.URL, http.StatusFound)
}))
// Allow both servers (both on 127.0.0.1)
svc.requiredHosts = []string{"127.0.0.1"}
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: ts.URL,
TimeoutMs: 1000,
})
Expect(err).ToNot(HaveOccurred())
Expect(resp.StatusCode).To(Equal(int32(200)))
Expect(string(resp.Body)).To(Equal("final"))
})
It("should block redirects to non-allowed hosts", func() {
// Server that redirects to a disallowed host
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "http://evil.example.com/steal", http.StatusFound)
}))
// Override requiredHosts to allow the test server
svc.requiredHosts = []string{"127.0.0.1"}
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: ts.URL,
TimeoutMs: 1000,
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not allowed"))
})
It("should block redirects to private IPs when allowlist is set", func() {
// Server that redirects to a private IP
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "http://10.0.0.1/internal", http.StatusFound)
}))
// Allow the test server; redirect to 10.0.0.1 is blocked by allowlist
svc.requiredHosts = []string{"127.0.0.1"}
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: ts.URL,
TimeoutMs: 1000,
})
Expect(err).To(HaveOccurred())
Expect(resp).To(BeNil())
})
It("should allow wildcard host patterns", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("wildcard"))
}))
// *.allowed.org is in the requiredHosts from BeforeEach, but test server is 127.0.0.1
// Override with a wildcard that matches the test server
svc.requiredHosts = []string{"*.0.0.1"}
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: ts.URL,
TimeoutMs: 1000,
})
Expect(err).ToNot(HaveOccurred())
Expect(string(resp.Body)).To(Equal("wildcard"))
})
It("should reject hosts not matching wildcard patterns", func() {
svc.requiredHosts = []string{"*.example.com"}
_, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: "http://evil.other.com/test",
TimeoutMs: 1000,
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not allowed"))
})
})
Context("response body size limit", func() {
BeforeEach(func() {
svc = newHTTPService("test-plugin", &HTTPPermission{
RequiredHosts: []string{"127.0.0.1"},
})
})
It("should truncate response body at the size limit", func() {
// Serve a body larger than the limit
oversizedBody := strings.Repeat("x", httpClientMaxResponseBodyLen+1024)
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(oversizedBody))
}))
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "GET",
URL: ts.URL,
TimeoutMs: 5000,
})
Expect(err).ToNot(HaveOccurred())
Expect(len(resp.Body)).To(Equal(httpClientMaxResponseBodyLen))
})
})
Context("edge cases", func() {
BeforeEach(func() {
svc = newHTTPService("test-plugin", &HTTPPermission{
RequiredHosts: []string{"127.0.0.1"},
})
})
It("should default empty method to GET", func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("method:" + r.Method))
}))
// Empty method — Go's http.NewRequestWithContext normalizes "" to "GET"
resp, err := svc.Send(context.Background(), host.HTTPRequest{
Method: "",
URL: ts.URL,
TimeoutMs: 1000,
})
Expect(err).ToNot(HaveOccurred())
Expect(string(resp.Body)).To(Equal("method:GET"))
})
})
})
var _ = Describe("extractHostname", func() {
It("should extract hostname from host:port", func() {
Expect(extractHostname("example.com:8080")).To(Equal("example.com"))
})
It("should return hostname when no port", func() {
Expect(extractHostname("example.com")).To(Equal("example.com"))
})
It("should handle IPv6 with port", func() {
Expect(extractHostname("[::1]:8080")).To(Equal("::1"))
})
It("should handle IPv6 without port", func() {
Expect(extractHostname("::1")).To(Equal("::1"))
})
It("should strip brackets from IPv6 without port", func() {
Expect(extractHostname("[::1]")).To(Equal("::1"))
})
It("should handle IPv4 with port", func() {
Expect(extractHostname("127.0.0.1:9090")).To(Equal("127.0.0.1"))
})
It("should handle IPv4 without port", func() {
Expect(extractHostname("127.0.0.1")).To(Equal("127.0.0.1"))
})
})
var _ = Describe("isPrivateOrLoopback", func() {
It("should detect IPv4 loopback", func() {
Expect(isPrivateOrLoopback("127.0.0.1")).To(BeTrue())
Expect(isPrivateOrLoopback("127.0.0.2")).To(BeTrue())
})
It("should detect IPv6 loopback", func() {
Expect(isPrivateOrLoopback("::1")).To(BeTrue())
})
It("should detect localhost by name", func() {
Expect(isPrivateOrLoopback("localhost")).To(BeTrue())
Expect(isPrivateOrLoopback("LOCALHOST")).To(BeTrue())
})
It("should detect 10.x.x.x private range", func() {
Expect(isPrivateOrLoopback("10.0.0.1")).To(BeTrue())
Expect(isPrivateOrLoopback("10.255.255.255")).To(BeTrue())
})
It("should detect 172.16.x.x private range", func() {
Expect(isPrivateOrLoopback("172.16.0.1")).To(BeTrue())
Expect(isPrivateOrLoopback("172.31.255.255")).To(BeTrue())
})
It("should detect 192.168.x.x private range", func() {
Expect(isPrivateOrLoopback("192.168.0.1")).To(BeTrue())
Expect(isPrivateOrLoopback("192.168.255.255")).To(BeTrue())
})
It("should detect link-local addresses", func() {
Expect(isPrivateOrLoopback("169.254.169.254")).To(BeTrue())
Expect(isPrivateOrLoopback("169.254.0.1")).To(BeTrue())
})
It("should detect IPv6 private (fc00::/7)", func() {
Expect(isPrivateOrLoopback("fd00::1")).To(BeTrue())
})
It("should detect IPv6 link-local (fe80::/10)", func() {
Expect(isPrivateOrLoopback("fe80::1")).To(BeTrue())
})
It("should allow public IPs", func() {
Expect(isPrivateOrLoopback("8.8.8.8")).To(BeFalse())
Expect(isPrivateOrLoopback("203.0.113.1")).To(BeFalse())
Expect(isPrivateOrLoopback("2001:db8::1")).To(BeFalse())
})
It("should allow non-IP hostnames (DNS names)", func() {
Expect(isPrivateOrLoopback("example.com")).To(BeFalse())
Expect(isPrivateOrLoopback("api.example.com")).To(BeFalse())
})
It("should not treat 172.32.x.x as private", func() {
Expect(isPrivateOrLoopback("172.32.0.1")).To(BeFalse())
})
})

View File

@@ -256,8 +256,11 @@ func (s *webSocketServiceImpl) isHostAllowed(host string) bool {
}
// matchHostPattern matches a host against a pattern.
// Supports wildcards like *.example.com
// Supports "*" (allow all) and wildcards like "*.example.com".
func matchHostPattern(pattern, host string) bool {
if pattern == "*" {
return true
}
if pattern == host {
return true
}

View File

@@ -575,6 +575,12 @@ var _ = Describe("WebSocketService", Ordered, func() {
Expect(matchHostPattern("*.example.com", "deep.api.example.com")).To(BeTrue())
})
It("should match bare '*' as allow-all", func() {
Expect(matchHostPattern("*", "anything.example.com")).To(BeTrue())
Expect(matchHostPattern("*", "127.0.0.1")).To(BeTrue())
Expect(matchHostPattern("*", "::1")).To(BeTrue())
})
It("should not match partial patterns", func() {
Expect(matchHostPattern("*.example.com", "example.com.evil.org")).To(BeFalse())
})

View File

@@ -119,6 +119,15 @@ var hostServices = []hostServiceEntry{
return host.RegisterUsersHostFunctions(service), nil
},
},
{
name: "HTTP",
hasPermission: func(p *Permissions) bool { return p != nil && p.Http != nil },
create: func(ctx *serviceContext) ([]extism.HostFunction, io.Closer) {
perm := ctx.permissions.Http
service := newHTTPService(ctx.pluginName, perm)
return host.RegisterHTTPHostFunctions(service), nil
},
},
}
// extractManifest reads manifest from an .ndp package and computes its SHA-256 hash.

View File

@@ -38,6 +38,7 @@ The following host services are available:
- Artwork: provides artwork public URL generation capabilities for plugins.
- Cache: provides in-memory TTL-based caching capabilities for plugins.
- Config: provides access to plugin configuration values.
- HTTP: provides outbound HTTP request capabilities for plugins.
- KVStore: provides persistent key-value storage for plugins.
- Library: provides access to music library metadata for plugins.
- Scheduler: provides task scheduling capabilities for plugins.

View File

@@ -0,0 +1,87 @@
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the HTTP host service.
// It is intended for use in Navidrome plugins built with TinyGo.
//
//go:build wasip1
package host
import (
"encoding/json"
"errors"
"github.com/navidrome/navidrome/plugins/pdk/go/pdk"
)
// HTTPRequest represents an outbound HTTP request from a plugin.
type HTTPRequest struct {
Method string `json:"method"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Body []byte `json:"body"`
TimeoutMs int32 `json:"timeoutMs"`
}
// HTTPResponse represents the response from an outbound HTTP request.
type HTTPResponse struct {
StatusCode int32 `json:"statusCode"`
Headers map[string]string `json:"headers"`
Body []byte `json:"body"`
}
// http_send is the host function provided by Navidrome.
//
//go:wasmimport extism:host/user http_send
func http_send(uint64) uint64
type httpSendRequest struct {
Request HTTPRequest `json:"request"`
}
type httpSendResponse struct {
Result *HTTPResponse `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
// HTTPSend calls the http_send host function.
// Send executes an HTTP request and returns the response.
//
// Parameters:
// - request: The HTTP request to execute, including method, URL, headers, body, and timeout
//
// Returns the HTTP response with status code, headers, and body.
// Network errors, timeouts, and permission failures are returned as Go errors.
// Successful HTTP calls (including 4xx/5xx status codes) return a non-nil response with nil error.
func HTTPSend(request HTTPRequest) (*HTTPResponse, error) {
// Marshal request to JSON
req := httpSendRequest{
Request: request,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
reqMem := pdk.AllocateBytes(reqBytes)
defer reqMem.Free()
// Call the host function
responsePtr := http_send(reqMem.Offset())
// Read the response from memory
responseMem := pdk.FindMemory(responsePtr)
responseBytes := responseMem.ReadBytes()
// Parse the response
var response httpSendResponse
if err := json.Unmarshal(responseBytes, &response); err != nil {
return nil, err
}
// Convert Error field to Go error
if response.Error != "" {
return nil, errors.New(response.Error)
}
return response.Result, nil
}

View File

@@ -0,0 +1,55 @@
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains mock implementations for non-WASM builds.
// These mocks allow IDE support, compilation, and unit testing on non-WASM platforms.
// Plugin authors can use the exported mock instances to set expectations in tests.
//
//go:build !wasip1
package host
import "github.com/stretchr/testify/mock"
// HTTPRequest represents an outbound HTTP request from a plugin.
type HTTPRequest struct {
Method string `json:"method"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Body []byte `json:"body"`
TimeoutMs int32 `json:"timeoutMs"`
}
// HTTPResponse represents the response from an outbound HTTP request.
type HTTPResponse struct {
StatusCode int32 `json:"statusCode"`
Headers map[string]string `json:"headers"`
Body []byte `json:"body"`
}
// mockHTTPService is the mock implementation for testing.
type mockHTTPService struct {
mock.Mock
}
// HTTPMock is the auto-instantiated mock instance for testing.
// Use this to set expectations: host.HTTPMock.On("MethodName", args...).Return(values...)
var HTTPMock = &mockHTTPService{}
// Send is the mock method for HTTPSend.
func (m *mockHTTPService) Send(request HTTPRequest) (*HTTPResponse, error) {
args := m.Called(request)
return args.Get(0).(*HTTPResponse), args.Error(1)
}
// HTTPSend delegates to the mock instance.
// Send executes an HTTP request and returns the response.
//
// Parameters:
// - request: The HTTP request to execute, including method, URL, headers, body, and timeout
//
// Returns the HTTP response with status code, headers, and body.
// Network errors, timeouts, and permission failures are returned as Go errors.
// Successful HTTP calls (including 4xx/5xx status codes) return a non-nil response with nil error.
func HTTPSend(request HTTPRequest) (*HTTPResponse, error) {
return HTTPMock.Send(request)
}

View File

@@ -0,0 +1,59 @@
# Code generated by ndpgen. DO NOT EDIT.
#
# This file contains client wrappers for the HTTP host service.
# It is intended for use in Navidrome plugins built with extism-py.
#
# IMPORTANT: Due to a limitation in extism-py, you cannot import this file directly.
# The @extism.import_fn decorators are only detected when defined in the plugin's
# main __init__.py file. Copy the needed functions from this file into your plugin.
from dataclasses import dataclass
from typing import Any
import extism
import json
class HostFunctionError(Exception):
"""Raised when a host function returns an error."""
pass
@extism.import_fn("extism:host/user", "http_send")
def _http_send(offset: int) -> int:
"""Raw host function - do not call directly."""
...
def http_send(request: Any) -> Any:
"""Send executes an HTTP request and returns the response.
Parameters:
- request: The HTTP request to execute, including method, URL, headers, body, and timeout
Returns the HTTP response with status code, headers, and body.
Network errors, timeouts, and permission failures are returned as errors.
Successful HTTP calls (including 4xx/5xx status codes) return a non-nil response with nil error.
Args:
request: Any parameter.
Returns:
Any: The result value.
Raises:
HostFunctionError: If the host function returns an error.
"""
request = {
"request": request,
}
request_bytes = json.dumps(request).encode("utf-8")
request_mem = extism.memory.alloc(request_bytes)
response_offset = _http_send(request_mem.offset)
response_mem = extism.memory.find(response_offset)
response = json.loads(extism.memory.string(response_mem))
if response.get("error"):
raise HostFunctionError(response["error"])
return response.get("result", None)

View File

@@ -35,6 +35,7 @@
//! - [`artwork`] - provides artwork public URL generation capabilities for plugins.
//! - [`cache`] - provides in-memory TTL-based caching capabilities for plugins.
//! - [`config`] - provides access to plugin configuration values.
//! - [`http`] - provides outbound HTTP request capabilities for plugins.
//! - [`kvstore`] - provides persistent key-value storage for plugins.
//! - [`library`] - provides access to music library metadata for plugins.
//! - [`scheduler`] - provides task scheduling capabilities for plugins.
@@ -63,6 +64,13 @@ pub mod config {
pub use super::nd_host_config::*;
}
#[doc(hidden)]
mod nd_host_http;
/// provides outbound HTTP request capabilities for plugins.
pub mod http {
pub use super::nd_host_http::*;
}
#[doc(hidden)]
mod nd_host_kvstore;
/// provides persistent key-value storage for plugins.

View File

@@ -0,0 +1,83 @@
// Code generated by ndpgen. DO NOT EDIT.
//
// This file contains client wrappers for the HTTP host service.
// It is intended for use in Navidrome plugins built with extism-pdk.
use extism_pdk::*;
use serde::{Deserialize, Serialize};
/// HTTPRequest represents an outbound HTTP request from a plugin.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HttpRequest {
pub method: String,
pub url: String,
#[serde(default)]
pub headers: std::collections::HashMap<String, String>,
#[serde(default)]
pub body: Vec<u8>,
#[serde(default)]
pub timeout_ms: i32,
}
/// HTTPResponse represents the response from an outbound HTTP request.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HttpResponse {
pub status_code: i32,
#[serde(default)]
pub headers: std::collections::HashMap<String, String>,
#[serde(default)]
pub body: Vec<u8>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct HTTPSendRequest {
request: HttpRequest,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct HTTPSendResponse {
#[serde(default)]
result: Option<HttpResponse>,
#[serde(default)]
error: Option<String>,
}
#[host_fn]
extern "ExtismHost" {
fn http_send(input: Json<HTTPSendRequest>) -> Json<HTTPSendResponse>;
}
/// Send executes an HTTP request and returns the response.
///
/// Parameters:
/// - request: The HTTP request to execute, including method, URL, headers, body, and timeout
///
/// Returns the HTTP response with status code, headers, and body.
/// Network errors, timeouts, and permission failures are returned as errors.
/// Successful HTTP calls (including 4xx/5xx status codes) return a non-nil response with nil error.
///
/// # Arguments
/// * `request` - HttpRequest parameter.
///
/// # Returns
/// The result value.
///
/// # Errors
/// Returns an error if the host function call fails.
pub fn send(request: HttpRequest) -> Result<Option<HttpResponse>, Error> {
let response = unsafe {
http_send(Json(HTTPSendRequest {
request: request,
}))?
};
if let Some(err) = response.0.error {
return Err(Error::msg(err));
}
Ok(response.0.result)
}

View File

@@ -1 +1 @@
-s -r "(\.go$$|\.cpp$$|\.h$$|navidrome.toml|resources|token_received.html)" -R "(^ui|^data|^db/migrations)" -R "_test\.go$$" -- go run -race -tags netgo,sqlite_fts5,sqlite_spellfix .
-s -r "(\.go$$|\.cpp$$|\.h$$|navidrome.toml|resources|token_received.html)" -R "(^ui|^data|^db/migrations)" -R "_test\.go$$" -- go run -race -tags netgo,sqlite_fts5 .

View File

@@ -76,6 +76,7 @@ func serveIndex(ds model.DataStore, fs fs.FS, shareInfo *model.Share) http.Handl
"separator": string(os.PathSeparator),
"enableInspect": conf.Server.Inspect.Enabled,
"pluginsEnabled": conf.Server.Plugins.Enabled,
"extAuthLogoutURL": conf.Server.ExtAuth.LogoutURL,
}
if strings.HasPrefix(conf.Server.UILoginBackgroundURL, "/") {
appConfig["loginBackgroundURL"] = path.Join(conf.Server.BasePath, conf.Server.UILoginBackgroundURL)

View File

@@ -104,6 +104,7 @@ var _ = Describe("serveIndex", func() {
Entry("enableUserEditing", func() { conf.Server.EnableUserEditing = false }, "enableUserEditing", false),
Entry("enableSharing", func() { conf.Server.EnableSharing = true }, "enableSharing", true),
Entry("devNewEventStream", func() { conf.Server.DevNewEventStream = true }, "devNewEventStream", true),
Entry("extAuthLogoutURL", func() { conf.Server.ExtAuth.LogoutURL = "https://auth.example.com/logout" }, "extAuthLogoutURL", "https://auth.example.com/logout"),
)
DescribeTable("sets other UI configuration values",

View File

@@ -1,7 +1,7 @@
<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="v0.55.0" openSubsonic="true">
<playlists>
<playlist id="111" name="aaa" comment="comment" songCount="2" duration="120" public="true" owner="admin" created="2023-02-20T14:45:00Z" changed="2023-02-20T14:45:00Z" coverArt="pl-123123123123" readonly="true" validUntil="2023-02-20T14:45:00Z"></playlist>
<playlist id="333" name="ccc" songCount="0" duration="0" created="0001-01-01T00:00:00Z" changed="0001-01-01T00:00:00Z"></playlist>
<playlist id="222" name="bbb" songCount="0" duration="0" created="0001-01-01T00:00:00Z" changed="0001-01-01T00:00:00Z"></playlist>
<playlist id="333" name="ccc" songCount="0" duration="0" public="false" created="0001-01-01T00:00:00Z" changed="0001-01-01T00:00:00Z"></playlist>
<playlist id="222" name="bbb" songCount="0" duration="0" public="false" created="0001-01-01T00:00:00Z" changed="0001-01-01T00:00:00Z"></playlist>
</playlists>
</subsonic-response>

View File

@@ -303,7 +303,7 @@ type Playlist struct {
Comment string `xml:"comment,attr,omitempty" json:"comment,omitempty"`
SongCount int32 `xml:"songCount,attr" json:"songCount"`
Duration int32 `xml:"duration,attr" json:"duration"`
Public bool `xml:"public,attr,omitempty" json:"public,omitempty"`
Public bool `xml:"public,attr" json:"public,omitempty"`
Owner string `xml:"owner,attr,omitempty" json:"owner,omitempty"`
Created time.Time `xml:"created,attr" json:"created"`
Changed time.Time `xml:"changed,attr" json:"changed"`

View File

@@ -66,6 +66,10 @@ const authProvider = {
logout: () => {
removeItems()
if (config.extAuthLogoutURL) {
window.location.href = config.extAuthLogoutURL
return Promise.resolve(false)
}
return Promise.resolve()
},

View File

@@ -122,7 +122,7 @@ const UserMenu = (props) => {
})
: null,
)}
{!config.auth && logout}
{(!config.auth || !!config.extAuthLogoutURL) && logout}
</MenuList>
</Popover>
</div>