Update On Mon Nov 17 19:42:26 CET 2025

This commit is contained in:
github-action[bot]
2025-11-17 19:42:26 +01:00
parent 3584da6277
commit 56abc53ba3
65 changed files with 1524 additions and 1200 deletions
+1
View File
@@ -1184,3 +1184,4 @@ Update On Thu Nov 13 19:41:20 CET 2025
Update On Fri Nov 14 19:38:12 CET 2025
Update On Sat Nov 15 19:34:45 CET 2025
Update On Sun Nov 16 19:35:35 CET 2025
Update On Mon Nov 17 19:42:18 CET 2025
+25
View File
@@ -2,6 +2,31 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [2.48.1](https://github.com/filebrowser/filebrowser/compare/v2.48.0...v2.48.1) (2025-11-17)
### Bug Fixes
* options should only override if set ([420adea](https://github.com/filebrowser/filebrowser/commit/420adea7e61a1c182cddd6fb2544a0752e5709f7))
## [2.48.0](https://github.com/filebrowser/filebrowser/compare/v2.47.0...v2.48.0) (2025-11-17)
### Features
* consistent flags and environment variables ([#5549](https://github.com/filebrowser/filebrowser/issues/5549)) ([0a0cb80](https://github.com/filebrowser/filebrowser/commit/0a0cb8046fce52f1ff926171b34bcdb7cd39aab3))
### Bug Fixes
* add tokenExpirationTime to `config init` and troubleshoot docs ([#5546](https://github.com/filebrowser/filebrowser/issues/5546)) ([8c5dc76](https://github.com/filebrowser/filebrowser/commit/8c5dc7641e6f8aadd9e5d5d3b25a2ad9f1ec9a1e))
* use all available flags in quick setup ([f41585f](https://github.com/filebrowser/filebrowser/commit/f41585f0392d65c08c01ab65b62d3eeb04c03b7d))
### Refactorings
* reuse logic for config init and set ([89be0b1](https://github.com/filebrowser/filebrowser/commit/89be0b1873527987dd2dddac746e93b8bc684d46))
## [2.47.0](https://github.com/filebrowser/filebrowser/compare/v2.46.1...v2.47.0) (2025-11-16)
+4 -1
View File
@@ -49,7 +49,10 @@ tasks:
cmds:
- task: docs:cli:generate
- git add www/docs/cli
- "git commit -m 'chore(docs): update CLI documentation'"
- |
if [[ `git status www/docs/cli --porcelain` ]]; then
git commit -m 'chore(docs): update CLI documentation'
fi
- task: release:dry-run
- task: release:make
+35
View File
@@ -0,0 +1,35 @@
package cmd
import (
"testing"
"github.com/samber/lo"
"github.com/spf13/cobra"
)
// TestEnvCollisions ensures that there are no collisions in the produced environment
// variable names for all commands and their flags.
func TestEnvCollisions(t *testing.T) {
testEnvCollisions(t, rootCmd)
}
func testEnvCollisions(t *testing.T, cmd *cobra.Command) {
for _, cmd := range cmd.Commands() {
testEnvCollisions(t, cmd)
}
replacements := generateEnvKeyReplacements(cmd)
envVariables := []string{}
for i := range replacements {
if i%2 != 0 {
envVariables = append(envVariables, replacements[i])
}
}
duplicates := lo.FindDuplicates(envVariables)
if len(duplicates) > 0 {
t.Errorf("Found duplicate environment variable keys for command %q: %v", cmd.Name(), duplicates)
}
}
+3 -1
View File
@@ -19,7 +19,8 @@ var cmdsLsCmd = &cobra.Command{
if err != nil {
return err
}
evt, err := getString(cmd.Flags(), "event")
evt, err := cmd.Flags().GetString("event")
if err != nil {
return err
}
@@ -32,6 +33,7 @@ var cmdsLsCmd = &cobra.Command{
show["after_"+evt] = s.Commands["after_"+evt]
printEvents(show)
}
return nil
}, pythonConfig{}),
}
+143 -14
View File
@@ -30,12 +30,18 @@ var configCmd = &cobra.Command{
func addConfigFlags(flags *pflag.FlagSet) {
addServerFlags(flags)
addUserFlags(flags)
flags.BoolP("signup", "s", false, "allow users to signup")
flags.Bool("hide-login-button", false, "hide login button from public pages")
flags.Bool("create-user-dir", false, "generate user's home directory automatically")
flags.Uint("minimum-password-length", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
flags.Bool("hideLoginButton", false, "hide login button from public pages")
flags.Bool("createUserDir", false, "generate user's home directory automatically")
flags.Uint("minimumPasswordLength", settings.DefaultMinimumPasswordLength, "minimum password length for new users")
flags.String("shell", "", "shell command to which other commands should be appended")
// NB: these are string so they can be presented as octal in the help text
// as that's the conventional representation for modes in Unix.
flags.String("fileMode", fmt.Sprintf("%O", settings.DefaultFileMode), "mode bits that new files are created with")
flags.String("dirMode", fmt.Sprintf("%O", settings.DefaultDirMode), "mode bits that new directories are created with")
flags.String("auth.method", string(auth.MethodJSONAuth), "authentication type")
flags.String("auth.header", "", "HTTP header for auth.method=proxy")
flags.String("auth.command", "", "command for auth.method=hook")
@@ -50,17 +56,13 @@ func addConfigFlags(flags *pflag.FlagSet) {
flags.String("branding.files", "", "path to directory with images and custom styles")
flags.Bool("branding.disableExternal", false, "disable external links such as GitHub links")
flags.Bool("branding.disableUsedPercentage", false, "disable used disk percentage graph")
// NB: these are string so they can be presented as octal in the help text
// as that's the conventional representation for modes in Unix.
flags.String("file-mode", fmt.Sprintf("%O", settings.DefaultFileMode), "mode bits that new files are created with")
flags.String("dir-mode", fmt.Sprintf("%O", settings.DefaultDirMode), "mode bits that new directories are created with")
flags.Uint64("tus.chunkSize", settings.DefaultTusChunkSize, "the tus chunk size")
flags.Uint16("tus.retryCount", settings.DefaultTusRetryCount, "the tus retry count")
}
func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.AuthMethod, map[string]interface{}, error) {
methodStr, err := getString(flags, "auth.method")
methodStr, err := flags.GetString("auth.method")
if err != nil {
return "", nil, err
}
@@ -91,7 +93,7 @@ func getAuthMethod(flags *pflag.FlagSet, defaults ...interface{}) (settings.Auth
}
func getProxyAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
header, err := getString(flags, "auth.header")
header, err := flags.GetString("auth.header")
if err != nil {
return nil, err
}
@@ -113,15 +115,17 @@ func getNoAuth() auth.Auther {
func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
jsonAuth := &auth.JSONAuth{}
host, err := getString(flags, "recaptcha.host")
host, err := flags.GetString("recaptcha.host")
if err != nil {
return nil, err
}
key, err := getString(flags, "recaptcha.key")
key, err := flags.GetString("recaptcha.key")
if err != nil {
return nil, err
}
secret, err := getString(flags, "recaptcha.secret")
secret, err := flags.GetString("recaptcha.secret")
if err != nil {
return nil, err
}
@@ -149,11 +153,10 @@ func getJSONAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (au
}
func getHookAuth(flags *pflag.FlagSet, defaultAuther map[string]interface{}) (auth.Auther, error) {
command, err := getString(flags, "auth.command")
command, err := flags.GetString("auth.command")
if err != nil {
return nil, err
}
if command == "" {
command = defaultAuther["command"].(string)
}
@@ -201,6 +204,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "Minimum Password Length:\t%d\n", set.MinimumPasswordLength)
fmt.Fprintf(w, "Auth Method:\t%s\n", set.AuthMethod)
fmt.Fprintf(w, "Shell:\t%s\t\n", strings.Join(set.Shell, " "))
fmt.Fprintln(w, "\nBranding:")
fmt.Fprintf(w, "\tName:\t%s\n", set.Branding.Name)
fmt.Fprintf(w, "\tFiles override:\t%s\n", set.Branding.Files)
@@ -208,6 +212,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tDisable used disk percentage graph:\t%t\n", set.Branding.DisableUsedPercentage)
fmt.Fprintf(w, "\tColor:\t%s\n", set.Branding.Color)
fmt.Fprintf(w, "\tTheme:\t%s\n", set.Branding.Theme)
fmt.Fprintln(w, "\nServer:")
fmt.Fprintf(w, "\tLog:\t%s\n", ser.Log)
fmt.Fprintf(w, "\tPort:\t%s\n", ser.Port)
@@ -217,10 +222,16 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tAddress:\t%s\n", ser.Address)
fmt.Fprintf(w, "\tTLS Cert:\t%s\n", ser.TLSCert)
fmt.Fprintf(w, "\tTLS Key:\t%s\n", ser.TLSKey)
fmt.Fprintf(w, "\tToken Expiration Time:\t%s\n", ser.TokenExpirationTime)
fmt.Fprintf(w, "\tExec Enabled:\t%t\n", ser.EnableExec)
fmt.Fprintf(w, "\tThumbnails Enabled:\t%t\n", ser.EnableThumbnails)
fmt.Fprintf(w, "\tResize Preview:\t%t\n", ser.ResizePreview)
fmt.Fprintf(w, "\tType Detection by Header:\t%t\n", ser.TypeDetectionByHeader)
fmt.Fprintln(w, "\nTUS:")
fmt.Fprintf(w, "\tChunk size:\t%d\n", set.Tus.ChunkSize)
fmt.Fprintf(w, "\tRetry count:\t%d\n", set.Tus.RetryCount)
fmt.Fprintln(w, "\nDefaults:")
fmt.Fprintf(w, "\tScope:\t%s\n", set.Defaults.Scope)
fmt.Fprintf(w, "\tHideDotfiles:\t%t\n", set.Defaults.HideDotfiles)
@@ -231,9 +242,11 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\tDirectory Creation Mode:\t%O\n", set.DirMode)
fmt.Fprintf(w, "\tCommands:\t%s\n", strings.Join(set.Defaults.Commands, " "))
fmt.Fprintf(w, "\tAce editor syntax highlighting theme:\t%s\n", set.Defaults.AceEditorTheme)
fmt.Fprintf(w, "\tSorting:\n")
fmt.Fprintf(w, "\t\tBy:\t%s\n", set.Defaults.Sorting.By)
fmt.Fprintf(w, "\t\tAsc:\t%t\n", set.Defaults.Sorting.Asc)
fmt.Fprintf(w, "\tPermissions:\n")
fmt.Fprintf(w, "\t\tAdmin:\t%t\n", set.Defaults.Perm.Admin)
fmt.Fprintf(w, "\t\tExecute:\t%t\n", set.Defaults.Perm.Execute)
@@ -243,6 +256,7 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Fprintf(w, "\t\tDelete:\t%t\n", set.Defaults.Perm.Delete)
fmt.Fprintf(w, "\t\tShare:\t%t\n", set.Defaults.Perm.Share)
fmt.Fprintf(w, "\t\tDownload:\t%t\n", set.Defaults.Perm.Download)
w.Flush()
b, err := json.MarshalIndent(auther, "", " ")
@@ -252,3 +266,118 @@ func printSettings(ser *settings.Server, set *settings.Settings, auther auth.Aut
fmt.Printf("\nAuther configuration (raw):\n\n%s\n\n", string(b))
return nil
}
func getSettings(flags *pflag.FlagSet, set *settings.Settings, ser *settings.Server, auther auth.Auther, all bool) (auth.Auther, error) {
errs := []error{}
hasAuth := false
visit := func(flag *pflag.Flag) {
var err error
switch flag.Name {
// Server flags from [addServerFlags]
case "address":
ser.Address, err = flags.GetString(flag.Name)
case "log":
ser.Log, err = flags.GetString(flag.Name)
case "port":
ser.Port, err = flags.GetString(flag.Name)
case "cert":
ser.TLSCert, err = flags.GetString(flag.Name)
case "key":
ser.TLSKey, err = flags.GetString(flag.Name)
case "root":
ser.Root, err = flags.GetString(flag.Name)
case "socket":
ser.Socket, err = flags.GetString(flag.Name)
case "baseURL":
ser.BaseURL, err = flags.GetString(flag.Name)
case "tokenExpirationTime":
ser.TokenExpirationTime, err = flags.GetString(flag.Name)
case "disableThumbnails":
ser.EnableThumbnails, err = flags.GetBool(flag.Name)
ser.EnableThumbnails = !ser.EnableThumbnails
case "disablePreviewResize":
ser.ResizePreview, err = flags.GetBool(flag.Name)
ser.ResizePreview = !ser.ResizePreview
case "disableExec":
ser.EnableExec, err = flags.GetBool(flag.Name)
ser.EnableExec = !ser.EnableExec
case "disableTypeDetectionByHeader":
ser.TypeDetectionByHeader, err = flags.GetBool(flag.Name)
ser.TypeDetectionByHeader = !ser.TypeDetectionByHeader
// Settings flags from [addConfigFlags]
case "signup":
set.Signup, err = flags.GetBool(flag.Name)
case "hideLoginButton":
set.HideLoginButton, err = flags.GetBool(flag.Name)
case "createUserDir":
set.CreateUserDir, err = flags.GetBool(flag.Name)
case "minimumPasswordLength":
set.MinimumPasswordLength, err = flags.GetUint(flag.Name)
case "shell":
var shell string
shell, err = flags.GetString(flag.Name)
if err == nil {
set.Shell = convertCmdStrToCmdArray(shell)
}
case "fileMode":
set.FileMode, err = getAndParseFileMode(flags, flag.Name)
case "dirMode":
set.DirMode, err = getAndParseFileMode(flags, flag.Name)
case "auth.method":
hasAuth = true
case "branding.name":
set.Branding.Name, err = flags.GetString(flag.Name)
case "branding.theme":
set.Branding.Theme, err = flags.GetString(flag.Name)
case "branding.color":
set.Branding.Color, err = flags.GetString(flag.Name)
case "branding.files":
set.Branding.Files, err = flags.GetString(flag.Name)
case "branding.disableExternal":
set.Branding.DisableExternal, err = flags.GetBool(flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage, err = flags.GetBool(flag.Name)
case "tus.chunkSize":
set.Tus.ChunkSize, err = flags.GetUint64(flag.Name)
case "tus.retryCount":
set.Tus.RetryCount, err = flags.GetUint16(flag.Name)
}
if err != nil {
errs = append(errs, err)
}
}
if all {
flags.VisitAll(visit)
} else {
flags.Visit(visit)
}
err := nerrors.Join(errs...)
if err != nil {
return nil, err
}
err = getUserDefaults(flags, &set.Defaults, all)
if err != nil {
return nil, err
}
if all {
set.AuthMethod, auther, err = getAuthentication(flags)
if err != nil {
return nil, err
}
} else {
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
if err != nil {
return nil, err
}
}
return auther, nil
}
+2 -2
View File
@@ -37,7 +37,7 @@ The path must be for a json or yaml file.`,
RunE: python(func(_ *cobra.Command, args []string, d *pythonData) error {
var key []byte
var err error
if d.hadDB {
if d.databaseExisted {
settings, settingErr := d.store.Settings.Get()
if settingErr != nil {
return settingErr
@@ -104,7 +104,7 @@ The path must be for a json or yaml file.`,
}
return printSettings(file.Server, file.Settings, auther)
}, pythonConfig{allowNoDB: true}),
}, pythonConfig{allowsNoDatabase: true}),
}
func getAuther(sample auth.Auther, data interface{}) (interface{}, error) {
+11 -152
View File
@@ -23,170 +23,29 @@ to the defaults when creating new users and you don't
override the options.`,
Args: cobra.NoArgs,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
defaults := settings.UserDefaults{}
flags := cmd.Flags()
err := getUserDefaults(flags, &defaults, true)
if err != nil {
return err
}
authMethod, auther, err := getAuthentication(flags)
// Initialize config
s := &settings.Settings{Key: generateKey()}
ser := &settings.Server{}
// Fill config with options
auther, err := getSettings(flags, s, ser, nil, true)
if err != nil {
return err
}
key := generateKey()
signup, err := getBool(flags, "signup")
if err != nil {
return err
}
hideLoginButton, err := getBool(flags, "hide-login-button")
if err != nil {
return err
}
createUserDir, err := getBool(flags, "create-user-dir")
if err != nil {
return err
}
minLength, err := getUint(flags, "minimum-password-length")
if err != nil {
return err
}
shell, err := getString(flags, "shell")
if err != nil {
return err
}
brandingName, err := getString(flags, "branding.name")
if err != nil {
return err
}
brandingDisableExternal, err := getBool(flags, "branding.disableExternal")
if err != nil {
return err
}
brandingDisableUsedPercentage, err := getBool(flags, "branding.disableUsedPercentage")
if err != nil {
return err
}
brandingTheme, err := getString(flags, "branding.theme")
if err != nil {
return err
}
brandingFiles, err := getString(flags, "branding.files")
if err != nil {
return err
}
tusChunkSize, err := flags.GetUint64("tus.chunkSize")
if err != nil {
return err
}
tusRetryCount, err := flags.GetUint16("tus.retryCount")
if err != nil {
return err
}
s := &settings.Settings{
Key: key,
Signup: signup,
HideLoginButton: hideLoginButton,
CreateUserDir: createUserDir,
MinimumPasswordLength: minLength,
Shell: convertCmdStrToCmdArray(shell),
AuthMethod: authMethod,
Defaults: defaults,
Branding: settings.Branding{
Name: brandingName,
DisableExternal: brandingDisableExternal,
DisableUsedPercentage: brandingDisableUsedPercentage,
Theme: brandingTheme,
Files: brandingFiles,
},
Tus: settings.Tus{
ChunkSize: tusChunkSize,
RetryCount: tusRetryCount,
},
}
s.FileMode, err = getMode(flags, "file-mode")
if err != nil {
return err
}
s.DirMode, err = getMode(flags, "dir-mode")
if err != nil {
return err
}
address, err := getString(flags, "address")
if err != nil {
return err
}
socket, err := getString(flags, "socket")
if err != nil {
return err
}
root, err := getString(flags, "root")
if err != nil {
return err
}
baseURL, err := getString(flags, "baseurl")
if err != nil {
return err
}
tlsKey, err := getString(flags, "key")
if err != nil {
return err
}
cert, err := getString(flags, "cert")
if err != nil {
return err
}
port, err := getString(flags, "port")
if err != nil {
return err
}
log, err := getString(flags, "log")
if err != nil {
return err
}
ser := &settings.Server{
Address: address,
Socket: socket,
Root: root,
BaseURL: baseURL,
TLSKey: tlsKey,
TLSCert: cert,
Port: port,
Log: log,
}
// Save updated config
err = d.store.Settings.Save(s)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
if err != nil {
return err
}
err = d.store.Auth.Save(auther)
if err != nil {
return err
@@ -198,5 +57,5 @@ Now add your first user via 'filebrowser users add' and then you just
need to call the main command to boot up the server.
`)
return printSettings(ser, s, auther)
}, pythonConfig{noDB: true}),
}, pythonConfig{expectsNoDatabase: true}),
}
+7 -72
View File
@@ -2,7 +2,6 @@ package cmd
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func init() {
@@ -18,6 +17,8 @@ you want to change. Other options will remain unchanged.`,
Args: cobra.NoArgs,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
flags := cmd.Flags()
// Read existing config
set, err := d.store.Settings.Get()
if err != nil {
return err
@@ -28,94 +29,28 @@ you want to change. Other options will remain unchanged.`,
return err
}
hasAuth := false
flags.Visit(func(flag *pflag.Flag) {
if err != nil {
return
}
switch flag.Name {
case "baseurl":
ser.BaseURL, err = getString(flags, flag.Name)
case "root":
ser.Root, err = getString(flags, flag.Name)
case "socket":
ser.Socket, err = getString(flags, flag.Name)
case "cert":
ser.TLSCert, err = getString(flags, flag.Name)
case "key":
ser.TLSKey, err = getString(flags, flag.Name)
case "address":
ser.Address, err = getString(flags, flag.Name)
case "port":
ser.Port, err = getString(flags, flag.Name)
case "log":
ser.Log, err = getString(flags, flag.Name)
case "hide-login-button":
set.HideLoginButton, err = getBool(flags, flag.Name)
case "signup":
set.Signup, err = getBool(flags, flag.Name)
case "auth.method":
hasAuth = true
case "shell":
var shell string
shell, err = getString(flags, flag.Name)
set.Shell = convertCmdStrToCmdArray(shell)
case "create-user-dir":
set.CreateUserDir, err = getBool(flags, flag.Name)
case "minimum-password-length":
set.MinimumPasswordLength, err = getUint(flags, flag.Name)
case "branding.name":
set.Branding.Name, err = getString(flags, flag.Name)
case "branding.color":
set.Branding.Color, err = getString(flags, flag.Name)
case "branding.theme":
set.Branding.Theme, err = getString(flags, flag.Name)
case "branding.disableExternal":
set.Branding.DisableExternal, err = getBool(flags, flag.Name)
case "branding.disableUsedPercentage":
set.Branding.DisableUsedPercentage, err = getBool(flags, flag.Name)
case "branding.files":
set.Branding.Files, err = getString(flags, flag.Name)
case "file-mode":
set.FileMode, err = getMode(flags, flag.Name)
case "dir-mode":
set.DirMode, err = getMode(flags, flag.Name)
case "tus.chunkSize":
set.Tus.ChunkSize, err = flags.GetUint64(flag.Name)
case "tus.retryCount":
set.Tus.RetryCount, err = flags.GetUint16(flag.Name)
}
})
if err != nil {
return err
}
err = getUserDefaults(flags, &set.Defaults, false)
if err != nil {
return err
}
// read the defaults
auther, err := d.store.Auth.Get(set.AuthMethod)
if err != nil {
return err
}
// check if there are new flags for existing auth method
set.AuthMethod, auther, err = getAuthentication(flags, hasAuth, set, auther)
// Get updated config
auther, err = getSettings(flags, set, ser, auther, false)
if err != nil {
return err
}
// Save updated config
err = d.store.Auth.Save(auther)
if err != nil {
return err
}
err = d.store.Settings.Save(set)
if err != nil {
return err
}
err = d.store.Settings.SaveServer(ser)
if err != nil {
return err
+141 -198
View File
@@ -13,15 +13,13 @@ import (
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
v "github.com/spf13/viper"
"github.com/spf13/viper"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
"github.com/filebrowser/filebrowser/v2/auth"
@@ -35,28 +33,67 @@ import (
)
var (
cfgFile string
flagNamesMigrations = map[string]string{
"file-mode": "fileMode",
"dir-mode": "dirMode",
"hide-login-button": "hideLoginButton",
"create-user-dir": "createUserDir",
"minimum-password-length": "minimumPasswordLength",
"socket-perm": "socketPerm",
"disable-thumbnails": "disableThumbnails",
"disable-preview-resize": "disablePreviewResize",
"disable-exec": "disableExec",
"disable-type-detection-by-header": "disableTypeDetectionByHeader",
"img-processors": "imageProcessors",
"cache-dir": "cacheDir",
"token-expiration-time": "tokenExpirationTime",
"baseurl": "baseURL",
}
warnedFlags = map[string]bool{}
)
// TODO(remove): remove after July 2026.
func migrateFlagNames(f *pflag.FlagSet, name string) pflag.NormalizedName {
if newName, ok := flagNamesMigrations[name]; ok {
if !warnedFlags[name] {
warnedFlags[name] = true
fmt.Printf("WARNING: Flag --%s has been deprecated, use --%s instead\n", name, newName)
}
name = newName
}
return pflag.NormalizedName(name)
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.SilenceUsage = true
rootCmd.SetGlobalNormalizationFunc(migrateFlagNames)
cobra.MousetrapHelpText = ""
rootCmd.SetVersionTemplate("File Browser version {{printf \"%s\" .Version}}\n")
flags := rootCmd.Flags()
// Flags available across the whole program
persistent := rootCmd.PersistentFlags()
persistent.StringVarP(&cfgFile, "config", "c", "", "config file path")
persistent.StringP("config", "c", "", "config file path")
persistent.StringP("database", "d", "./filebrowser.db", "database path")
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
flags.String("username", "admin", "username for the first user when using quick config")
flags.String("password", "", "hashed password for the first user when using quick config")
// Runtime flags for the root command
flags := rootCmd.Flags()
flags.Bool("noauth", false, "use the noauth auther when using quick setup")
flags.String("username", "admin", "username for the first user when using quick setup")
flags.String("password", "", "hashed password for the first user when using quick setup")
flags.Uint32("socketPerm", 0666, "unix socket file permissions")
flags.String("cacheDir", "", "file cache directory (disabled if empty)")
flags.Int("imageProcessors", 4, "image processors count")
addServerFlags(flags)
}
// addServerFlags adds server related flags to the given FlagSet. These flags are available
// in both the root command, config set and config init commands.
func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("address", "a", "127.0.0.1", "address to listen on")
flags.StringP("log", "l", "stdout", "log output")
@@ -65,15 +102,12 @@ func addServerFlags(flags *pflag.FlagSet) {
flags.StringP("key", "k", "", "tls key")
flags.StringP("root", "r", ".", "root to prepend to relative paths")
flags.String("socket", "", "socket to listen to (cannot be used with address, port, cert nor key flags)")
flags.Uint32("socket-perm", 0666, "unix socket file permissions")
flags.StringP("baseurl", "b", "", "base url")
flags.String("cache-dir", "", "file cache directory (disabled if empty)")
flags.String("token-expiration-time", "2h", "user session timeout")
flags.Int("img-processors", 4, "image processors count")
flags.Bool("disable-thumbnails", false, "disable image thumbnails")
flags.Bool("disable-preview-resize", false, "disable resize of image previews")
flags.Bool("disable-exec", true, "disables Command Runner feature")
flags.Bool("disable-type-detection-by-header", false, "disables type detection by reading file headers")
flags.StringP("baseURL", "b", "", "base url")
flags.String("tokenExpirationTime", "2h", "user session timeout")
flags.Bool("disableThumbnails", false, "disable image thumbnails")
flags.Bool("disablePreviewResize", false, "disable resize of image previews")
flags.Bool("disableExec", true, "disables Command Runner feature")
flags.Bool("disableTypeDetectionByHeader", false, "disables type detection by reading file headers")
}
var rootCmd = &cobra.Command{
@@ -88,12 +122,14 @@ it. Don't worry: you don't need to setup a separate database server.
We're using Bolt DB which is a single file database and all managed
by ourselves.
For this specific command, all the flags you have available (except
"config" for the configuration file), can be given either through
environment variables or configuration files.
For this command, all flags are available as environmental variables,
except for "--config", which specifies the configuration file to use.
The environment variables are prefixed by "FB_" followed by the flag name in
UPPER_SNAKE_CASE. For example, the flag "--disablePreviewResize" is available
as FB_DISABLE_PREVIEW_RESIZE.
If you don't set "config", it will look for a configuration file called
.filebrowser.{json, toml, yaml, yml} in the following directories:
If "--config" is not specified, File Browser will look for a configuration
file named .filebrowser.{json, toml, yaml, yml} in the following directories:
- ./
- $HOME/
@@ -101,44 +137,32 @@ If you don't set "config", it will look for a configuration file called
The precedence of the configuration values are as follows:
- flags
- environment variables
- configuration file
- database values
- defaults
The environment variables are prefixed by "FB_" followed by the option
name in caps. So to set "database" via an env variable, you should
set FB_DATABASE.
- Flags
- Environment variables
- Configuration file
- Database values
- Defaults
Also, if the database path doesn't exist, File Browser will enter into
the quick setup mode and a new database will be bootstrapped and a new
user created with the credentials from options "username" and "password".`,
RunE: python(func(cmd *cobra.Command, _ []string, d *pythonData) error {
log.Println(cfgFile)
if !d.hadDB {
err := quickSetup(cmd.Flags(), *d)
if !d.databaseExisted {
err := quickSetup(*d)
if err != nil {
return err
}
}
// build img service
workersCount, err := cmd.Flags().GetInt("img-processors")
if err != nil {
return err
}
if workersCount < 1 {
imgWorkersCount := d.viper.GetInt("imageProcessors")
if imgWorkersCount < 1 {
return errors.New("image resize workers count could not be < 1")
}
imgSvc := img.New(workersCount)
imageService := img.New(imgWorkersCount)
var fileCache diskcache.Interface = diskcache.NewNoOp()
cacheDir, err := cmd.Flags().GetString("cache-dir")
if err != nil {
return err
}
cacheDir := d.viper.GetString("cacheDir")
if cacheDir != "" {
if err := os.MkdirAll(cacheDir, 0700); err != nil {
return fmt.Errorf("can't make directory %s: %w", cacheDir, err)
@@ -146,7 +170,7 @@ user created with the credentials from options "username" and "password".`,
fileCache = diskcache.New(afero.NewOsFs(), cacheDir)
}
server, err := getRunParams(cmd.Flags(), d.store)
server, err := getServerSettings(d.viper, d.store)
if err != nil {
return err
}
@@ -168,10 +192,7 @@ user created with the credentials from options "username" and "password".`,
if err != nil {
return err
}
socketPerm, err := cmd.Flags().GetUint32("socket-perm")
if err != nil {
return err
}
socketPerm := d.viper.GetUint32("socketPerm")
err = os.Chmod(server.Socket, os.FileMode(socketPerm))
if err != nil {
return err
@@ -200,7 +221,7 @@ user created with the credentials from options "username" and "password".`,
panic(err)
}
handler, err := fbhttp.NewHandler(imgSvc, fileCache, d.store, server, assetsFs)
handler, err := fbhttp.NewHandler(imageService, fileCache, d.store, server, assetsFs)
if err != nil {
return err
}
@@ -241,53 +262,73 @@ user created with the credentials from options "username" and "password".`,
log.Println("Graceful shutdown complete.")
return nil
}, pythonConfig{allowNoDB: true}),
}, pythonConfig{allowsNoDatabase: true}),
}
func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server, error) {
func getServerSettings(v *viper.Viper, st *storage.Storage) (*settings.Server, error) {
server, err := st.Settings.GetServer()
if err != nil {
return nil, err
}
if val, set := getStringParamB(flags, "root"); set {
server.Root = val
}
if val, set := getStringParamB(flags, "baseurl"); set {
server.BaseURL = val
}
if val, set := getStringParamB(flags, "log"); set {
server.Log = val
}
isSocketSet := false
isAddrSet := false
if val, set := getStringParamB(flags, "address"); set {
server.Address = val
isAddrSet = isAddrSet || set
if v.IsSet("address") {
server.Address = v.GetString("address")
isAddrSet = true
}
if val, set := getStringParamB(flags, "port"); set {
server.Port = val
isAddrSet = isAddrSet || set
if v.IsSet("log") {
server.Log = v.GetString("log")
}
if val, set := getStringParamB(flags, "key"); set {
server.TLSKey = val
isAddrSet = isAddrSet || set
if v.IsSet("port") {
server.Port = v.GetString("port")
isAddrSet = true
}
if val, set := getStringParamB(flags, "cert"); set {
server.TLSCert = val
isAddrSet = isAddrSet || set
if v.IsSet("cert") {
server.TLSCert = v.GetString("cert")
isAddrSet = true
}
if val, set := getStringParamB(flags, "socket"); set {
server.Socket = val
isSocketSet = isSocketSet || set
if v.IsSet("key") {
server.TLSKey = v.GetString("key")
isAddrSet = true
}
if v.IsSet("root") {
server.Root = v.GetString("root")
}
if v.IsSet("socket") {
server.Socket = v.GetString("socket")
isSocketSet = true
}
if v.IsSet("baseURL") {
server.BaseURL = v.GetString("baseURL")
}
if v.IsSet("tokenExpirationTime") {
server.TokenExpirationTime = v.GetString("tokenExpirationTime")
}
if v.IsSet("disableThumbnails") {
server.EnableThumbnails = !v.GetBool("disableThumbnails")
}
if v.IsSet("disablePreviewResize") {
server.ResizePreview = !v.GetBool("disablePreviewResize")
}
if v.IsSet("disableTypeDetectionByHeader") {
server.TypeDetectionByHeader = !v.GetBool("disableTypeDetectionByHeader")
}
if v.IsSet("disableExec") {
server.EnableExec = !v.GetBool("disableExec")
}
if isAddrSet && isSocketSet {
@@ -299,18 +340,6 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server,
server.Socket = ""
}
disableThumbnails := getBoolParam(flags, "disable-thumbnails")
server.EnableThumbnails = !disableThumbnails
disablePreviewResize := getBoolParam(flags, "disable-preview-resize")
server.ResizePreview = !disablePreviewResize
disableTypeDetectionByHeader := getBoolParam(flags, "disable-type-detection-by-header")
server.TypeDetectionByHeader = !disableTypeDetectionByHeader
disableExec := getBoolParam(flags, "disable-exec")
server.EnableExec = !disableExec
if server.EnableExec {
log.Println("WARNING: Command Runner feature enabled!")
log.Println("WARNING: This feature has known security vulnerabilities and should not")
@@ -318,71 +347,9 @@ func getRunParams(flags *pflag.FlagSet, st *storage.Storage) (*settings.Server,
log.Println("WARNING: read https://github.com/filebrowser/filebrowser/issues/5199")
}
if val, set := getStringParamB(flags, "token-expiration-time"); set {
server.TokenExpirationTime = val
}
return server, nil
}
// getBoolParamB returns a parameter as a string and a boolean to tell if it is different from the default
//
// NOTE: we could simply bind the flags to viper and use IsSet.
// Although there is a bug on Viper that always returns true on IsSet
// if a flag is binded. Our alternative way is to manually check
// the flag and then the value from env/config/gotten by viper.
// https://github.com/spf13/viper/pull/331
func getBoolParamB(flags *pflag.FlagSet, key string) (value, ok bool) {
value, _ = flags.GetBool(key)
// If set on Flags, use it.
if flags.Changed(key) {
return value, true
}
// If set through viper (env, config), return it.
if v.IsSet(key) {
return v.GetBool(key), true
}
// Otherwise use default value on flags.
return value, false
}
func getBoolParam(flags *pflag.FlagSet, key string) bool {
val, _ := getBoolParamB(flags, key)
return val
}
// getStringParamB returns a parameter as a string and a boolean to tell if it is different from the default
//
// NOTE: we could simply bind the flags to viper and use IsSet.
// Although there is a bug on Viper that always returns true on IsSet
// if a flag is binded. Our alternative way is to manually check
// the flag and then the value from env/config/gotten by viper.
// https://github.com/spf13/viper/pull/331
func getStringParamB(flags *pflag.FlagSet, key string) (string, bool) {
value, _ := flags.GetString(key)
// If set on Flags, use it.
if flags.Changed(key) {
return value, true
}
// If set through viper (env, config), return it.
if v.IsSet(key) {
return v.GetString(key), true
}
// Otherwise use default value on flags.
return value, false
}
func getStringParam(flags *pflag.FlagSet, key string) string {
val, _ := getStringParamB(flags, key)
return val
}
func setupLog(logMethod string) {
switch logMethod {
case "stdout":
@@ -401,7 +368,7 @@ func setupLog(logMethod string) {
}
}
func quickSetup(flags *pflag.FlagSet, d pythonData) error {
func quickSetup(d pythonData) error {
log.Println("Performing quick setup")
set := &settings.Settings{
@@ -415,7 +382,7 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
Scope: ".",
Locale: "en",
SingleClick: false,
AceEditorTheme: getStringParam(flags, "defaults.aceEditorTheme"),
AceEditorTheme: d.viper.GetString("defaults.aceEditorTheme"),
Perm: users.Permissions{
Admin: false,
Execute: true,
@@ -439,7 +406,7 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
}
var err error
if _, noauth := getStringParamB(flags, "noauth"); noauth {
if d.viper.GetBool("noauth") {
set.AuthMethod = auth.MethodNoAuth
err = d.store.Auth.Save(&auth.NoAuth{})
} else {
@@ -456,13 +423,18 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
}
ser := &settings.Server{
BaseURL: getStringParam(flags, "baseurl"),
Port: getStringParam(flags, "port"),
Log: getStringParam(flags, "log"),
TLSKey: getStringParam(flags, "key"),
TLSCert: getStringParam(flags, "cert"),
Address: getStringParam(flags, "address"),
Root: getStringParam(flags, "root"),
BaseURL: d.viper.GetString("baseURL"),
Port: d.viper.GetString("port"),
Log: d.viper.GetString("log"),
TLSKey: d.viper.GetString("key"),
TLSCert: d.viper.GetString("cert"),
Address: d.viper.GetString("address"),
Root: d.viper.GetString("root"),
TokenExpirationTime: d.viper.GetString("tokenExpirationTime"),
EnableThumbnails: !d.viper.GetBool("disableThumbnails"),
ResizePreview: !d.viper.GetBool("disablePreviewResize"),
EnableExec: !d.viper.GetBool("disableExec"),
TypeDetectionByHeader: !d.viper.GetBool("disableTypeDetectionByHeader"),
}
err = d.store.Settings.SaveServer(ser)
@@ -470,8 +442,8 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
return err
}
username := getStringParam(flags, "username")
password := getStringParam(flags, "password")
username := d.viper.GetString("username")
password := d.viper.GetString("password")
if password == "" {
var pwd string
@@ -504,32 +476,3 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) error {
return d.store.Users.Save(user)
}
func initConfig() {
if cfgFile == "" {
home, err := homedir.Dir()
if err != nil {
panic(err)
}
v.AddConfigPath(".")
v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/")
v.SetConfigName(".filebrowser")
} else {
v.SetConfigFile(cfgFile)
}
v.SetEnvPrefix("FB")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
if err := v.ReadInConfig(); err != nil {
var configParseError v.ConfigParseError
if errors.As(err, &configParseError) {
panic(err)
}
cfgFile = "No config file used"
} else {
cfgFile = "Using config file: " + v.ConfigFileUsed()
}
}
+3 -2
View File
@@ -69,11 +69,12 @@ func runRules(st *storage.Storage, cmd *cobra.Command, usersFn func(*users.User)
}
func getUserIdentifier(flags *pflag.FlagSet) (interface{}, error) {
id, err := getUint(flags, "id")
id, err := flags.GetUint("id")
if err != nil {
return nil, err
}
username, err := getString(flags, "username")
username, err := flags.GetString("username")
if err != nil {
return nil, err
}
+6 -2
View File
@@ -22,14 +22,18 @@ var rulesAddCmd = &cobra.Command{
Long: `Add a global rule or user rule.`,
Args: cobra.ExactArgs(1),
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
allow, err := getBool(cmd.Flags(), "allow")
flags := cmd.Flags()
allow, err := flags.GetBool("allow")
if err != nil {
return err
}
regex, err := getBool(cmd.Flags(), "regex")
regex, err := flags.GetBool("regex")
if err != nil {
return err
}
exp := args[0]
if regex {
+26 -24
View File
@@ -82,63 +82,64 @@ func addUserFlags(flags *pflag.FlagSet) {
flags.String("aceEditorTheme", "", "ace editor's syntax highlighting theme for users")
}
func getViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
viewModeStr, err := getString(flags, "viewMode")
func getAndParseViewMode(flags *pflag.FlagSet) (users.ViewMode, error) {
viewModeStr, err := flags.GetString("viewMode")
if err != nil {
return "", err
}
viewMode := users.ViewMode(viewModeStr)
if viewMode != users.ListViewMode && viewMode != users.MosaicViewMode {
return "", errors.New("view mode must be \"" + string(users.ListViewMode) + "\" or \"" + string(users.MosaicViewMode) + "\"")
}
return viewMode, nil
}
func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all bool) error {
var visitErr error
errs := []error{}
visit := func(flag *pflag.Flag) {
if visitErr != nil {
return
}
var err error
switch flag.Name {
case "scope":
defaults.Scope, err = getString(flags, flag.Name)
defaults.Scope, err = flags.GetString(flag.Name)
case "locale":
defaults.Locale, err = getString(flags, flag.Name)
defaults.Locale, err = flags.GetString(flag.Name)
case "viewMode":
defaults.ViewMode, err = getViewMode(flags)
defaults.ViewMode, err = getAndParseViewMode(flags)
case "singleClick":
defaults.SingleClick, err = getBool(flags, flag.Name)
defaults.SingleClick, err = flags.GetBool(flag.Name)
case "aceEditorTheme":
defaults.AceEditorTheme, err = getString(flags, flag.Name)
defaults.AceEditorTheme, err = flags.GetString(flag.Name)
case "perm.admin":
defaults.Perm.Admin, err = getBool(flags, flag.Name)
defaults.Perm.Admin, err = flags.GetBool(flag.Name)
case "perm.execute":
defaults.Perm.Execute, err = getBool(flags, flag.Name)
defaults.Perm.Execute, err = flags.GetBool(flag.Name)
case "perm.create":
defaults.Perm.Create, err = getBool(flags, flag.Name)
defaults.Perm.Create, err = flags.GetBool(flag.Name)
case "perm.rename":
defaults.Perm.Rename, err = getBool(flags, flag.Name)
defaults.Perm.Rename, err = flags.GetBool(flag.Name)
case "perm.modify":
defaults.Perm.Modify, err = getBool(flags, flag.Name)
defaults.Perm.Modify, err = flags.GetBool(flag.Name)
case "perm.delete":
defaults.Perm.Delete, err = getBool(flags, flag.Name)
defaults.Perm.Delete, err = flags.GetBool(flag.Name)
case "perm.share":
defaults.Perm.Share, err = getBool(flags, flag.Name)
defaults.Perm.Share, err = flags.GetBool(flag.Name)
case "perm.download":
defaults.Perm.Download, err = getBool(flags, flag.Name)
defaults.Perm.Download, err = flags.GetBool(flag.Name)
case "commands":
defaults.Commands, err = flags.GetStringSlice(flag.Name)
case "sorting.by":
defaults.Sorting.By, err = getString(flags, flag.Name)
defaults.Sorting.By, err = flags.GetString(flag.Name)
case "sorting.asc":
defaults.Sorting.Asc, err = getBool(flags, flag.Name)
defaults.Sorting.Asc, err = flags.GetBool(flag.Name)
case "hideDotfiles":
defaults.HideDotfiles, err = getBool(flags, flag.Name)
defaults.HideDotfiles, err = flags.GetBool(flag.Name)
}
if err != nil {
visitErr = err
errs = append(errs, err)
}
}
@@ -147,5 +148,6 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all
} else {
flags.Visit(visit)
}
return visitErr
return errors.Join(errs...)
}
+19 -21
View File
@@ -17,11 +17,12 @@ var usersAddCmd = &cobra.Command{
Long: `Create a new user and add it to the database.`,
Args: cobra.ExactArgs(2),
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
flags := cmd.Flags()
s, err := d.store.Settings.Get()
if err != nil {
return err
}
err = getUserDefaults(cmd.Flags(), &s.Defaults, false)
err = getUserDefaults(flags, &s.Defaults, false)
if err != nil {
return err
}
@@ -31,27 +32,24 @@ var usersAddCmd = &cobra.Command{
return err
}
lockPassword, err := getBool(cmd.Flags(), "lockPassword")
if err != nil {
return err
}
dateFormat, err := getBool(cmd.Flags(), "dateFormat")
if err != nil {
return err
}
hideDotfiles, err := getBool(cmd.Flags(), "hideDotfiles")
if err != nil {
return err
}
user := &users.User{
Username: args[0],
Password: password,
LockPassword: lockPassword,
DateFormat: dateFormat,
HideDotfiles: hideDotfiles,
Username: args[0],
Password: password,
}
user.LockPassword, err = flags.GetBool("lockPassword")
if err != nil {
return err
}
user.DateFormat, err = flags.GetBool("dateFormat")
if err != nil {
return err
}
user.HideDotfiles, err = flags.GetBool("hideDotfiles")
if err != nil {
return err
}
s.Defaults.Apply(user)
+3 -2
View File
@@ -26,6 +26,7 @@ installation. For that, just don't place their ID on the files
list or set it to 0.`,
Args: jsonYamlArg,
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
flags := cmd.Flags()
fd, err := os.Open(args[0])
if err != nil {
return err
@@ -45,7 +46,7 @@ list or set it to 0.`,
}
}
replace, err := getBool(cmd.Flags(), "replace")
replace, err := flags.GetBool("replace")
if err != nil {
return err
}
@@ -69,7 +70,7 @@ list or set it to 0.`,
}
}
overwrite, err := getBool(cmd.Flags(), "overwrite")
overwrite, err := flags.GetBool("overwrite")
if err != nil {
return err
}
+11 -8
View File
@@ -22,13 +22,14 @@ var usersUpdateCmd = &cobra.Command{
options you want to change.`,
Args: cobra.ExactArgs(1),
RunE: python(func(cmd *cobra.Command, args []string, d *pythonData) error {
username, id := parseUsernameOrID(args[0])
flags := cmd.Flags()
password, err := getString(flags, "password")
username, id := parseUsernameOrID(args[0])
password, err := flags.GetString("password")
if err != nil {
return err
}
newUsername, err := getString(flags, "username")
newUsername, err := flags.GetString("username")
if err != nil {
return err
}
@@ -41,13 +42,11 @@ options you want to change.`,
var (
user *users.User
)
if id != 0 {
user, err = d.store.Users.Get("", id)
} else {
user, err = d.store.Users.Get("", username)
}
if err != nil {
return err
}
@@ -61,10 +60,12 @@ options you want to change.`,
Sorting: user.Sorting,
Commands: user.Commands,
}
err = getUserDefaults(flags, &defaults, false)
if err != nil {
return err
}
user.Scope = defaults.Scope
user.Locale = defaults.Locale
user.ViewMode = defaults.ViewMode
@@ -72,15 +73,17 @@ options you want to change.`,
user.Perm = defaults.Perm
user.Commands = defaults.Commands
user.Sorting = defaults.Sorting
user.LockPassword, err = getBool(flags, "lockPassword")
user.LockPassword, err = flags.GetBool("lockPassword")
if err != nil {
return err
}
user.DateFormat, err = getBool(flags, "dateFormat")
user.DateFormat, err = flags.GetBool("dateFormat")
if err != nil {
return err
}
user.HideDotfiles, err = getBool(flags, "hideDotfiles")
user.HideDotfiles, err = flags.GetBool("hideDotfiles")
if err != nil {
return err
}
+111 -39
View File
@@ -12,8 +12,11 @@ import (
"strings"
"github.com/asdine/storm/v3"
homedir "github.com/mitchellh/go-homedir"
"github.com/samber/lo"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
yaml "gopkg.in/yaml.v3"
"github.com/filebrowser/filebrowser/v2/settings"
@@ -21,32 +24,21 @@ import (
"github.com/filebrowser/filebrowser/v2/storage/bolt"
)
const dbPerms = 0640
const databasePermissions = 0640
func getString(flags *pflag.FlagSet, flag string) (string, error) {
return flags.GetString(flag)
}
func getMode(flags *pflag.FlagSet, flag string) (fs.FileMode, error) {
s, err := getString(flags, flag)
func getAndParseFileMode(flags *pflag.FlagSet, name string) (fs.FileMode, error) {
mode, err := flags.GetString(name)
if err != nil {
return 0, err
}
b, err := strconv.ParseUint(s, 0, 32)
b, err := strconv.ParseUint(mode, 0, 32)
if err != nil {
return 0, err
}
return fs.FileMode(b), nil
}
func getBool(flags *pflag.FlagSet, flag string) (bool, error) {
return flags.GetBool(flag)
}
func getUint(flags *pflag.FlagSet, flag string) (uint, error) {
return flags.GetUint(flag)
}
func generateKey() []byte {
k, err := settings.GenerateKey()
if err != nil {
@@ -55,19 +47,6 @@ func generateKey() []byte {
return k
}
type cobraFunc func(cmd *cobra.Command, args []string) error
type pythonFunc func(cmd *cobra.Command, args []string, data *pythonData) error
type pythonConfig struct {
noDB bool
allowNoDB bool
}
type pythonData struct {
hadDB bool
store *storage.Storage
}
func dbExists(path string) (bool, error) {
stat, err := os.Stat(path)
if err == nil {
@@ -88,38 +67,131 @@ func dbExists(path string) (bool, error) {
return false, err
}
// Generate the replacements for all environment variables. This allows to
// use FB_BRANDING_DISABLE_EXTERNAL environment variables, even when the
// option name is branding.disableExternal.
func generateEnvKeyReplacements(cmd *cobra.Command) []string {
replacements := []string{}
cmd.Flags().VisitAll(func(f *pflag.Flag) {
oldName := strings.ToUpper(f.Name)
newName := strings.ToUpper(lo.SnakeCase(f.Name))
replacements = append(replacements, oldName, newName)
})
return replacements
}
func initViper(cmd *cobra.Command) (*viper.Viper, error) {
v := viper.New()
// Get config file from flag
cfgFile, err := cmd.Flags().GetString("config")
if err != nil {
return nil, err
}
// Configuration file
if cfgFile == "" {
home, err := homedir.Dir()
if err != nil {
return nil, err
}
v.AddConfigPath(".")
v.AddConfigPath(home)
v.AddConfigPath("/etc/filebrowser/")
v.SetConfigName(".filebrowser")
} else {
v.SetConfigFile(cfgFile)
}
// Environment variables
v.SetEnvPrefix("FB")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(generateEnvKeyReplacements(cmd)...))
// Bind the flags
err = v.BindPFlags(cmd.Flags())
if err != nil {
return nil, err
}
// Read in configuration
if err := v.ReadInConfig(); err != nil {
if errors.Is(err, viper.ConfigParseError{}) {
return nil, err
}
log.Println("No config file used")
} else {
log.Printf("Using config file: %s", v.ConfigFileUsed())
}
// Return Viper
return v, nil
}
type cobraFunc func(cmd *cobra.Command, args []string) error
type pythonFunc func(cmd *cobra.Command, args []string, data *pythonData) error
type pythonConfig struct {
expectsNoDatabase bool
allowsNoDatabase bool
}
type pythonData struct {
databaseExisted bool
viper *viper.Viper
store *storage.Storage
}
func python(fn pythonFunc, cfg pythonConfig) cobraFunc {
return func(cmd *cobra.Command, args []string) error {
data := &pythonData{hadDB: true}
v, err := initViper(cmd)
if err != nil {
return err
}
data := &pythonData{databaseExisted: true}
path := v.GetString("database")
// Only make the viper instance available to the root command (filebrowser).
// This is to make sure that we don't make the mistake of using it somewhere
// else.
if cmd.Name() == "filebrowser" {
data.viper = v
}
path := getStringParam(cmd.Flags(), "database")
absPath, err := filepath.Abs(path)
if err != nil {
panic(err)
return err
}
exists, err := dbExists(path)
exists, err := dbExists(path)
if err != nil {
panic(err)
} else if exists && cfg.noDB {
return err
} else if exists && cfg.expectsNoDatabase {
log.Fatal(absPath + " already exists")
} else if !exists && !cfg.noDB && !cfg.allowNoDB {
} else if !exists && !cfg.expectsNoDatabase && !cfg.allowsNoDatabase {
log.Fatal(absPath + " does not exist. Please run 'filebrowser config init' first.")
} else if !exists && !cfg.noDB {
} else if !exists && !cfg.expectsNoDatabase {
log.Println("Warning: filebrowser.db can't be found. Initialing in " + strings.TrimSuffix(absPath, "filebrowser.db"))
}
log.Println("Using database: " + absPath)
data.hadDB = exists
db, err := storm.Open(path, storm.BoltOptions(dbPerms, nil))
data.databaseExisted = exists
db, err := storm.Open(path, storm.BoltOptions(databasePermissions, nil))
if err != nil {
return err
}
defer db.Close()
data.store, err = bolt.NewStorage(db)
if err != nil {
return err
}
return fn(cmd, args, data)
}
}
@@ -5,4 +5,4 @@
"log": "stdout",
"database": "/database/filebrowser.db",
"root": "/srv"
}
}
+1
View File
@@ -16,6 +16,7 @@ require (
github.com/marusama/semaphore/v2 v2.5.0
github.com/mholt/archives v0.1.5
github.com/mitchellh/go-homedir v1.1.0
github.com/samber/lo v1.52.0
github.com/shirou/gopsutil/v4 v4.25.10
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.1
+2
View File
@@ -202,6 +202,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA=
github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM=
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
@@ -17,63 +17,60 @@ filebrowser config init [flags]
## Options
```
--aceEditorTheme string ace editor's syntax highlighting theme for users
-a, --address string address to listen on (default "127.0.0.1")
--auth.command string command for auth.method=hook
--auth.header string HTTP header for auth.method=proxy
--auth.method string authentication type (default "json")
-b, --baseurl string base url
--branding.color string set the theme color
--branding.disableExternal disable external links such as GitHub links
--branding.disableUsedPercentage disable used disk percentage graph
--branding.files string path to directory with images and custom styles
--branding.name string replace 'File Browser' by this name
--branding.theme string set the theme
--cache-dir string file cache directory (disabled if empty)
-t, --cert string tls certificate
--commands strings a list of the commands a user can execute
--create-user-dir generate user's home directory automatically
--dateFormat use date format (true for absolute time, false for relative)
--dir-mode string mode bits that new directories are created with (default "0o750")
--disable-exec disables Command Runner feature (default true)
--disable-preview-resize disable resize of image previews
--disable-thumbnails disable image thumbnails
--disable-type-detection-by-header disables type detection by reading file headers
--file-mode string mode bits that new files are created with (default "0o640")
-h, --help help for init
--hide-login-button hide login button from public pages
--hideDotfiles hide dotfiles
--img-processors int image processors count (default 4)
-k, --key string tls key
--locale string locale for users (default "en")
--lockPassword lock password
-l, --log string log output (default "stdout")
--minimum-password-length uint minimum password length for new users (default 12)
--perm.admin admin perm for users
--perm.create create perm for users (default true)
--perm.delete delete perm for users (default true)
--perm.download download perm for users (default true)
--perm.execute execute perm for users (default true)
--perm.modify modify perm for users (default true)
--perm.rename rename perm for users (default true)
--perm.share share perm for users (default true)
-p, --port string port to listen on (default "8080")
--recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com")
--recaptcha.key string ReCaptcha site key
--recaptcha.secret string ReCaptcha secret
-r, --root string root to prepend to relative paths (default ".")
--scope string scope for users (default ".")
--shell string shell command to which other commands should be appended
-s, --signup allow users to signup
--singleClick use single clicks only
--socket string socket to listen to (cannot be used with address, port, cert nor key flags)
--socket-perm uint32 unix socket file permissions (default 438)
--sorting.asc sorting by ascending order
--sorting.by string sorting mode (name, size or modified) (default "name")
--token-expiration-time string user session timeout (default "2h")
--tus.chunkSize uint the tus chunk size (default 10485760)
--tus.retryCount uint16 the tus retry count (default 5)
--viewMode string view mode for users (default "list")
--aceEditorTheme string ace editor's syntax highlighting theme for users
-a, --address string address to listen on (default "127.0.0.1")
--auth.command string command for auth.method=hook
--auth.header string HTTP header for auth.method=proxy
--auth.method string authentication type (default "json")
-b, --baseURL string base url
--branding.color string set the theme color
--branding.disableExternal disable external links such as GitHub links
--branding.disableUsedPercentage disable used disk percentage graph
--branding.files string path to directory with images and custom styles
--branding.name string replace 'File Browser' by this name
--branding.theme string set the theme
-t, --cert string tls certificate
--commands strings a list of the commands a user can execute
--createUserDir generate user's home directory automatically
--dateFormat use date format (true for absolute time, false for relative)
--dirMode string mode bits that new directories are created with (default "0o750")
--disableExec disables Command Runner feature (default true)
--disablePreviewResize disable resize of image previews
--disableThumbnails disable image thumbnails
--disableTypeDetectionByHeader disables type detection by reading file headers
--fileMode string mode bits that new files are created with (default "0o640")
-h, --help help for init
--hideDotfiles hide dotfiles
--hideLoginButton hide login button from public pages
-k, --key string tls key
--locale string locale for users (default "en")
--lockPassword lock password
-l, --log string log output (default "stdout")
--minimumPasswordLength uint minimum password length for new users (default 12)
--perm.admin admin perm for users
--perm.create create perm for users (default true)
--perm.delete delete perm for users (default true)
--perm.download download perm for users (default true)
--perm.execute execute perm for users (default true)
--perm.modify modify perm for users (default true)
--perm.rename rename perm for users (default true)
--perm.share share perm for users (default true)
-p, --port string port to listen on (default "8080")
--recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com")
--recaptcha.key string ReCaptcha site key
--recaptcha.secret string ReCaptcha secret
-r, --root string root to prepend to relative paths (default ".")
--scope string scope for users (default ".")
--shell string shell command to which other commands should be appended
-s, --signup allow users to signup
--singleClick use single clicks only
--socket string socket to listen to (cannot be used with address, port, cert nor key flags)
--sorting.asc sorting by ascending order
--sorting.by string sorting mode (name, size or modified) (default "name")
--tokenExpirationTime string user session timeout (default "2h")
--tus.chunkSize uint the tus chunk size (default 10485760)
--tus.retryCount uint16 the tus retry count (default 5)
--viewMode string view mode for users (default "list")
```
## Options inherited from parent commands
@@ -14,63 +14,60 @@ filebrowser config set [flags]
## Options
```
--aceEditorTheme string ace editor's syntax highlighting theme for users
-a, --address string address to listen on (default "127.0.0.1")
--auth.command string command for auth.method=hook
--auth.header string HTTP header for auth.method=proxy
--auth.method string authentication type (default "json")
-b, --baseurl string base url
--branding.color string set the theme color
--branding.disableExternal disable external links such as GitHub links
--branding.disableUsedPercentage disable used disk percentage graph
--branding.files string path to directory with images and custom styles
--branding.name string replace 'File Browser' by this name
--branding.theme string set the theme
--cache-dir string file cache directory (disabled if empty)
-t, --cert string tls certificate
--commands strings a list of the commands a user can execute
--create-user-dir generate user's home directory automatically
--dateFormat use date format (true for absolute time, false for relative)
--dir-mode string mode bits that new directories are created with (default "0o750")
--disable-exec disables Command Runner feature (default true)
--disable-preview-resize disable resize of image previews
--disable-thumbnails disable image thumbnails
--disable-type-detection-by-header disables type detection by reading file headers
--file-mode string mode bits that new files are created with (default "0o640")
-h, --help help for set
--hide-login-button hide login button from public pages
--hideDotfiles hide dotfiles
--img-processors int image processors count (default 4)
-k, --key string tls key
--locale string locale for users (default "en")
--lockPassword lock password
-l, --log string log output (default "stdout")
--minimum-password-length uint minimum password length for new users (default 12)
--perm.admin admin perm for users
--perm.create create perm for users (default true)
--perm.delete delete perm for users (default true)
--perm.download download perm for users (default true)
--perm.execute execute perm for users (default true)
--perm.modify modify perm for users (default true)
--perm.rename rename perm for users (default true)
--perm.share share perm for users (default true)
-p, --port string port to listen on (default "8080")
--recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com")
--recaptcha.key string ReCaptcha site key
--recaptcha.secret string ReCaptcha secret
-r, --root string root to prepend to relative paths (default ".")
--scope string scope for users (default ".")
--shell string shell command to which other commands should be appended
-s, --signup allow users to signup
--singleClick use single clicks only
--socket string socket to listen to (cannot be used with address, port, cert nor key flags)
--socket-perm uint32 unix socket file permissions (default 438)
--sorting.asc sorting by ascending order
--sorting.by string sorting mode (name, size or modified) (default "name")
--token-expiration-time string user session timeout (default "2h")
--tus.chunkSize uint the tus chunk size (default 10485760)
--tus.retryCount uint16 the tus retry count (default 5)
--viewMode string view mode for users (default "list")
--aceEditorTheme string ace editor's syntax highlighting theme for users
-a, --address string address to listen on (default "127.0.0.1")
--auth.command string command for auth.method=hook
--auth.header string HTTP header for auth.method=proxy
--auth.method string authentication type (default "json")
-b, --baseURL string base url
--branding.color string set the theme color
--branding.disableExternal disable external links such as GitHub links
--branding.disableUsedPercentage disable used disk percentage graph
--branding.files string path to directory with images and custom styles
--branding.name string replace 'File Browser' by this name
--branding.theme string set the theme
-t, --cert string tls certificate
--commands strings a list of the commands a user can execute
--createUserDir generate user's home directory automatically
--dateFormat use date format (true for absolute time, false for relative)
--dirMode string mode bits that new directories are created with (default "0o750")
--disableExec disables Command Runner feature (default true)
--disablePreviewResize disable resize of image previews
--disableThumbnails disable image thumbnails
--disableTypeDetectionByHeader disables type detection by reading file headers
--fileMode string mode bits that new files are created with (default "0o640")
-h, --help help for set
--hideDotfiles hide dotfiles
--hideLoginButton hide login button from public pages
-k, --key string tls key
--locale string locale for users (default "en")
--lockPassword lock password
-l, --log string log output (default "stdout")
--minimumPasswordLength uint minimum password length for new users (default 12)
--perm.admin admin perm for users
--perm.create create perm for users (default true)
--perm.delete delete perm for users (default true)
--perm.download download perm for users (default true)
--perm.execute execute perm for users (default true)
--perm.modify modify perm for users (default true)
--perm.rename rename perm for users (default true)
--perm.share share perm for users (default true)
-p, --port string port to listen on (default "8080")
--recaptcha.host string use another host for ReCAPTCHA. recaptcha.net might be useful in China (default "https://www.google.com")
--recaptcha.key string ReCaptcha site key
--recaptcha.secret string ReCaptcha secret
-r, --root string root to prepend to relative paths (default ".")
--scope string scope for users (default ".")
--shell string shell command to which other commands should be appended
-s, --signup allow users to signup
--singleClick use single clicks only
--socket string socket to listen to (cannot be used with address, port, cert nor key flags)
--sorting.asc sorting by ascending order
--sorting.by string sorting mode (name, size or modified) (default "name")
--tokenExpirationTime string user session timeout (default "2h")
--tus.chunkSize uint the tus chunk size (default 10485760)
--tus.retryCount uint16 the tus retry count (default 5)
--viewMode string view mode for users (default "list")
```
## Options inherited from parent commands
+34 -36
View File
@@ -13,12 +13,14 @@ it. Don't worry: you don't need to setup a separate database server.
We're using Bolt DB which is a single file database and all managed
by ourselves.
For this specific command, all the flags you have available (except
"config" for the configuration file), can be given either through
environment variables or configuration files.
For this command, all flags are available as environmental variables,
except for "--config", which specifies the configuration file to use.
The environment variables are prefixed by "FB_" followed by the flag name in
UPPER_SNAKE_CASE. For example, the flag "--disablePreviewResize" is available
as FB_DISABLE_PREVIEW_RESIZE.
If you don't set "config", it will look for a configuration file called
.filebrowser.{json, toml, yaml, yml} in the following directories:
If "--config" is not specified, File Browser will look for a configuration
file named .filebrowser.{json, toml, yaml, yml} in the following directories:
- ./
- $HOME/
@@ -26,15 +28,11 @@ If you don't set "config", it will look for a configuration file called
The precedence of the configuration values are as follows:
- flags
- environment variables
- configuration file
- database values
- defaults
The environment variables are prefixed by "FB_" followed by the option
name in caps. So to set "database" via an env variable, you should
set FB_DATABASE.
- Flags
- Environment variables
- Configuration file
- Database values
- Defaults
Also, if the database path doesn't exist, File Browser will enter into
the quick setup mode and a new database will be bootstrapped and a new
@@ -47,28 +45,28 @@ filebrowser [flags]
## Options
```
-a, --address string address to listen on (default "127.0.0.1")
-b, --baseurl string base url
--cache-dir string file cache directory (disabled if empty)
-t, --cert string tls certificate
-c, --config string config file path
-d, --database string database path (default "./filebrowser.db")
--disable-exec disables Command Runner feature (default true)
--disable-preview-resize disable resize of image previews
--disable-thumbnails disable image thumbnails
--disable-type-detection-by-header disables type detection by reading file headers
-h, --help help for filebrowser
--img-processors int image processors count (default 4)
-k, --key string tls key
-l, --log string log output (default "stdout")
--noauth use the noauth auther when using quick setup
--password string hashed password for the first user when using quick config
-p, --port string port to listen on (default "8080")
-r, --root string root to prepend to relative paths (default ".")
--socket string socket to listen to (cannot be used with address, port, cert nor key flags)
--socket-perm uint32 unix socket file permissions (default 438)
--token-expiration-time string user session timeout (default "2h")
--username string username for the first user when using quick config (default "admin")
-a, --address string address to listen on (default "127.0.0.1")
-b, --baseURL string base url
--cacheDir string file cache directory (disabled if empty)
-t, --cert string tls certificate
-c, --config string config file path
-d, --database string database path (default "./filebrowser.db")
--disableExec disables Command Runner feature (default true)
--disablePreviewResize disable resize of image previews
--disableThumbnails disable image thumbnails
--disableTypeDetectionByHeader disables type detection by reading file headers
-h, --help help for filebrowser
--imageProcessors int image processors count (default 4)
-k, --key string tls key
-l, --log string log output (default "stdout")
--noauth use the noauth auther when using quick setup
--password string hashed password for the first user when using quick setup
-p, --port string port to listen on (default "8080")
-r, --root string root to prepend to relative paths (default ".")
--socket string socket to listen to (cannot be used with address, port, cert nor key flags)
--socketPerm uint32 unix socket file permissions (default 438)
--tokenExpirationTime string user session timeout (default "2h")
--username string username for the first user when using quick setup (default "admin")
```
## See Also
+9
View File
@@ -0,0 +1,9 @@
# Troubleshooting
## Session Timeout
By default, user sessions expire after **2 hours**. If you're uploading large files over slower connections, you may need to increase this timeout to prevent sessions from expiring mid-upload. You can configure the session timeout using the `tokenExpirationTime` setting.
You can either set this option during runtime by using the flag `--tokenExpirationTime`, the environment variable `FB_TOKEN_EXPIRATION_TIME`, or in your configuration file. If you want to persist this to the configuration, please use [`filebrowser config set`](cli/filebrowser-config-set.md).
Valid duration formats include `"2h"`, `"30m"`, `"24h"`, or combinations like `"2h30m"`.
+1
View File
@@ -100,6 +100,7 @@ nav:
- customization.md
- authentication.md
- command-execution.md
- Troubleshooting: troubleshooting.md
- Deployment: deployment.md
- Command Line Usage:
- cli/filebrowser.md
+2 -2
View File
@@ -1,2 +1,2 @@
LINUX_VERSION-6.12 = .56
LINUX_KERNEL_HASH-6.12.56 = 55432b2af352f7bf3053c348d8549df2f2deeaa4a361c65d638c2f3b2ca7ec96
LINUX_VERSION-6.12 = .58
LINUX_KERNEL_HASH-6.12.58 = 5f1c4c546660a6a81046fdfa6195306bad2c8d17c0d69876dc100a85ad4613ac
+2 -2
View File
@@ -1,2 +1,2 @@
LINUX_VERSION-6.6 = .115
LINUX_KERNEL_HASH-6.6.115 = 0a98c05e8d0f6b49fad71b8d779410a0811ea5ae17d81744fe30718633fd9047
LINUX_VERSION-6.6 = .116
LINUX_KERNEL_HASH-6.6.116 = a9a59742c29be284c205dc87cbe9b065f9688488132c8f5a6057a5539230a51d
@@ -1,125 +0,0 @@
From 9989fcd49c52500a2bf1f6d49411690dec45d2dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marko=20M=C3=A4kel=C3=A4?= <marko.makela@iki.fi>
Date: Sat, 2 Aug 2025 12:47:08 +0300
Subject: [PATCH] clk: qcom: gcc-ipq6018: rework nss_port5 clock to multiple
conf
Rework nss_port5 to use the new multiple configuration implementation
and correctly fix the clocks for this port under some corner case.
In OpenWrt, this patch avoids intermittent dmesg errors of the form
nss_port5_rx_clk_src: rcg didn't update its configuration.
This is a mechanical, straightforward port of
commit e88f03230dc07aa3293b6aeb078bd27370bb2594
("clk: qcom: gcc-ipq8074: rework nss_port5/6 clock to multiple conf")
to gcc-ipq6018, with two conflicts resolved: different frequency of the
P_XO clock source, and only 5 Ethernet ports.
This was originally developed by JiaY-shi <shi05275@163.com>.
Link: https://lore.kernel.org/all/20231220221724.3822-4-ansuelsmth@gmail.com/
Signed-off-by: Marko Mäkelä <marko.makela@iki.fi>
Tested-by: Marko Mäkelä <marko.makela@iki.fi>
---
drivers/clk/qcom/gcc-ipq6018.c | 60 +++++++++++++++++++++-------------
1 file changed, 38 insertions(+), 22 deletions(-)
--- a/drivers/clk/qcom/gcc-ipq6018.c
+++ b/drivers/clk/qcom/gcc-ipq6018.c
@@ -511,15 +511,23 @@ static struct clk_rcg2 apss_ahb_clk_src
},
};
-static const struct freq_tbl ftbl_nss_port5_rx_clk_src[] = {
- F(24000000, P_XO, 1, 0, 0),
- F(25000000, P_UNIPHY1_RX, 12.5, 0, 0),
- F(25000000, P_UNIPHY0_RX, 5, 0, 0),
- F(78125000, P_UNIPHY1_RX, 4, 0, 0),
- F(125000000, P_UNIPHY1_RX, 2.5, 0, 0),
- F(125000000, P_UNIPHY0_RX, 1, 0, 0),
- F(156250000, P_UNIPHY1_RX, 2, 0, 0),
- F(312500000, P_UNIPHY1_RX, 1, 0, 0),
+static const struct freq_conf ftbl_nss_port5_rx_clk_src_25[] = {
+ C(P_UNIPHY1_RX, 12.5, 0, 0),
+ C(P_UNIPHY0_RX, 5, 0, 0),
+};
+
+static const struct freq_conf ftbl_nss_port5_rx_clk_src_125[] = {
+ C(P_UNIPHY1_RX, 2.5, 0, 0),
+ C(P_UNIPHY0_RX, 1, 0, 0),
+};
+
+static const struct freq_multi_tbl ftbl_nss_port5_rx_clk_src[] = {
+ FMS(24000000, P_XO, 1, 0, 0),
+ FM(25000000, ftbl_nss_port5_rx_clk_src_25),
+ FMS(78125000, P_UNIPHY1_RX, 4, 0, 0),
+ FM(125000000, ftbl_nss_port5_rx_clk_src_125),
+ FMS(156250000, P_UNIPHY1_RX, 2, 0, 0),
+ FMS(312500000, P_UNIPHY1_RX, 1, 0, 0),
{ }
};
@@ -547,26 +555,34 @@ gcc_xo_uniphy0_rx_tx_uniphy1_rx_tx_ubi32
static struct clk_rcg2 nss_port5_rx_clk_src = {
.cmd_rcgr = 0x68060,
- .freq_tbl = ftbl_nss_port5_rx_clk_src,
+ .freq_multi_tbl = ftbl_nss_port5_rx_clk_src,
.hid_width = 5,
.parent_map = gcc_xo_uniphy0_rx_tx_uniphy1_rx_tx_ubi32_bias_map,
.clkr.hw.init = &(struct clk_init_data){
.name = "nss_port5_rx_clk_src",
.parent_data = gcc_xo_uniphy0_rx_tx_uniphy1_rx_tx_ubi32_bias,
.num_parents = 7,
- .ops = &clk_rcg2_ops,
+ .ops = &clk_rcg2_fm_ops,
},
};
-static const struct freq_tbl ftbl_nss_port5_tx_clk_src[] = {
- F(24000000, P_XO, 1, 0, 0),
- F(25000000, P_UNIPHY1_TX, 12.5, 0, 0),
- F(25000000, P_UNIPHY0_TX, 5, 0, 0),
- F(78125000, P_UNIPHY1_TX, 4, 0, 0),
- F(125000000, P_UNIPHY1_TX, 2.5, 0, 0),
- F(125000000, P_UNIPHY0_TX, 1, 0, 0),
- F(156250000, P_UNIPHY1_TX, 2, 0, 0),
- F(312500000, P_UNIPHY1_TX, 1, 0, 0),
+static const struct freq_conf ftbl_nss_port5_tx_clk_src_25[] = {
+ C(P_UNIPHY1_TX, 12.5, 0, 0),
+ C(P_UNIPHY0_TX, 5, 0, 0),
+};
+
+static const struct freq_conf ftbl_nss_port5_tx_clk_src_125[] = {
+ C(P_UNIPHY1_TX, 2.5, 0, 0),
+ C(P_UNIPHY0_TX, 1, 0, 0),
+};
+
+static const struct freq_multi_tbl ftbl_nss_port5_tx_clk_src[] = {
+ FMS(24000000, P_XO, 1, 0, 0),
+ FM(25000000, ftbl_nss_port5_tx_clk_src_25),
+ FMS(78125000, P_UNIPHY1_TX, 4, 0, 0),
+ FM(125000000, ftbl_nss_port5_tx_clk_src_125),
+ FMS(156250000, P_UNIPHY1_TX, 2, 0, 0),
+ FMS(312500000, P_UNIPHY1_TX, 1, 0, 0),
{ }
};
@@ -594,14 +610,14 @@ gcc_xo_uniphy0_tx_rx_uniphy1_tx_rx_ubi32
static struct clk_rcg2 nss_port5_tx_clk_src = {
.cmd_rcgr = 0x68068,
- .freq_tbl = ftbl_nss_port5_tx_clk_src,
+ .freq_multi_tbl = ftbl_nss_port5_tx_clk_src,
.hid_width = 5,
.parent_map = gcc_xo_uniphy0_tx_rx_uniphy1_tx_rx_ubi32_bias_map,
.clkr.hw.init = &(struct clk_init_data){
.name = "nss_port5_tx_clk_src",
.parent_data = gcc_xo_uniphy0_tx_rx_uniphy1_tx_rx_ubi32_bias,
.num_parents = 7,
- .ops = &clk_rcg2_ops,
+ .ops = &clk_rcg2_fm_ops,
},
};
-28
View File
@@ -1,28 +0,0 @@
// Copyright (C) 2024 mieru authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package protocol
import "time"
const (
// periodicOutputInterval triggers periodic output of packet transport,
// even if there is no new data to send.
periodicOutputInterval = 1 * time.Millisecond
// backPressureDelay is a short sleep to add back pressure
// to the writer.
backPressureDelay = 100 * time.Microsecond
)
+9 -9
View File
@@ -54,6 +54,14 @@ const (
serverRespTimeout = 10 * time.Second
sessionHeartbeatInterval = 5 * time.Second
// periodicOutputInterval triggers periodic output of packet transport,
// even if there is no new data to send.
periodicOutputInterval = 1 * time.Millisecond
// backPressureDelay is a short sleep to add back pressure
// to the writer.
backPressureDelay = 100 * time.Microsecond
// Number of ack to trigger early retransmission.
earlyRetransmission = 3
// Maximum number of early retransmission attempt.
@@ -1236,15 +1244,7 @@ func (s *Session) sendWindowSize() int {
// receiveWindowSize determines how many more packets this session can receive.
func (s *Session) receiveWindowSize() int {
var underlayWaitingPackets int
if s.conn != nil {
packetUnderlay, ok := s.conn.(*PacketUnderlay)
if ok {
// Other packets sharing the same UDP socket reduce the congestion window.
underlayWaitingPackets = len(packetUnderlay.packetQueue)
}
}
return mathext.Max(0, int(s.cubicSendAlgorithm.CongestionWindowSize())-s.recvBuf.Len()-underlayWaitingPackets)
return mathext.Max(0, int(s.cubicSendAlgorithm.CongestionWindowSize())-s.recvBuf.Len())
}
func (s *Session) checkQuota(userName string) (ok bool, err error) {
+2 -2
View File
@@ -36,8 +36,8 @@ const (
sessionCleanInterval = 5 * time.Second
// Buffer received network packets before they are dropped by OS kernel.
packetChanCapacityClient = 4 * 1024
packetChanCapacityServer = 4 * 1024
packetChanCapacityClient = segmentTreeCapacity
packetChanCapacityServer = segmentTreeCapacity
)
// baseUnderlay contains a partial implementation of underlay.
+4 -1
View File
@@ -23,14 +23,17 @@ WORKDIR /test
COPY mihomo mieru mita httpserver sockshttpclient socksudpclient udpserver \
test/deploy/mihomo/mihomo-client-tcp.yaml \
test/deploy/mihomo/mihomo-client-tcp-no-wait.yaml \
test/deploy/mihomo/mihomo-client-udp.yaml \
test/deploy/mihomo/mihomo-client-udp-no-wait.yaml \
test/deploy/mihomo/mihomo-server.yaml \
test/deploy/mihomo/client_tcp_no_wait.json \
test/deploy/mihomo/client_tcp.json \
test/deploy/mihomo/client_udp_no_wait.json \
test/deploy/mihomo/client_udp.json \
test/deploy/mihomo/server_tcp.json \
test/deploy/mihomo/server_udp.json \
test/deploy/mihomo/libtest.sh \
test/deploy/mihomo/test_client_tcp.sh \
test/deploy/mihomo/test_client.sh \
test/deploy/mihomo/test_server.sh \
test/deploy/mihomo/test.sh /test/
+1 -1
View File
@@ -24,7 +24,7 @@
],
"activeProfile": "default",
"rpcPort": 8989,
"socks5Port": 1082,
"socks5Port": 1085,
"advancedSettings": {
"noCheckUpdate": true
},
@@ -25,7 +25,7 @@
],
"activeProfile": "default",
"rpcPort": 8989,
"socks5Port": 1082,
"socks5Port": 1085,
"advancedSettings": {
"noCheckUpdate": true
},
@@ -0,0 +1,19 @@
dns:
enable: true
nameserver:
- 8.8.8.8
log-level: warning
mixed-port: 1084
mode: rule
proxies:
- name: mieru
type: mieru
server: 127.0.0.1
port-range: 8964-8965
transport: UDP
udp: true
username: baozi
password: manlianpenfen
handshake-mode: HANDSHAKE_NO_WAIT
rules:
- MATCH,mieru
@@ -0,0 +1,18 @@
dns:
enable: true
nameserver:
- 8.8.8.8
log-level: warning
mixed-port: 1083
mode: rule
proxies:
- name: mieru
type: mieru
server: 127.0.0.1
port-range: 8964-8965
transport: UDP
udp: true
username: baozi
password: manlianpenfen
rules:
- MATCH,mieru
+28
View File
@@ -0,0 +1,28 @@
{
"portBindings": [
{
"portRange": "8964-8965",
"protocol": "UDP"
},
{
"port": 9648,
"protocol": "UDP"
},
{
"port": 6489,
"protocol": "UDP"
},
{
"port": 4896,
"protocol": "UDP"
}
],
"users": [
{
"name": "baozi",
"password": "manlianpenfen",
"allowLoopbackIP": true
}
],
"loggingLevel": "INFO"
}
+4 -4
View File
@@ -40,10 +40,10 @@ echo "========== BEGIN OF SERVER TEST =========="
./test_server.sh
echo "========== END OF SERVER TEST =========="
# Run client TCP test.
echo "========== BEGIN OF CLIENT TCP TEST =========="
./test_client_tcp.sh
echo "========== END OF CLIENT TCP TEST =========="
# Run client test.
echo "========== BEGIN OF CLIENT TEST =========="
./test_client.sh
echo "========== END OF CLIENT TEST =========="
echo "Test is successful."
sleep 1
@@ -21,28 +21,6 @@
# Load test library.
source ./libtest.sh
# Update mieru server with TCP config.
./mita apply config server_tcp.json
if [[ "$?" -ne 0 ]]; then
echo "command 'mita apply config server_tcp.json' failed"
exit 1
fi
echo "mieru server config:"
./mita describe config
# Start mieru server proxy.
./mita start
if [[ "$?" -ne 0 ]]; then
echo "command 'mita start' failed"
exit 1
fi
# Start mihomo.
./mihomo -f mihomo-client-tcp.yaml &
sleep 1
./mihomo -f mihomo-client-tcp-no-wait.yaml &
sleep 1
function run_tcp_tests() {
local port="$1"
local suffix="${2:-}"
@@ -58,17 +36,6 @@ function run_tcp_tests() {
exit 1
fi
sleep 1
echo ">>> http - new connections - TCP ${suffix} <<<"
./sockshttpclient -proxy_mode=http -dst_host=127.0.0.1 -dst_port=8080 \
-local_http_host=127.0.0.1 -local_http_port=${port} \
-test_case=new_conn -num_request=1000
if [ "$?" -ne "0" ]; then
print_mieru_server_thread_dump
echo "TCP - test HTTP new_conn ${suffix} failed."
exit 1
fi
sleep 1
echo ">>> socks5 - reuse one connection - TCP ${suffix} <<<"
./sockshttpclient -dst_host=127.0.0.1 -dst_port=8080 \
@@ -84,7 +51,7 @@ function run_tcp_tests() {
echo ">>> socks5 UDP associate - TCP ${suffix} <<<"
./socksudpclient -dst_host=127.0.0.1 -dst_port=9090 \
-local_proxy_host=127.0.0.1 -local_proxy_port=${port} \
-interval_ms=10 -num_request=100 -num_conn=60
-interval_ms=10 -num_request=100 -num_conn=30
if [ "$?" -ne "0" ]; then
print_mieru_server_thread_dump
echo "TCP - test socks5 udp_associate ${suffix} failed."
@@ -92,18 +59,96 @@ function run_tcp_tests() {
fi
}
# Start testing.
function run_udp_tests() {
local port="$1"
local suffix="${2:-}"
sleep 1
echo ">>> socks5 - new connections - UDP ${suffix} <<<"
./sockshttpclient -dst_host=127.0.0.1 -dst_port=8080 \
-local_proxy_host=127.0.0.1 -local_proxy_port=${port} \
-test_case=new_conn -num_request=3000
if [ "$?" -ne "0" ]; then
print_mieru_server_thread_dump
echo "UDP - test socks5 new_conn ${suffix} failed."
exit 1
fi
sleep 1
echo ">>> socks5 - reuse one connection - UDP ${suffix} <<<"
./sockshttpclient -dst_host=127.0.0.1 -dst_port=8080 \
-local_proxy_host=127.0.0.1 -local_proxy_port=${port} \
-test_case=reuse_conn -test_time_sec=30
if [ "$?" -ne "0" ]; then
print_mieru_server_thread_dump
echo "UDP - test socks5 reuse_conn ${suffix} failed."
exit 1
fi
sleep 1
echo ">>> socks5 UDP associate - UDP ${suffix} <<<"
./socksudpclient -dst_host=127.0.0.1 -dst_port=9090 \
-local_proxy_host=127.0.0.1 -local_proxy_port=${port} \
-interval_ms=10 -num_request=100 -num_conn=30
if [ "$?" -ne "0" ]; then
print_mieru_server_thread_dump
echo "UDP - test socks5 udp_associate ${suffix} failed."
exit 1
fi
}
echo "========== BEGIN OF CLIENT TCP TEST =========="
./mita apply config server_tcp.json
if [[ "$?" -ne 0 ]]; then
echo "command 'mita apply config server_tcp.json' failed"
exit 1
fi
echo "mieru server config:"
./mita describe config
./mita start
if [[ "$?" -ne 0 ]]; then
echo "command 'mita start' failed"
exit 1
fi
./mihomo -f mihomo-client-tcp.yaml &
sleep 1
./mihomo -f mihomo-client-tcp-no-wait.yaml &
sleep 1
run_tcp_tests 1080
run_tcp_tests 1081 "(handshake no wait)"
# Print metrics and memory statistics.
print_mieru_server_metrics
sleep 1
# Stop mieru server proxy.
./mita stop
if [[ "$?" -ne 0 ]]; then
echo "command 'mita stop' failed"
exit 1
fi
sleep 1
echo "========== END OF CLIENT TCP TEST =========="
# echo "========== BEGIN OF CLIENT UDP TEST =========="
# ./mita apply config server_udp.json
# if [[ "$?" -ne 0 ]]; then
# echo "command 'mita apply config server_udp.json' failed"
# exit 1
# fi
# echo "mieru server config:"
# ./mita describe config
# ./mita start
# if [[ "$?" -ne 0 ]]; then
# echo "command 'mita start' failed"
# exit 1
# fi
# ./mihomo -f mihomo-client-udp.yaml &
# sleep 1
# ./mihomo -f mihomo-client-udp-no-wait.yaml &
# sleep 1
# run_udp_tests 1083
# run_udp_tests 1084 "(handshake no wait)"
# print_mieru_server_metrics
# sleep 1
# ./mita stop
# if [[ "$?" -ne 0 ]]; then
# echo "command 'mita stop' failed"
# exit 1
# fi
# echo "========== END OF CLIENT UDP TEST =========="
+18 -12
View File
@@ -23,10 +23,12 @@ source ./libtest.sh
function run_new_conn_test() {
local config="$1"
local port="$2"
sleep 1
echo ">>> socks5 - new connections with mihomo server - $config <<<"
./sockshttpclient -dst_host=127.0.0.1 -dst_port=8080 \
-local_proxy_host=127.0.0.1 -local_proxy_port=1082 \
-local_proxy_host=127.0.0.1 -local_proxy_port=${port} \
-test_case=new_conn -num_request=3000
if [ "$?" -ne "0" ]; then
print_mieru_client_log
@@ -37,10 +39,12 @@ function run_new_conn_test() {
function run_udp_associate_test() {
local config="$1"
local port="$2"
sleep 1
echo ">>> socks5 UDP associate - with mihomo server - $config <<<"
./socksudpclient -dst_host=127.0.0.1 -dst_port=9090 \
-local_proxy_host=127.0.0.1 -local_proxy_port=1082 \
-local_proxy_host=127.0.0.1 -local_proxy_port=${port} \
-interval_ms=10 -num_request=100 -num_conn=30
if [ "$?" -ne "0" ]; then
print_mieru_client_log
@@ -50,7 +54,8 @@ function run_udp_associate_test() {
}
function test_mieru_with_config() {
config="$1"
local config="$1"
local port="$2"
# Update mieru client with TCP config.
./mieru apply config $config
@@ -69,8 +74,8 @@ function test_mieru_with_config() {
fi
# Start testing.
run_new_conn_test "$config"
run_udp_associate_test "$config"
run_new_conn_test "${config}" "${port}"
run_udp_associate_test "${config}" "${port}"
# Stop mieru client.
./mieru stop
@@ -87,11 +92,12 @@ function test_mieru_with_config() {
./mihomo -f mihomo-server.yaml &
sleep 1
test_mieru_with_config client_tcp.json
test_mieru_with_config client_tcp_no_wait.json
test_mieru_with_config client_udp.json
test_mieru_with_config client_udp_no_wait.json
echo "========== BEGIN OF SERVER TCP TEST =========="
test_mieru_with_config client_tcp.json 1082
test_mieru_with_config client_tcp_no_wait.json 1082
echo "========== END OF SERVER TCP TEST =========="
echo "Test is successful."
sleep 1
exit 0
echo "========== BEGIN OF SERVER UDP TEST =========="
test_mieru_with_config client_udp.json 1085
test_mieru_with_config client_udp_no_wait.json 1085
echo "========== END OF SERVER UDP TEST =========="
+2 -2
View File
@@ -5,12 +5,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=filebrowser
PKG_VERSION:=2.47.0
PKG_VERSION:=2.48.1
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/filebrowser/filebrowser/tar.gz/v${PKG_VERSION}?
PKG_HASH:=f90b2a9981545570006c0984aa39ee7178f4efd71d1ddf660c8661ea2f9bfbd6
PKG_HASH:=556b8092c69b65c11d917c8d1e0fef418ea88a9d437c2ec7b1cab506973eb743
PKG_LICENSE:=Apache-2.0
PKG_LICENSE_FILES:=LICENSE
-43
View File
@@ -1,43 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-only
#
# Copyright (C) 2020 jerryk <jerrykuku@qq.com>
# Copyright (C) 2021 ImmortalWrt.org
include $(TOPDIR)/rules.mk
PKG_NAME:=lua-maxminddb
PKG_VERSION:=0.2
PKG_RELEASE:=2
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/fabled/lua-maxminddb.git
PKG_SOURCE_DATE:=2019-03-14
PKG_SOURCE_VERSION:=93da9f4e6c814c3a23044dd2cdd22d4a6b4f665b
PKG_MIRROR_HASH:=bebf4fbb25c33013ca1e09b8d1a50ee9ae5ce1c810d3335b285f9bf2d1f316b2
PKG_LICENSE:=MIT
PKG_LICENSE_FILES:=LICENSE
PKG_BUILD_PARALLEL:=1
include $(INCLUDE_DIR)/package.mk
define Package/lua-maxminddb
SUBMENU:=Lua
SECTION:=lang
CATEGORY:=Languages
TITLE:=libmaxminddb bindings for lua
URL:=https://github.com/fabled/lua-maxminddb
DEPENDS:=+lua +libmaxminddb
endef
TARGET_CFLAGS += $(FPIC)
MAKE_VARS += LUA_PKG=lua
define Package/lua-maxminddb/install
$(INSTALL_DIR) $(1)/usr/lib/lua
$(INSTALL_BIN) $(PKG_BUILD_DIR)/maxminddb.so $(1)/usr/lib/lua/
endef
$(eval $(call BuildPackage,lua-maxminddb))
+1 -1
View File
@@ -16,7 +16,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-amlogic
PKG_VERSION:=3.1.276
PKG_VERSION:=3.1.278
PKG_RELEASE:=1
PKG_LICENSE:=GPL-2.0 License
@@ -76,7 +76,6 @@ local known_tags = {
local ophub_extra_tags = {
kernel_flippy = "kernel_flippy [Mainline Stable Kernel by Flippy]",
kernel_h6 = "kernel_h6 [Allwinner H6 Kernel]",
kernel_dev = "kernel_dev [Development Kernel]",
kernel_beta = "kernel_beta [Beta Kernel]",
}
-- Conditionally add the extra tags to the list.
@@ -413,9 +413,6 @@ msgstr "kernel_flippy [Flippy 的主线稳定内核]"
msgid "kernel_h6 [Allwinner H6 Kernel]"
msgstr "kernel_h6 [Allwinner H6 内核]"
msgid "kernel_dev [Development Kernel]"
msgstr "kernel_dev [开发版内核]"
msgid "kernel_beta [Beta Kernel]"
msgstr "kernel_beta [测试版内核]"
@@ -338,7 +338,7 @@ o.remove = function(self, section)
local new_val = (v.type == "Xray") and "xray" or "sing-box"
m:set(section, self.option, new_val)
local dns_field = s.fields[new_val .. "_dns_mode"]
local dns_field = s.fields[v.type == "Xray" and "xray_dns_mode" or "singbox_dns_mode"]
local v2ray_dns_mode = dns_field and dns_field:formvalue(section)
if v2ray_dns_mode then
m:set(section, "v2ray_dns_mode", v2ray_dns_mode)
@@ -350,6 +350,8 @@ o.remove = function(self, section)
end
o = s:option(ListValue, "xray_dns_mode", translate("Request protocol"))
o.default = "tcp"
o:value("udp", "UDP")
o:value("tcp", "TCP")
o:value("tcp+doh", "TCP + DoH (" .. translate("A/AAAA type") .. ")")
o:depends("dns_mode", "xray")
@@ -363,6 +365,8 @@ o.write = function(self, section, value)
end
o = s:option(ListValue, "singbox_dns_mode", translate("Request protocol"))
o.default = "tcp"
o:value("udp", "UDP")
o:value("tcp", "TCP")
o:value("doh", "DoH")
o:depends("dns_mode", "sing-box")
@@ -387,53 +391,53 @@ o:value("149.112.112.112", "149.112.112.112 (Quad9-Recommended)")
o:value("208.67.220.220", "208.67.220.220 (OpenDNS)")
o:value("208.67.222.222", "208.67.222.222 (OpenDNS)")
o:depends({dns_mode = "dns2socks"})
o:depends({xray_dns_mode = "udp"})
o:depends({xray_dns_mode = "tcp"})
o:depends({xray_dns_mode = "tcp+doh"})
o:depends({singbox_dns_mode = "udp"})
o:depends({singbox_dns_mode = "tcp"})
if has_singbox or has_xray then
o = s:option(Value, "remote_dns_doh", translate("Remote DNS DoH"))
o:value("https://1.1.1.1/dns-query", "CloudFlare")
o:value("https://1.1.1.2/dns-query", "CloudFlare-Security")
o:value("https://8.8.4.4/dns-query", "Google 8844")
o:value("https://8.8.8.8/dns-query", "Google 8888")
o:value("https://9.9.9.9/dns-query", "Quad9-Recommended 9.9.9.9")
o:value("https://149.112.112.112/dns-query", "Quad9-Recommended 149.112.112.112")
o:value("https://208.67.222.222/dns-query", "OpenDNS")
o:value("https://dns.adguard.com/dns-query,176.103.130.130", "AdGuard")
o:value("https://doh.libredns.gr/dns-query,116.202.176.26", "LibreDNS")
o:value("https://doh.libredns.gr/ads,116.202.176.26", "LibreDNS (No Ads)")
o.default = "https://1.1.1.1/dns-query"
o.validate = function(self, value, t)
if value ~= "" then
value = api.trim(value)
local flag = 0
local util = require "luci.util"
local val = util.split(value, ",")
local url = val[1]
val[1] = nil
for i = 1, #val do
local v = val[i]
if v then
if not api.datatypes.ipmask4(v) then
flag = 1
end
o = s:option(Value, "remote_dns_doh", translate("Remote DNS DoH"))
o:value("https://1.1.1.1/dns-query", "1.1.1.1 (CloudFlare)")
o:value("https://1.1.1.2/dns-query", "1.1.1.2 (CloudFlare-Security)")
o:value("https://8.8.4.4/dns-query", "8.8.4.4 (Google)")
o:value("https://8.8.8.8/dns-query", "8.8.8.8 (Google)")
o:value("https://9.9.9.9/dns-query", "9.9.9.9 (Quad9)")
o:value("https://149.112.112.112/dns-query", "149.112.112.112 (Quad9)")
o:value("https://208.67.222.222/dns-query", "208.67.222.222 (OpenDNS)")
o:value("https://dns.adguard.com/dns-query,94.140.14.14", "94.140.14.14 (AdGuard)")
o:value("https://doh.libredns.gr/dns-query,116.202.176.26", "116.202.176.26 (LibreDNS)")
o:value("https://doh.libredns.gr/ads,116.202.176.26", "116.202.176.26 (LibreDNS-NoAds)")
o.default = "https://1.1.1.1/dns-query"
o.validate = function(self, value, t)
if value ~= "" then
value = api.trim(value)
local flag = 0
local util = require "luci.util"
local val = util.split(value, ",")
local url = val[1]
val[1] = nil
for i = 1, #val do
local v = val[i]
if v then
if not api.datatypes.ipmask4(v) then
flag = 1
end
end
if flag == 0 then
return value
end
end
return nil, translate("DoH request address") .. " " .. translate("Format must be:") .. " URL,IP"
if flag == 0 then
return value
end
end
o:depends({xray_dns_mode = "tcp+doh"})
o:depends({singbox_dns_mode = "doh"})
o = s:option(Value, "remote_dns_client_ip", translate("EDNS Client Subnet"))
o.datatype = "ipaddr"
o:depends({dns_mode = "sing-box"})
o:depends({dns_mode = "xray"})
return nil, translate("DoH request address") .. " " .. translate("Format must be:") .. " URL,IP"
end
o:depends({xray_dns_mode = "tcp+doh"})
o:depends({singbox_dns_mode = "doh"})
o = s:option(Value, "remote_dns_client_ip", translate("EDNS Client Subnet"))
o.datatype = "ipaddr"
o:depends({dns_mode = "sing-box"})
o:depends({dns_mode = "xray"})
o = s:option(ListValue, "chinadns_ng_default_tag", translate("Default DNS"))
o.default = "none"
@@ -432,6 +432,8 @@ if api.is_finded("smartdns") then
end
o = s:taboption("DNS", ListValue, "xray_dns_mode", translate("Remote DNS") .. " " .. translate("Request protocol"))
o.default = "tcp"
o:value("udp", "UDP")
o:value("tcp", "TCP")
o:value("tcp+doh", "TCP + DoH (" .. translate("A/AAAA type") .. ")")
o:depends("dns_mode", "xray")
@@ -446,6 +448,8 @@ o.write = function(self, section, value)
end
o = s:taboption("DNS", ListValue, "singbox_dns_mode", translate("Remote DNS") .. " " .. translate("Request protocol"))
o.default = "tcp"
o:value("udp", "UDP")
o:value("tcp", "TCP")
o:value("doh", "DoH")
o:depends("dns_mode", "sing-box")
@@ -485,8 +489,10 @@ o:value("208.67.222.222", "208.67.222.222 (OpenDNS)")
o:depends({dns_mode = "dns2socks"})
o:depends({dns_mode = "tcp"})
o:depends({dns_mode = "udp"})
o:depends({xray_dns_mode = "udp"})
o:depends({xray_dns_mode = "tcp"})
o:depends({xray_dns_mode = "tcp+doh"})
o:depends({singbox_dns_mode = "udp"})
o:depends({singbox_dns_mode = "tcp"})
---- DoH
@@ -1544,8 +1544,7 @@ function gen_config(var)
}
if remote_dns_udp_server then
local server_port = tonumber(remote_dns_port) or 53
remote_server.address = "udp://" .. remote_dns_udp_server .. ":" .. server_port
remote_server.address = remote_dns_udp_server
end
if remote_dns_tcp_server then
@@ -1597,9 +1596,9 @@ function gen_config(var)
if remote_dns_udp_server then
local server_port = tonumber(remote_dns_port) or 53
remote_server.type = "udp"
remote_server.server = remote_dns_udp_server
remote_server.server = remote_dns_server
remote_server.server_port = server_port
tmp_address = remote_dns_udp_server
tmp_address = remote_dns_server
end
if remote_dns_tcp_server then
@@ -580,6 +580,8 @@ function gen_config(var)
local direct_dns_udp_server = var["-direct_dns_udp_server"]
local direct_dns_tcp_server = var["-direct_dns_tcp_server"]
local direct_dns_query_strategy = var["-direct_dns_query_strategy"]
local remote_dns_udp_server = var["-remote_dns_udp_server"]
local remote_dns_udp_port = var["-remote_dns_udp_port"]
local remote_dns_tcp_server = var["-remote_dns_tcp_server"]
local remote_dns_tcp_port = var["-remote_dns_tcp_port"]
local remote_dns_doh_url = var["-remote_dns_doh_url"]
@@ -1175,7 +1177,7 @@ function gen_config(var)
end
end
if remote_dns_tcp_server and remote_dns_tcp_port then
if (remote_dns_udp_server and remote_dns_udp_port) or (remote_dns_tcp_server and remote_dns_tcp_port) then
if not routing then
routing = {
domainStrategy = "IPOnDemand",
@@ -1230,8 +1232,13 @@ function gen_config(var)
local _remote_dns = {
--tag = "dns-global-remote",
queryStrategy = (remote_dns_query_strategy and remote_dns_query_strategy ~= "") and remote_dns_query_strategy or "UseIPv4",
address = "tcp://" .. remote_dns_tcp_server .. ":" .. tonumber(remote_dns_tcp_port) or 53
}
if remote_dns_udp_server then
_remote_dns.address = remote_dns_udp_server
_remote_dns.port = tonumber(remote_dns_udp_port) or 53
else
address = "tcp://" .. remote_dns_tcp_server .. ":" .. tonumber(remote_dns_tcp_port) or 53
end
local _remote_dns_host
if remote_dns_doh_url and remote_dns_doh_host then
@@ -1309,8 +1316,8 @@ function gen_config(var)
protocol = "dokodemo-door",
tag = "dns-in",
settings = {
address = remote_dns_tcp_server,
port = tonumber(remote_dns_tcp_port),
address = remote_dns_udp_server or remote_dns_tcp_server,
port = tonumber(remote_dns_udp_port) or tonumber(remote_dns_tcp_port),
network = "tcp,udp"
}
})
@@ -1322,9 +1329,9 @@ function gen_config(var)
tag = dns_outbound_tag
} or nil,
settings = {
address = remote_dns_tcp_server,
port = tonumber(remote_dns_tcp_port),
network = "tcp",
address = remote_dns_udp_server or remote_dns_tcp_server,
port = tonumber(remote_dns_udp_port) or tonumber(remote_dns_tcp_port),
network = remote_dns_udp_server and "udp" or "tcp",
nonIPQuery = "drop"
}
})
@@ -486,11 +486,12 @@ run_singbox() {
[ -n "$remote_dns_query_strategy" ] && _extra_param="${_extra_param} -remote_dns_query_strategy ${remote_dns_query_strategy}"
case "$remote_dns_protocol" in
tcp)
local _dns=$(get_first_dns remote_dns_tcp_server 53 | sed 's/#/:/g')
udp|tcp)
local _proto="$remote_dns_protocol"
local _dns=$(get_first_dns remote_dns_${_proto}_server 53 | sed 's/#/:/g')
local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}')
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}')
_extra_param="${_extra_param} -remote_dns_server ${_dns_address} -remote_dns_port ${_dns_port} -remote_dns_tcp_server tcp://${_dns}"
_extra_param="${_extra_param} -remote_dns_server ${_dns_address} -remote_dns_port ${_dns_port} -remote_dns_${_proto}_server ${_proto}://${_dns}"
;;
doh)
local _doh_url _doh_host _doh_port _doh_bootstrap
@@ -508,7 +509,7 @@ run_singbox() {
run_xray() {
local flag type node tcp_redir_port tcp_proxy_way udp_redir_port socks_address socks_port socks_username socks_password http_address http_port http_username http_password
local dns_listen_port direct_dns_query_strategy direct_dns_port direct_dns_udp_server direct_dns_tcp_server remote_dns_udp_server remote_dns_tcp_server remote_dns_doh remote_dns_client_ip remote_fakedns remote_dns_query_strategy dns_cache dns_socks_address dns_socks_port
local dns_listen_port direct_dns_query_strategy direct_dns_port direct_dns_udp_server direct_dns_tcp_server remote_dns_protocol remote_dns_udp_server remote_dns_tcp_server remote_dns_doh remote_dns_client_ip remote_fakedns remote_dns_query_strategy dns_cache dns_socks_address dns_socks_port
local loglevel log_file config_file server_host server_port no_run
local _extra_param=""
eval_set_val $@
@@ -557,18 +558,27 @@ run_xray() {
[ -n "$remote_dns_client_ip" ] && _extra_param="${_extra_param} -remote_dns_client_ip ${remote_dns_client_ip}"
[ "$remote_fakedns" = "1" ] && _extra_param="${_extra_param} -remote_dns_fake 1"
[ -n "$dns_cache" ] && _extra_param="${_extra_param} -dns_cache ${dns_cache}"
[ -n "${remote_dns_tcp_server}" ] && {
local _dns=$(get_first_dns remote_dns_tcp_server 53 | sed 's/#/:/g')
local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}')
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}')
_extra_param="${_extra_param} -remote_dns_tcp_server ${_dns_address} -remote_dns_tcp_port ${_dns_port}"
}
[ -n "${remote_dns_doh}" ] && {
local _doh_url _doh_host _doh_port _doh_bootstrap
parse_doh "$remote_dns_doh" _doh_url _doh_host _doh_port _doh_bootstrap
[ -n "$_doh_bootstrap" ] && _extra_param="${_extra_param} -remote_dns_doh_ip ${_doh_bootstrap}"
_extra_param="${_extra_param} -remote_dns_doh_port ${_doh_port} -remote_dns_doh_url ${_doh_url} -remote_dns_doh_host ${_doh_host}"
}
case "$remote_dns_protocol" in
udp)
local _dns=$(get_first_dns remote_dns_udp_server 53 | sed 's/#/:/g')
local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}')
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}')
_extra_param="${_extra_param} -remote_dns_udp_server ${_dns_address} -remote_dns_udp_port ${_dns_port}"
;;
tcp|tcp+doh)
local _dns=$(get_first_dns remote_dns_tcp_server 53 | sed 's/#/:/g')
local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}')
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}')
_extra_param="${_extra_param} -remote_dns_tcp_server ${_dns_address} -remote_dns_tcp_port ${_dns_port}"
[ "$remote_dns_protocol" = "tcp+doh" ] && {
local _doh_url _doh_host _doh_port _doh_bootstrap
parse_doh "$remote_dns_doh" _doh_url _doh_host _doh_port _doh_bootstrap
[ -n "$_doh_bootstrap" ] && _extra_param="${_extra_param} -remote_dns_doh_ip ${_doh_bootstrap}"
_extra_param="${_extra_param} -remote_dns_doh_port ${_doh_port} -remote_dns_doh_url ${_doh_url} -remote_dns_doh_host ${_doh_host}"
}
;;
esac
_extra_param="${_extra_param} -loglevel $loglevel"
[ -n "$no_run" ] && _extra_param="${_extra_param} -no_run 1"
lua $UTIL_XRAY gen_config ${_extra_param} > $config_file
@@ -963,9 +973,10 @@ run_redir() {
_args="${_args} remote_dns_protocol=${v2ray_dns_mode}"
case "$v2ray_dns_mode" in
tcp)
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}"
resolve_dns_log="Sing-Box DNS(127.0.0.1#${resolve_dns_port}) -> tcp://${REMOTE_DNS}"
udp|tcp)
local _proto="$v2ray_dns_mode"
_args="${_args} remote_dns_${_proto}_server=${REMOTE_DNS}"
resolve_dns_log="Sing-Box DNS(127.0.0.1#${resolve_dns_port}) -> ${_proto}://${REMOTE_DNS}"
;;
doh)
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
@@ -1041,14 +1052,23 @@ run_redir() {
;;
esac
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}"
if [ "$v2ray_dns_mode" = "tcp+doh" ]; then
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
_args="${_args} remote_dns_doh=${remote_dns_doh}"
resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> (${remote_dns_doh})(A/AAAA) + tcp://${REMOTE_DNS}"
else
resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> tcp://${REMOTE_DNS}"
fi
_args="${_args} remote_dns_protocol=${v2ray_dns_mode}"
case "$v2ray_dns_mode" in
udp)
_args="${_args} remote_dns_udp_server=${REMOTE_DNS}"
resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> udp://${REMOTE_DNS}"
;;
tcp|tcp+doh)
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}"
if [ "$v2ray_dns_mode" = "tcp+doh" ]; then
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
_args="${_args} remote_dns_doh=${remote_dns_doh}"
resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> (${remote_dns_doh})(A/AAAA) + tcp://${REMOTE_DNS}"
else
resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> tcp://${REMOTE_DNS}"
fi
;;
esac
local remote_fakedns=$(config_t_get global remote_fakedns 0)
[ "${remote_fakedns}" = "1" ] && {
fakedns=1
@@ -1498,9 +1518,10 @@ start_dns() {
_args="${_args} dns_listen_port=${NEXT_DNS_LISTEN_PORT}"
_args="${_args} remote_dns_protocol=${v2ray_dns_mode}"
case "$v2ray_dns_mode" in
tcp)
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}"
echolog " - Sing-Box DNS(${TUN_DNS}) -> tcp://${REMOTE_DNS}"
udp|tcp)
local _proto="$v2ray_dns_mode"
_args="${_args} remote_dns_${_proto}_server=${REMOTE_DNS}"
echolog " - Sing-Box DNS(${TUN_DNS}) -> ${_proto}://${REMOTE_DNS}"
;;
doh)
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
@@ -1531,19 +1552,27 @@ start_dns() {
[ -n "${_remote_dns_client_ip}" ] && _args="${_args} remote_dns_client_ip=${_remote_dns_client_ip}"
TCP_PROXY_DNS=1
_args="${_args} dns_listen_port=${NEXT_DNS_LISTEN_PORT}"
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}"
local v2ray_dns_mode=$(config_t_get global v2ray_dns_mode tcp)
if [ "$v2ray_dns_mode" = "tcp+doh" ]; then
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
_args="${_args} remote_dns_doh=${remote_dns_doh}"
echolog " - Xray DNS(${TUN_DNS}) -> (${remote_dns_doh})(A/AAAA) + tcp://${REMOTE_DNS}"
case "$v2ray_dns_mode" in
udp)
_args="${_args} remote_dns_udp_server=${REMOTE_DNS}"
echolog " - Xray DNS(${TUN_DNS}) -> udp://${REMOTE_DNS}"
;;
tcp|tcp+doh)
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}"
local v2ray_dns_mode=$(config_t_get global v2ray_dns_mode tcp)
if [ "$v2ray_dns_mode" = "tcp+doh" ]; then
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
_args="${_args} remote_dns_doh=${remote_dns_doh}"
echolog " - Xray DNS(${TUN_DNS}) -> (${remote_dns_doh})(A/AAAA) + tcp://${REMOTE_DNS}"
local _doh_url _doh_host _doh_port _doh_bootstrap
parse_doh "$remote_dns_doh" _doh_url _doh_host _doh_port _doh_bootstrap
[ -n "${_doh_bootstrap}" ] && REMOTE_DNS="${REMOTE_DNS},${_doh_bootstrap}#${_doh_port}"
else
echolog " - Xray DNS(${TUN_DNS}) -> tcp://${REMOTE_DNS}"
fi
local _doh_url _doh_host _doh_port _doh_bootstrap
parse_doh "$remote_dns_doh" _doh_url _doh_host _doh_port _doh_bootstrap
[ -n "${_doh_bootstrap}" ] && REMOTE_DNS="${REMOTE_DNS},${_doh_bootstrap}#${_doh_port}"
else
echolog " - Xray DNS(${TUN_DNS}) -> tcp://${REMOTE_DNS}"
fi
;;
esac
_args="${_args} dns_socks_address=127.0.0.1 dns_socks_port=${tcp_node_socks_port}"
run_xray ${_args}
}
@@ -1849,7 +1878,7 @@ acl_app() {
dnsmasq_filter_proxy_ipv6=0
remote_dns_query_strategy="UseIP"
[ "$filter_proxy_ipv6" = "1" ] && remote_dns_query_strategy="UseIPv4"
run_${type} flag=acl_${sid} type=$dns_mode dns_socks_address=127.0.0.1 dns_socks_port=$socks_port dns_listen_port=${_dns_port} remote_dns_protocol=${v2ray_dns_mode} remote_dns_tcp_server=${remote_dns} remote_dns_doh="${remote_dns_doh}" remote_dns_query_strategy=${remote_dns_query_strategy} remote_dns_client_ip=${remote_dns_client_ip} config_file=$config_file
run_${type} flag=acl_${sid} type=$dns_mode dns_socks_address=127.0.0.1 dns_socks_port=$socks_port dns_listen_port=${_dns_port} remote_dns_protocol=${v2ray_dns_mode} remote_dns_udp_server=${remote_dns} remote_dns_tcp_server=${remote_dns} remote_dns_doh="${remote_dns_doh}" remote_dns_query_strategy=${remote_dns_query_strategy} remote_dns_client_ip=${remote_dns_client_ip} config_file=$config_file
fi
set_cache_var "node_${tcp_node}_$(echo -n "${remote_dns}" | md5sum | cut -d " " -f1)" "${_dns_port}"
}
@@ -1944,7 +1973,7 @@ acl_app() {
remote_dns_query_strategy="UseIP"
[ "$filter_proxy_ipv6" = "1" ] && remote_dns_query_strategy="UseIPv4"
[ "$dns_mode" = "xray" ] && [ "$v2ray_dns_mode" = "tcp+doh" ] && remote_dns_doh=${remote_dns_doh:-https://1.1.1.1/dns-query}
_extra_param="dns_listen_port=${_dns_port} remote_dns_protocol=${v2ray_dns_mode} remote_dns_tcp_server=${remote_dns} remote_dns_doh=${remote_dns_doh} remote_dns_query_strategy=${remote_dns_query_strategy} remote_dns_client_ip=${remote_dns_client_ip}"
_extra_param="dns_listen_port=${_dns_port} remote_dns_protocol=${v2ray_dns_mode} remote_dns_udp_server=${remote_dns} remote_dns_tcp_server=${remote_dns} remote_dns_doh=${remote_dns_doh} remote_dns_query_strategy=${remote_dns_query_strategy} remote_dns_client_ip=${remote_dns_client_ip}"
fi
[ -n "$udp_node" ] && ([ "$udp_node" = "tcp" ] || [ "$udp_node" = "$tcp_node" ]) && {
config_file="${config_file//TCP_/TCP_UDP_}"
@@ -338,7 +338,7 @@ o.remove = function(self, section)
local new_val = (v.type == "Xray") and "xray" or "sing-box"
m:set(section, self.option, new_val)
local dns_field = s.fields[new_val .. "_dns_mode"]
local dns_field = s.fields[v.type == "Xray" and "xray_dns_mode" or "singbox_dns_mode"]
local v2ray_dns_mode = dns_field and dns_field:formvalue(section)
if v2ray_dns_mode then
m:set(section, "v2ray_dns_mode", v2ray_dns_mode)
@@ -350,6 +350,8 @@ o.remove = function(self, section)
end
o = s:option(ListValue, "xray_dns_mode", translate("Request protocol"))
o.default = "tcp"
o:value("udp", "UDP")
o:value("tcp", "TCP")
o:value("tcp+doh", "TCP + DoH (" .. translate("A/AAAA type") .. ")")
o:depends("dns_mode", "xray")
@@ -363,6 +365,8 @@ o.write = function(self, section, value)
end
o = s:option(ListValue, "singbox_dns_mode", translate("Request protocol"))
o.default = "tcp"
o:value("udp", "UDP")
o:value("tcp", "TCP")
o:value("doh", "DoH")
o:depends("dns_mode", "sing-box")
@@ -387,53 +391,53 @@ o:value("149.112.112.112", "149.112.112.112 (Quad9-Recommended)")
o:value("208.67.220.220", "208.67.220.220 (OpenDNS)")
o:value("208.67.222.222", "208.67.222.222 (OpenDNS)")
o:depends({dns_mode = "dns2socks"})
o:depends({xray_dns_mode = "udp"})
o:depends({xray_dns_mode = "tcp"})
o:depends({xray_dns_mode = "tcp+doh"})
o:depends({singbox_dns_mode = "udp"})
o:depends({singbox_dns_mode = "tcp"})
if has_singbox or has_xray then
o = s:option(Value, "remote_dns_doh", translate("Remote DNS DoH"))
o:value("https://1.1.1.1/dns-query", "CloudFlare")
o:value("https://1.1.1.2/dns-query", "CloudFlare-Security")
o:value("https://8.8.4.4/dns-query", "Google 8844")
o:value("https://8.8.8.8/dns-query", "Google 8888")
o:value("https://9.9.9.9/dns-query", "Quad9-Recommended 9.9.9.9")
o:value("https://149.112.112.112/dns-query", "Quad9-Recommended 149.112.112.112")
o:value("https://208.67.222.222/dns-query", "OpenDNS")
o:value("https://dns.adguard.com/dns-query,176.103.130.130", "AdGuard")
o:value("https://doh.libredns.gr/dns-query,116.202.176.26", "LibreDNS")
o:value("https://doh.libredns.gr/ads,116.202.176.26", "LibreDNS (No Ads)")
o.default = "https://1.1.1.1/dns-query"
o.validate = function(self, value, t)
if value ~= "" then
value = api.trim(value)
local flag = 0
local util = require "luci.util"
local val = util.split(value, ",")
local url = val[1]
val[1] = nil
for i = 1, #val do
local v = val[i]
if v then
if not api.datatypes.ipmask4(v) then
flag = 1
end
o = s:option(Value, "remote_dns_doh", translate("Remote DNS DoH"))
o:value("https://1.1.1.1/dns-query", "1.1.1.1 (CloudFlare)")
o:value("https://1.1.1.2/dns-query", "1.1.1.2 (CloudFlare-Security)")
o:value("https://8.8.4.4/dns-query", "8.8.4.4 (Google)")
o:value("https://8.8.8.8/dns-query", "8.8.8.8 (Google)")
o:value("https://9.9.9.9/dns-query", "9.9.9.9 (Quad9)")
o:value("https://149.112.112.112/dns-query", "149.112.112.112 (Quad9)")
o:value("https://208.67.222.222/dns-query", "208.67.222.222 (OpenDNS)")
o:value("https://dns.adguard.com/dns-query,94.140.14.14", "94.140.14.14 (AdGuard)")
o:value("https://doh.libredns.gr/dns-query,116.202.176.26", "116.202.176.26 (LibreDNS)")
o:value("https://doh.libredns.gr/ads,116.202.176.26", "116.202.176.26 (LibreDNS-NoAds)")
o.default = "https://1.1.1.1/dns-query"
o.validate = function(self, value, t)
if value ~= "" then
value = api.trim(value)
local flag = 0
local util = require "luci.util"
local val = util.split(value, ",")
local url = val[1]
val[1] = nil
for i = 1, #val do
local v = val[i]
if v then
if not api.datatypes.ipmask4(v) then
flag = 1
end
end
if flag == 0 then
return value
end
end
return nil, translate("DoH request address") .. " " .. translate("Format must be:") .. " URL,IP"
if flag == 0 then
return value
end
end
o:depends({xray_dns_mode = "tcp+doh"})
o:depends({singbox_dns_mode = "doh"})
o = s:option(Value, "remote_dns_client_ip", translate("EDNS Client Subnet"))
o.datatype = "ipaddr"
o:depends({dns_mode = "sing-box"})
o:depends({dns_mode = "xray"})
return nil, translate("DoH request address") .. " " .. translate("Format must be:") .. " URL,IP"
end
o:depends({xray_dns_mode = "tcp+doh"})
o:depends({singbox_dns_mode = "doh"})
o = s:option(Value, "remote_dns_client_ip", translate("EDNS Client Subnet"))
o.datatype = "ipaddr"
o:depends({dns_mode = "sing-box"})
o:depends({dns_mode = "xray"})
o = s:option(ListValue, "chinadns_ng_default_tag", translate("Default DNS"))
o.default = "none"
@@ -432,6 +432,8 @@ if api.is_finded("smartdns") then
end
o = s:taboption("DNS", ListValue, "xray_dns_mode", translate("Remote DNS") .. " " .. translate("Request protocol"))
o.default = "tcp"
o:value("udp", "UDP")
o:value("tcp", "TCP")
o:value("tcp+doh", "TCP + DoH (" .. translate("A/AAAA type") .. ")")
o:depends("dns_mode", "xray")
@@ -446,6 +448,8 @@ o.write = function(self, section, value)
end
o = s:taboption("DNS", ListValue, "singbox_dns_mode", translate("Remote DNS") .. " " .. translate("Request protocol"))
o.default = "tcp"
o:value("udp", "UDP")
o:value("tcp", "TCP")
o:value("doh", "DoH")
o:depends("dns_mode", "sing-box")
@@ -485,8 +489,10 @@ o:value("208.67.222.222", "208.67.222.222 (OpenDNS)")
o:depends({dns_mode = "dns2socks"})
o:depends({dns_mode = "tcp"})
o:depends({dns_mode = "udp"})
o:depends({xray_dns_mode = "udp"})
o:depends({xray_dns_mode = "tcp"})
o:depends({xray_dns_mode = "tcp+doh"})
o:depends({singbox_dns_mode = "udp"})
o:depends({singbox_dns_mode = "tcp"})
---- DoH
@@ -1544,8 +1544,7 @@ function gen_config(var)
}
if remote_dns_udp_server then
local server_port = tonumber(remote_dns_port) or 53
remote_server.address = "udp://" .. remote_dns_udp_server .. ":" .. server_port
remote_server.address = remote_dns_udp_server
end
if remote_dns_tcp_server then
@@ -1597,9 +1596,9 @@ function gen_config(var)
if remote_dns_udp_server then
local server_port = tonumber(remote_dns_port) or 53
remote_server.type = "udp"
remote_server.server = remote_dns_udp_server
remote_server.server = remote_dns_server
remote_server.server_port = server_port
tmp_address = remote_dns_udp_server
tmp_address = remote_dns_server
end
if remote_dns_tcp_server then
@@ -580,6 +580,8 @@ function gen_config(var)
local direct_dns_udp_server = var["-direct_dns_udp_server"]
local direct_dns_tcp_server = var["-direct_dns_tcp_server"]
local direct_dns_query_strategy = var["-direct_dns_query_strategy"]
local remote_dns_udp_server = var["-remote_dns_udp_server"]
local remote_dns_udp_port = var["-remote_dns_udp_port"]
local remote_dns_tcp_server = var["-remote_dns_tcp_server"]
local remote_dns_tcp_port = var["-remote_dns_tcp_port"]
local remote_dns_doh_url = var["-remote_dns_doh_url"]
@@ -1175,7 +1177,7 @@ function gen_config(var)
end
end
if remote_dns_tcp_server and remote_dns_tcp_port then
if (remote_dns_udp_server and remote_dns_udp_port) or (remote_dns_tcp_server and remote_dns_tcp_port) then
if not routing then
routing = {
domainStrategy = "IPOnDemand",
@@ -1230,8 +1232,13 @@ function gen_config(var)
local _remote_dns = {
--tag = "dns-global-remote",
queryStrategy = (remote_dns_query_strategy and remote_dns_query_strategy ~= "") and remote_dns_query_strategy or "UseIPv4",
address = "tcp://" .. remote_dns_tcp_server .. ":" .. tonumber(remote_dns_tcp_port) or 53
}
if remote_dns_udp_server then
_remote_dns.address = remote_dns_udp_server
_remote_dns.port = tonumber(remote_dns_udp_port) or 53
else
address = "tcp://" .. remote_dns_tcp_server .. ":" .. tonumber(remote_dns_tcp_port) or 53
end
local _remote_dns_host
if remote_dns_doh_url and remote_dns_doh_host then
@@ -1309,8 +1316,8 @@ function gen_config(var)
protocol = "dokodemo-door",
tag = "dns-in",
settings = {
address = remote_dns_tcp_server,
port = tonumber(remote_dns_tcp_port),
address = remote_dns_udp_server or remote_dns_tcp_server,
port = tonumber(remote_dns_udp_port) or tonumber(remote_dns_tcp_port),
network = "tcp,udp"
}
})
@@ -1322,9 +1329,9 @@ function gen_config(var)
tag = dns_outbound_tag
} or nil,
settings = {
address = remote_dns_tcp_server,
port = tonumber(remote_dns_tcp_port),
network = "tcp",
address = remote_dns_udp_server or remote_dns_tcp_server,
port = tonumber(remote_dns_udp_port) or tonumber(remote_dns_tcp_port),
network = remote_dns_udp_server and "udp" or "tcp",
nonIPQuery = "drop"
}
})
@@ -486,11 +486,12 @@ run_singbox() {
[ -n "$remote_dns_query_strategy" ] && _extra_param="${_extra_param} -remote_dns_query_strategy ${remote_dns_query_strategy}"
case "$remote_dns_protocol" in
tcp)
local _dns=$(get_first_dns remote_dns_tcp_server 53 | sed 's/#/:/g')
udp|tcp)
local _proto="$remote_dns_protocol"
local _dns=$(get_first_dns remote_dns_${_proto}_server 53 | sed 's/#/:/g')
local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}')
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}')
_extra_param="${_extra_param} -remote_dns_server ${_dns_address} -remote_dns_port ${_dns_port} -remote_dns_tcp_server tcp://${_dns}"
_extra_param="${_extra_param} -remote_dns_server ${_dns_address} -remote_dns_port ${_dns_port} -remote_dns_${_proto}_server ${_proto}://${_dns}"
;;
doh)
local _doh_url _doh_host _doh_port _doh_bootstrap
@@ -508,7 +509,7 @@ run_singbox() {
run_xray() {
local flag type node tcp_redir_port tcp_proxy_way udp_redir_port socks_address socks_port socks_username socks_password http_address http_port http_username http_password
local dns_listen_port direct_dns_query_strategy direct_dns_port direct_dns_udp_server direct_dns_tcp_server remote_dns_udp_server remote_dns_tcp_server remote_dns_doh remote_dns_client_ip remote_fakedns remote_dns_query_strategy dns_cache dns_socks_address dns_socks_port
local dns_listen_port direct_dns_query_strategy direct_dns_port direct_dns_udp_server direct_dns_tcp_server remote_dns_protocol remote_dns_udp_server remote_dns_tcp_server remote_dns_doh remote_dns_client_ip remote_fakedns remote_dns_query_strategy dns_cache dns_socks_address dns_socks_port
local loglevel log_file config_file server_host server_port no_run
local _extra_param=""
eval_set_val $@
@@ -557,18 +558,27 @@ run_xray() {
[ -n "$remote_dns_client_ip" ] && _extra_param="${_extra_param} -remote_dns_client_ip ${remote_dns_client_ip}"
[ "$remote_fakedns" = "1" ] && _extra_param="${_extra_param} -remote_dns_fake 1"
[ -n "$dns_cache" ] && _extra_param="${_extra_param} -dns_cache ${dns_cache}"
[ -n "${remote_dns_tcp_server}" ] && {
local _dns=$(get_first_dns remote_dns_tcp_server 53 | sed 's/#/:/g')
local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}')
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}')
_extra_param="${_extra_param} -remote_dns_tcp_server ${_dns_address} -remote_dns_tcp_port ${_dns_port}"
}
[ -n "${remote_dns_doh}" ] && {
local _doh_url _doh_host _doh_port _doh_bootstrap
parse_doh "$remote_dns_doh" _doh_url _doh_host _doh_port _doh_bootstrap
[ -n "$_doh_bootstrap" ] && _extra_param="${_extra_param} -remote_dns_doh_ip ${_doh_bootstrap}"
_extra_param="${_extra_param} -remote_dns_doh_port ${_doh_port} -remote_dns_doh_url ${_doh_url} -remote_dns_doh_host ${_doh_host}"
}
case "$remote_dns_protocol" in
udp)
local _dns=$(get_first_dns remote_dns_udp_server 53 | sed 's/#/:/g')
local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}')
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}')
_extra_param="${_extra_param} -remote_dns_udp_server ${_dns_address} -remote_dns_udp_port ${_dns_port}"
;;
tcp|tcp+doh)
local _dns=$(get_first_dns remote_dns_tcp_server 53 | sed 's/#/:/g')
local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}')
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}')
_extra_param="${_extra_param} -remote_dns_tcp_server ${_dns_address} -remote_dns_tcp_port ${_dns_port}"
[ "$remote_dns_protocol" = "tcp+doh" ] && {
local _doh_url _doh_host _doh_port _doh_bootstrap
parse_doh "$remote_dns_doh" _doh_url _doh_host _doh_port _doh_bootstrap
[ -n "$_doh_bootstrap" ] && _extra_param="${_extra_param} -remote_dns_doh_ip ${_doh_bootstrap}"
_extra_param="${_extra_param} -remote_dns_doh_port ${_doh_port} -remote_dns_doh_url ${_doh_url} -remote_dns_doh_host ${_doh_host}"
}
;;
esac
_extra_param="${_extra_param} -loglevel $loglevel"
[ -n "$no_run" ] && _extra_param="${_extra_param} -no_run 1"
lua $UTIL_XRAY gen_config ${_extra_param} > $config_file
@@ -963,9 +973,10 @@ run_redir() {
_args="${_args} remote_dns_protocol=${v2ray_dns_mode}"
case "$v2ray_dns_mode" in
tcp)
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}"
resolve_dns_log="Sing-Box DNS(127.0.0.1#${resolve_dns_port}) -> tcp://${REMOTE_DNS}"
udp|tcp)
local _proto="$v2ray_dns_mode"
_args="${_args} remote_dns_${_proto}_server=${REMOTE_DNS}"
resolve_dns_log="Sing-Box DNS(127.0.0.1#${resolve_dns_port}) -> ${_proto}://${REMOTE_DNS}"
;;
doh)
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
@@ -1041,14 +1052,23 @@ run_redir() {
;;
esac
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}"
if [ "$v2ray_dns_mode" = "tcp+doh" ]; then
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
_args="${_args} remote_dns_doh=${remote_dns_doh}"
resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> (${remote_dns_doh})(A/AAAA) + tcp://${REMOTE_DNS}"
else
resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> tcp://${REMOTE_DNS}"
fi
_args="${_args} remote_dns_protocol=${v2ray_dns_mode}"
case "$v2ray_dns_mode" in
udp)
_args="${_args} remote_dns_udp_server=${REMOTE_DNS}"
resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> udp://${REMOTE_DNS}"
;;
tcp|tcp+doh)
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}"
if [ "$v2ray_dns_mode" = "tcp+doh" ]; then
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
_args="${_args} remote_dns_doh=${remote_dns_doh}"
resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> (${remote_dns_doh})(A/AAAA) + tcp://${REMOTE_DNS}"
else
resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> tcp://${REMOTE_DNS}"
fi
;;
esac
local remote_fakedns=$(config_t_get global remote_fakedns 0)
[ "${remote_fakedns}" = "1" ] && {
fakedns=1
@@ -1498,9 +1518,10 @@ start_dns() {
_args="${_args} dns_listen_port=${NEXT_DNS_LISTEN_PORT}"
_args="${_args} remote_dns_protocol=${v2ray_dns_mode}"
case "$v2ray_dns_mode" in
tcp)
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}"
echolog " - Sing-Box DNS(${TUN_DNS}) -> tcp://${REMOTE_DNS}"
udp|tcp)
local _proto="$v2ray_dns_mode"
_args="${_args} remote_dns_${_proto}_server=${REMOTE_DNS}"
echolog " - Sing-Box DNS(${TUN_DNS}) -> ${_proto}://${REMOTE_DNS}"
;;
doh)
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
@@ -1531,19 +1552,27 @@ start_dns() {
[ -n "${_remote_dns_client_ip}" ] && _args="${_args} remote_dns_client_ip=${_remote_dns_client_ip}"
TCP_PROXY_DNS=1
_args="${_args} dns_listen_port=${NEXT_DNS_LISTEN_PORT}"
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}"
local v2ray_dns_mode=$(config_t_get global v2ray_dns_mode tcp)
if [ "$v2ray_dns_mode" = "tcp+doh" ]; then
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
_args="${_args} remote_dns_doh=${remote_dns_doh}"
echolog " - Xray DNS(${TUN_DNS}) -> (${remote_dns_doh})(A/AAAA) + tcp://${REMOTE_DNS}"
case "$v2ray_dns_mode" in
udp)
_args="${_args} remote_dns_udp_server=${REMOTE_DNS}"
echolog " - Xray DNS(${TUN_DNS}) -> udp://${REMOTE_DNS}"
;;
tcp|tcp+doh)
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}"
local v2ray_dns_mode=$(config_t_get global v2ray_dns_mode tcp)
if [ "$v2ray_dns_mode" = "tcp+doh" ]; then
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
_args="${_args} remote_dns_doh=${remote_dns_doh}"
echolog " - Xray DNS(${TUN_DNS}) -> (${remote_dns_doh})(A/AAAA) + tcp://${REMOTE_DNS}"
local _doh_url _doh_host _doh_port _doh_bootstrap
parse_doh "$remote_dns_doh" _doh_url _doh_host _doh_port _doh_bootstrap
[ -n "${_doh_bootstrap}" ] && REMOTE_DNS="${REMOTE_DNS},${_doh_bootstrap}#${_doh_port}"
else
echolog " - Xray DNS(${TUN_DNS}) -> tcp://${REMOTE_DNS}"
fi
local _doh_url _doh_host _doh_port _doh_bootstrap
parse_doh "$remote_dns_doh" _doh_url _doh_host _doh_port _doh_bootstrap
[ -n "${_doh_bootstrap}" ] && REMOTE_DNS="${REMOTE_DNS},${_doh_bootstrap}#${_doh_port}"
else
echolog " - Xray DNS(${TUN_DNS}) -> tcp://${REMOTE_DNS}"
fi
;;
esac
_args="${_args} dns_socks_address=127.0.0.1 dns_socks_port=${tcp_node_socks_port}"
run_xray ${_args}
}
@@ -1849,7 +1878,7 @@ acl_app() {
dnsmasq_filter_proxy_ipv6=0
remote_dns_query_strategy="UseIP"
[ "$filter_proxy_ipv6" = "1" ] && remote_dns_query_strategy="UseIPv4"
run_${type} flag=acl_${sid} type=$dns_mode dns_socks_address=127.0.0.1 dns_socks_port=$socks_port dns_listen_port=${_dns_port} remote_dns_protocol=${v2ray_dns_mode} remote_dns_tcp_server=${remote_dns} remote_dns_doh="${remote_dns_doh}" remote_dns_query_strategy=${remote_dns_query_strategy} remote_dns_client_ip=${remote_dns_client_ip} config_file=$config_file
run_${type} flag=acl_${sid} type=$dns_mode dns_socks_address=127.0.0.1 dns_socks_port=$socks_port dns_listen_port=${_dns_port} remote_dns_protocol=${v2ray_dns_mode} remote_dns_udp_server=${remote_dns} remote_dns_tcp_server=${remote_dns} remote_dns_doh="${remote_dns_doh}" remote_dns_query_strategy=${remote_dns_query_strategy} remote_dns_client_ip=${remote_dns_client_ip} config_file=$config_file
fi
set_cache_var "node_${tcp_node}_$(echo -n "${remote_dns}" | md5sum | cut -d " " -f1)" "${_dns_port}"
}
@@ -1944,7 +1973,7 @@ acl_app() {
remote_dns_query_strategy="UseIP"
[ "$filter_proxy_ipv6" = "1" ] && remote_dns_query_strategy="UseIPv4"
[ "$dns_mode" = "xray" ] && [ "$v2ray_dns_mode" = "tcp+doh" ] && remote_dns_doh=${remote_dns_doh:-https://1.1.1.1/dns-query}
_extra_param="dns_listen_port=${_dns_port} remote_dns_protocol=${v2ray_dns_mode} remote_dns_tcp_server=${remote_dns} remote_dns_doh=${remote_dns_doh} remote_dns_query_strategy=${remote_dns_query_strategy} remote_dns_client_ip=${remote_dns_client_ip}"
_extra_param="dns_listen_port=${_dns_port} remote_dns_protocol=${v2ray_dns_mode} remote_dns_udp_server=${remote_dns} remote_dns_tcp_server=${remote_dns} remote_dns_doh=${remote_dns_doh} remote_dns_query_strategy=${remote_dns_query_strategy} remote_dns_client_ip=${remote_dns_client_ip}"
fi
[ -n "$udp_node" ] && ([ "$udp_node" = "tcp" ] || [ "$udp_node" = "$tcp_node" ]) && {
config_file="${config_file//TCP_/TCP_UDP_}"
+2 -2
View File
@@ -30,13 +30,13 @@ define Download/geosite
HASH:=c41cd987bdd3dda6df67bbf15790713c833e59c619e289e7673bcec178db90c0
endef
GEOSITE_IRAN_VER:=202511100042
GEOSITE_IRAN_VER:=202511170041
GEOSITE_IRAN_FILE:=iran.dat.$(GEOSITE_IRAN_VER)
define Download/geosite-ir
URL:=https://github.com/bootmortis/iran-hosted-domains/releases/download/$(GEOSITE_IRAN_VER)/
URL_FILE:=iran.dat
FILE:=$(GEOSITE_IRAN_FILE)
HASH:=ca18f8e1676f6df20a1fcf5338e033a9e7d6aae7494b13d35e27f2d45bf02f50
HASH:=aa94b6efc99660838f85d700bbfea38d71321eeb1b3cc6cf92ade1db775d9e25
endef
define Package/v2ray-geodata/template
@@ -4,7 +4,7 @@ namespace ServiceLib.Handler.Fmt;
public class BaseFmt
{
private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure", "verify" };
private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure" };
protected static string GetIpv6(string address)
{
@@ -73,7 +73,7 @@ open class FmtBase {
config.security = null
}
// Support multiple possible query keys for allowInsecure like the C# implementation
val allowInsecureKeys = arrayOf("insecure", "allowInsecure", "allow_insecure", "verify")
val allowInsecureKeys = arrayOf("insecure", "allowInsecure", "allow_insecure")
config.insecure = when {
allowInsecureKeys.any { queryParam[it] == "1" } -> true
allowInsecureKeys.any { queryParam[it] == "0" } -> false
+33
View File
@@ -755,6 +755,17 @@ class TestHTTPRequestHandler(TestRequestHandlerBase):
assert res.read(0) == b''
assert res.read() == b'<video src="/vid.mp4" /></html>'
def test_partial_read_greater_than_response_then_full_read(self, handler):
with handler() as rh:
for encoding in ('', 'gzip', 'deflate'):
res = validate_and_send(rh, Request(
f'http://127.0.0.1:{self.http_port}/content-encoding',
headers={'ytdl-encoding': encoding}))
assert res.headers.get('Content-Encoding') == encoding
assert res.read(512) == b'<html><video src="/vid.mp4" /></html>'
assert res.read(0) == b''
assert res.read() == b''
@pytest.mark.parametrize('handler', ['Urllib', 'Requests', 'CurlCFFI'], indirect=True)
@pytest.mark.handler_flaky('CurlCFFI', reason='segfaults')
@@ -920,6 +931,28 @@ class TestUrllibRequestHandler(TestRequestHandlerBase):
assert res.fp.fp is None
assert res.closed
def test_data_uri_partial_read_then_full_read(self, handler):
with handler() as rh:
res = validate_and_send(rh, Request('data:text/plain,hello%20world'))
assert res.read(6) == b'hello '
assert res.read(0) == b''
assert res.read() == b'world'
# Should automatically close the underlying file object
assert res.fp.closed
assert res.closed
def test_data_uri_partial_read_greater_than_response_then_full_read(self, handler):
with handler() as rh:
res = validate_and_send(rh, Request('data:text/plain,hello%20world'))
assert res.read(512) == b'hello world'
# Response and its underlying file object should already be closed now
assert res.fp.closed
assert res.closed
assert res.read(0) == b''
assert res.read() == b''
assert res.fp.closed
assert res.closed
def test_http_error_returns_content(self, handler):
# urllib HTTPError will try close the underlying response if reference to the HTTPError object is lost
def get_response():
+1 -1
View File
@@ -40,7 +40,7 @@ TEST_DIR = os.path.dirname(os.path.abspath(__file__))
pytestmark = pytest.mark.handler_flaky(
'Websockets',
os.name != 'nt' and sys.implementation.name == 'pypy',
os.name == 'nt' or sys.implementation.name == 'pypy',
reason='segfaults',
)
+8 -1
View File
@@ -691,6 +691,10 @@ from .frontendmasters import (
FrontendMastersIE,
FrontendMastersLessonIE,
)
from .frontro import (
TheChosenGroupIE,
TheChosenIE,
)
from .fujitv import FujiTVFODPlus7IE
from .funk import FunkIE
from .funker530 import Funker530IE
@@ -1094,7 +1098,10 @@ from .markiza import (
from .massengeschmacktv import MassengeschmackTVIE
from .masters import MastersIE
from .matchtv import MatchTVIE
from .mave import MaveIE
from .mave import (
MaveChannelIE,
MaveIE,
)
from .mbn import MBNIE
from .mdr import MDRIE
from .medaltv import MedalTVIE
+164
View File
@@ -0,0 +1,164 @@
import json
from .common import InfoExtractor
from ..utils import int_or_none, parse_iso8601, url_or_none
from ..utils.traversal import traverse_obj
class FrontoBaseIE(InfoExtractor):
def _get_auth_headers(self, url):
return traverse_obj(self._get_cookies(url), {
'authorization': ('frAccessToken', 'value', {lambda token: f'Bearer {token}' if token else None}),
})
class FrontroVideoBaseIE(FrontoBaseIE):
_CHANNEL_ID = None
def _real_extract(self, url):
video_id = self._match_id(url)
metadata = self._download_json(
'https://api.frontrow.cc/query', video_id, data=json.dumps({
'operationName': 'Video',
'variables': {'channelID': self._CHANNEL_ID, 'videoID': video_id},
'query': '''query Video($channelID: ID!, $videoID: ID!) {
video(ChannelID: $channelID, VideoID: $videoID) {
... on Video {title description updatedAt thumbnail createdAt duration likeCount comments views url hasAccess}
}
}''',
}).encode(), headers={
'content-type': 'application/json',
**self._get_auth_headers(url),
})['data']['video']
if not traverse_obj(metadata, 'hasAccess'):
self.raise_login_required()
formats, subtitles = self._extract_m3u8_formats_and_subtitles(metadata['url'], video_id)
return {
'id': video_id,
'formats': formats,
'subtitles': subtitles,
**traverse_obj(metadata, {
'title': ('title', {str}),
'description': ('description', {str}),
'thumbnail': ('thumbnail', {url_or_none}),
'timestamp': ('createdAt', {parse_iso8601}),
'modified_timestamp': ('updatedAt', {parse_iso8601}),
'duration': ('duration', {int_or_none}),
'like_count': ('likeCount', {int_or_none}),
'comment_count': ('comments', {int_or_none}),
'view_count': ('views', {int_or_none}),
}),
}
class FrontroGroupBaseIE(FrontoBaseIE):
_CHANNEL_ID = None
_VIDEO_EXTRACTOR = None
_VIDEO_URL_TMPL = None
def _real_extract(self, url):
group_id = self._match_id(url)
metadata = self._download_json(
'https://api.frontrow.cc/query', group_id, note='Downloading playlist metadata',
data=json.dumps({
'operationName': 'PaginatedStaticPageContainer',
'variables': {'channelID': self._CHANNEL_ID, 'first': 500, 'pageContainerID': group_id},
'query': '''query PaginatedStaticPageContainer($channelID: ID!, $pageContainerID: ID!) {
pageContainer(ChannelID: $channelID, PageContainerID: $pageContainerID) {
... on StaticPageContainer { id title updatedAt createdAt itemRefs {edges {node {
id contentItem { ... on ItemVideo { videoItem: item {
id
}}}
}}}
}
}
}''',
}).encode(), headers={
'content-type': 'application/json',
**self._get_auth_headers(url),
})['data']['pageContainer']
entries = []
for video_id in traverse_obj(metadata, (
'itemRefs', 'edges', ..., 'node', 'contentItem', 'videoItem', 'id', {str}),
):
entries.append(self.url_result(
self._VIDEO_URL_TMPL % video_id, self._VIDEO_EXTRACTOR, video_id))
return {
'_type': 'playlist',
'id': group_id,
'entries': entries,
**traverse_obj(metadata, {
'title': ('title', {str}),
'timestamp': ('createdAt', {parse_iso8601}),
'modified_timestamp': ('updatedAt', {parse_iso8601}),
}),
}
class TheChosenIE(FrontroVideoBaseIE):
_CHANNEL_ID = '12884901895'
_VALID_URL = r'https?://(?:www\.)?watch\.thechosen\.tv/video/(?P<id>[0-9]+)'
_TESTS = [{
'url': 'https://watch.thechosen.tv/video/184683594325',
'md5': '3f878b689588c71b38ec9943c54ff5b0',
'info_dict': {
'id': '184683594325',
'ext': 'mp4',
'title': 'Season 3 Episode 2: Two by Two',
'description': 'md5:174c373756ecc8df46b403f4fcfbaf8c',
'comment_count': int,
'view_count': int,
'like_count': int,
'duration': 4212,
'thumbnail': r're:https://fastly\.frontrowcdn\.com/channels/12884901895/VIDEO_THUMBNAIL/184683594325/',
'timestamp': 1698954546,
'upload_date': '20231102',
'modified_timestamp': int,
'modified_date': str,
},
}, {
'url': 'https://watch.thechosen.tv/video/184683596189',
'md5': 'd581562f9d29ce82f5b7770415334151',
'info_dict': {
'id': '184683596189',
'ext': 'mp4',
'title': 'Season 4 Episode 8: Humble',
'description': 'md5:20a57bead43da1cf77cd5b0fe29bbc76',
'comment_count': int,
'view_count': int,
'like_count': int,
'duration': 5092,
'thumbnail': r're:https://fastly\.frontrowcdn\.com/channels/12884901895/VIDEO_THUMBNAIL/184683596189/',
'timestamp': 1715019474,
'upload_date': '20240506',
'modified_timestamp': int,
'modified_date': str,
},
}]
class TheChosenGroupIE(FrontroGroupBaseIE):
_CHANNEL_ID = '12884901895'
_VIDEO_EXTRACTOR = TheChosenIE
_VIDEO_URL_TMPL = 'https://watch.thechosen.tv/video/%s'
_VALID_URL = r'https?://(?:www\.)?watch\.thechosen\.tv/group/(?P<id>[0-9]+)'
_TESTS = [{
'url': 'https://watch.thechosen.tv/group/309237658592',
'info_dict': {
'id': '309237658592',
'title': 'Season 3',
'timestamp': 1746203969,
'upload_date': '20250502',
'modified_timestamp': int,
'modified_date': str,
},
'playlist_count': 8,
}]
+119 -36
View File
@@ -1,7 +1,9 @@
import re
import functools
import math
from .common import InfoExtractor
from ..utils import (
InAdvancePagedList,
clean_html,
int_or_none,
parse_iso8601,
@@ -10,15 +12,64 @@ from ..utils import (
from ..utils.traversal import require, traverse_obj
class MaveIE(InfoExtractor):
_VALID_URL = r'https?://(?P<channel>[\w-]+)\.mave\.digital/(?P<id>ep-\d+)'
class MaveBaseIE(InfoExtractor):
_API_BASE_URL = 'https://api.mave.digital/v1/website'
_API_BASE_STORAGE_URL = 'https://store.cloud.mts.ru/mave/'
def _load_channel_meta(self, channel_id, display_id):
return traverse_obj(self._download_json(
f'{self._API_BASE_URL}/{channel_id}/', display_id,
note='Downloading channel metadata'), 'podcast')
def _load_episode_meta(self, channel_id, episode_code, display_id):
return self._download_json(
f'{self._API_BASE_URL}/{channel_id}/episodes/{episode_code}',
display_id, note='Downloading episode metadata')
def _create_entry(self, channel_id, channel_meta, episode_meta):
episode_code = traverse_obj(episode_meta, ('code', {int}, {require('episode code')}))
return {
'display_id': f'{channel_id}-{episode_code}',
'extractor_key': MaveIE.ie_key(),
'extractor': MaveIE.IE_NAME,
'webpage_url': f'https://{channel_id}.mave.digital/ep-{episode_code}',
'channel_id': channel_id,
'channel_url': f'https://{channel_id}.mave.digital/',
'vcodec': 'none',
**traverse_obj(episode_meta, {
'id': ('id', {str}),
'url': ('audio', {urljoin(self._API_BASE_STORAGE_URL)}),
'title': ('title', {str}),
'description': ('description', {clean_html}),
'thumbnail': ('image', {urljoin(self._API_BASE_STORAGE_URL)}),
'duration': ('duration', {int_or_none}),
'season_number': ('season', {int_or_none}),
'episode_number': ('number', {int_or_none}),
'view_count': ('listenings', {int_or_none}),
'like_count': ('reactions', lambda _, v: v['type'] == 'like', 'count', {int_or_none}, any),
'dislike_count': ('reactions', lambda _, v: v['type'] == 'dislike', 'count', {int_or_none}, any),
'age_limit': ('is_explicit', {bool}, {lambda x: 18 if x else None}),
'timestamp': ('publish_date', {parse_iso8601}),
}),
**traverse_obj(channel_meta, {
'series_id': ('id', {str}),
'series': ('title', {str}),
'channel': ('title', {str}),
'uploader': ('author', {str}),
}),
}
class MaveIE(MaveBaseIE):
IE_NAME = 'mave'
_VALID_URL = r'https?://(?P<channel_id>[\w-]+)\.mave\.digital/ep-(?P<episode_code>\d+)'
_TESTS = [{
'url': 'https://ochenlichnoe.mave.digital/ep-25',
'md5': 'aa3e513ef588b4366df1520657cbc10c',
'info_dict': {
'id': '4035f587-914b-44b6-aa5a-d76685ad9bc2',
'ext': 'mp3',
'display_id': 'ochenlichnoe-ep-25',
'display_id': 'ochenlichnoe-25',
'title': 'Между мной и миром: психология самооценки',
'description': 'md5:4b7463baaccb6982f326bce5c700382a',
'uploader': 'Самарский университет',
@@ -45,7 +96,7 @@ class MaveIE(InfoExtractor):
'info_dict': {
'id': '41898bb5-ff57-4797-9236-37a8e537aa21',
'ext': 'mp3',
'display_id': 'budem-ep-12',
'display_id': 'budem-12',
'title': 'Екатерина Михайлова: "Горе от ума" не про женщин написана',
'description': 'md5:fa3bdd59ee829dfaf16e3efcb13f1d19',
'uploader': 'Полина Цветкова+Евгения Акопова',
@@ -68,40 +119,72 @@ class MaveIE(InfoExtractor):
'upload_date': '20241230',
},
}]
_API_BASE_URL = 'https://api.mave.digital/'
def _real_extract(self, url):
channel_id, slug = self._match_valid_url(url).group('channel', 'id')
display_id = f'{channel_id}-{slug}'
webpage = self._download_webpage(url, display_id)
data = traverse_obj(
self._search_nuxt_json(webpage, display_id),
('data', lambda _, v: v['activeEpisodeData'], any, {require('podcast data')}))
channel_id, episode_code = self._match_valid_url(url).group(
'channel_id', 'episode_code')
display_id = f'{channel_id}-{episode_code}'
channel_meta = self._load_channel_meta(channel_id, display_id)
episode_meta = self._load_episode_meta(channel_id, episode_code, display_id)
return self._create_entry(channel_id, channel_meta, episode_meta)
class MaveChannelIE(MaveBaseIE):
IE_NAME = 'mave:channel'
_VALID_URL = r'https?://(?P<id>[\w-]+)\.mave\.digital/?(?:$|[?#])'
_TESTS = [{
'url': 'https://budem.mave.digital/',
'info_dict': {
'id': 'budem',
'title': 'Все там будем',
'description': 'md5:f04ae12a42be0f1d765c5e326b41987a',
},
'playlist_mincount': 15,
}, {
'url': 'https://ochenlichnoe.mave.digital/',
'info_dict': {
'id': 'ochenlichnoe',
'title': 'Очень личное',
'description': 'md5:ee36a6a52546b91b487fe08c552fdbb2',
},
'playlist_mincount': 20,
}, {
'url': 'https://geekcity.mave.digital/',
'info_dict': {
'id': 'geekcity',
'title': 'Мужчины в трико',
'description': 'md5:4164d425d60a0d97abdce9d1f6f8e049',
},
'playlist_mincount': 80,
}]
_PAGE_SIZE = 50
def _entries(self, channel_id, channel_meta, page_num):
page_data = self._download_json(
f'{self._API_BASE_URL}/{channel_id}/episodes', channel_id, query={
'view': 'all',
'page': page_num + 1,
'sort': 'newest',
'format': 'all',
}, note=f'Downloading page {page_num + 1}')
for ep in traverse_obj(page_data, ('episodes', lambda _, v: v['audio'] and v['id'])):
yield self._create_entry(channel_id, channel_meta, ep)
def _real_extract(self, url):
channel_id = self._match_id(url)
channel_meta = self._load_channel_meta(channel_id, channel_id)
return {
'display_id': display_id,
'channel_id': channel_id,
'channel_url': f'https://{channel_id}.mave.digital/',
'vcodec': 'none',
'thumbnail': re.sub(r'_\d+(?=\.(?:jpg|png))', '', self._og_search_thumbnail(webpage, default='')) or None,
**traverse_obj(data, ('activeEpisodeData', {
'url': ('audio', {urljoin(self._API_BASE_URL)}),
'id': ('id', {str}),
'_type': 'playlist',
'id': channel_id,
**traverse_obj(channel_meta, {
'title': ('title', {str}),
'description': ('description', {clean_html}),
'duration': ('duration', {int_or_none}),
'season_number': ('season', {int_or_none}),
'episode_number': ('number', {int_or_none}),
'view_count': ('listenings', {int_or_none}),
'like_count': ('reactions', lambda _, v: v['type'] == 'like', 'count', {int_or_none}, any),
'dislike_count': ('reactions', lambda _, v: v['type'] == 'dislike', 'count', {int_or_none}, any),
'age_limit': ('is_explicit', {bool}, {lambda x: 18 if x else None}),
'timestamp': ('publish_date', {parse_iso8601}),
})),
**traverse_obj(data, ('podcast', 'podcast', {
'series_id': ('id', {str}),
'series': ('title', {str}),
'channel': ('title', {str}),
'uploader': ('author', {str}),
})),
'description': ('description', {str}),
}),
'entries': InAdvancePagedList(
functools.partial(self._entries, channel_id, channel_meta),
math.ceil(channel_meta['episodes_count'] / self._PAGE_SIZE), self._PAGE_SIZE),
}
+12 -4
View File
@@ -3150,6 +3150,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
self._downloader.deprecated_feature('[youtube] include_duplicate_formats extractor argument is deprecated. '
'Use formats=duplicate extractor argument instead')
def is_super_resolution(f_url):
return '1' in traverse_obj(f_url, ({parse_qs}, 'xtags', ..., {urllib.parse.parse_qs}, 'sr', ...))
def solve_sig(s, spec):
return ''.join(s[i] for i in spec)
@@ -3202,7 +3205,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
def get_stream_id(fmt_stream):
return str_or_none(fmt_stream.get('itag')), traverse_obj(fmt_stream, 'audioTrack', 'id'), fmt_stream.get('isDrc')
def process_format_stream(fmt_stream, proto, missing_pot):
def process_format_stream(fmt_stream, proto, missing_pot, super_resolution=False):
itag = str_or_none(fmt_stream.get('itag'))
audio_track = fmt_stream.get('audioTrack') or {}
quality = fmt_stream.get('quality')
@@ -3253,10 +3256,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
dct = {
'asr': int_or_none(fmt_stream.get('audioSampleRate')),
'filesize': int_or_none(fmt_stream.get('contentLength')),
'format_id': f'{itag}{"-drc" if fmt_stream.get("isDrc") else ""}',
'format_id': join_nonempty(itag, (
'drc' if fmt_stream.get('isDrc')
else 'sr' if super_resolution
else None)),
'format_note': join_nonempty(
join_nonempty(audio_track.get('displayName'), audio_track.get('audioIsDefault') and '(default)', delim=' '),
name, fmt_stream.get('isDrc') and 'DRC',
name, fmt_stream.get('isDrc') and 'DRC', super_resolution and 'AI-upscaled',
try_get(fmt_stream, lambda x: x['projectionType'].replace('RECTANGULAR', '').lower()),
try_get(fmt_stream, lambda x: x['spatialAudioType'].replace('SPATIAL_AUDIO_TYPE_', '').lower()),
is_damaged and 'DAMAGED', missing_pot and 'MISSING POT',
@@ -3342,7 +3348,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
self.report_warning(msg, video_id, only_once=True)
continue
fmt = process_format_stream(fmt_stream, proto, missing_pot=require_po_token and not po_token)
fmt = process_format_stream(
fmt_stream, proto, missing_pot=require_po_token and not po_token,
super_resolution=is_super_resolution(fmt_url))
if not fmt:
continue
+2
View File
@@ -305,6 +305,8 @@ class UrllibResponseAdapter(Response):
status=getattr(res, 'status', None) or res.getcode(), reason=getattr(res, 'reason', None))
def read(self, amt=None):
if self.closed:
return b''
try:
data = self.fp.read(amt)
underlying = getattr(self.fp, 'fp', None)