Compare commits
4 Commits
libsyncthi
...
libsyncthi
Author | SHA1 | Date |
---|---|---|
Martchus | b67dd1dc62 | |
Jakob Borg | b014a9ebc2 | |
Jakob Borg | 53123c0b01 | |
Jakob Borg | 07ad2db503 |
|
@ -49,6 +49,19 @@ jobs:
|
||||||
# The oldest version in this list should match what we have in our go.mod.
|
# The oldest version in this list should match what we have in our go.mod.
|
||||||
# Variables don't seem to be supported here, or we could have done something nice.
|
# Variables don't seem to be supported here, or we could have done something nice.
|
||||||
go: ["1.20", "1.21"]
|
go: ["1.20", "1.21"]
|
||||||
|
|
||||||
|
# Don't run the Windows tests with Go 1.21.4 or 1.20.11; this can be
|
||||||
|
# removed when 1.21.5 and 1.20.12 is released.
|
||||||
|
exclude:
|
||||||
|
- runner: windows-latest
|
||||||
|
go: "1.20"
|
||||||
|
- runner: windows-latest
|
||||||
|
go: "1.21"
|
||||||
|
include:
|
||||||
|
- runner: windows-latest
|
||||||
|
go: "~1.20.12 || ~1.20.0 <1.20.11"
|
||||||
|
- runner: windows-latest
|
||||||
|
go: "~1.21.5 || ~1.21.1 <1.21.4"
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
steps:
|
steps:
|
||||||
- name: Set git to use LF
|
- name: Set git to use LF
|
||||||
|
@ -79,6 +92,7 @@ jobs:
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: |
|
run: |
|
||||||
|
go version
|
||||||
go run build.go test | go-test-json-to-loki
|
go run build.go test | go-test-json-to-loki
|
||||||
env:
|
env:
|
||||||
GOFLAGS: "-json"
|
GOFLAGS: "-json"
|
||||||
|
@ -155,7 +169,9 @@ jobs:
|
||||||
|
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
# Temporary version constraint to avoid 1.21.4 which has a bug in
|
||||||
|
# path handling. This can be removed when 1.21.5 is released.
|
||||||
|
go-version: "~1.21.5 || ~1.21.1 <1.21.4"
|
||||||
cache: false
|
cache: false
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
#include "c_bindings.h"
|
||||||
|
|
||||||
|
libst_logging_callback_function_t libst_logging_callback_function = NULL;
|
||||||
|
|
||||||
|
void libst_invoke_logging_callback(int log_level, const char *message, size_t message_size)
|
||||||
|
{
|
||||||
|
if (!libst_logging_callback_function) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
libst_logging_callback_function(log_level, message, message_size);
|
||||||
|
}
|
|
@ -0,0 +1,241 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"unsafe"
|
||||||
|
"path/filepath"
|
||||||
|
_ "net/http/pprof" // Need to import this to support STPROFILER.
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
|
"github.com/syncthing/syncthing/lib/logger"
|
||||||
|
"github.com/syncthing/syncthing/lib/locations"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/svcutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/syncthing"
|
||||||
|
"github.com/syncthing/syncthing/cmd/syncthing/cli"
|
||||||
|
"github.com/syncthing/syncthing/cmd/syncthing"
|
||||||
|
"github.com/thejerf/suture/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// include header for required C helper functions (so the following comment is NO comment)
|
||||||
|
|
||||||
|
// #include "c_bindings.h"
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
var theApp *syncthing.App
|
||||||
|
var myID protocol.DeviceID
|
||||||
|
var cliArgs []string
|
||||||
|
|
||||||
|
const (
|
||||||
|
tlsDefaultCommonName = "syncthing"
|
||||||
|
)
|
||||||
|
|
||||||
|
//export libst_own_device_id
|
||||||
|
func libst_own_device_id() string {
|
||||||
|
return myID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export libst_init_logging
|
||||||
|
func libst_init_logging() {
|
||||||
|
l.AddHandler(logger.LevelVerbose, func(level logger.LogLevel, msg string) {
|
||||||
|
runes := []byte(msg)
|
||||||
|
length := len(runes)
|
||||||
|
if length <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
C.libst_invoke_logging_callback(C.int(level), (*C.char)(unsafe.Pointer(&runes[0])), C.size_t(len(runes)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//export libst_clear_cli_args
|
||||||
|
func libst_clear_cli_args(command string) {
|
||||||
|
if command == "cli" {
|
||||||
|
cliArgs = []string{"syncthing", command}
|
||||||
|
} else {
|
||||||
|
cliArgs = []string{command}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export libst_append_cli_arg
|
||||||
|
func libst_append_cli_arg(arg string) {
|
||||||
|
cliArgs = append(cliArgs, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export libst_run_cli
|
||||||
|
func libst_run_cli() int {
|
||||||
|
if err := cli.RunWithArgs(cliArgs); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
//export libst_run_main
|
||||||
|
func libst_run_main() int {
|
||||||
|
if err := syncthing_main.RunWithArgs(cliArgs); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
//export libst_run_syncthing
|
||||||
|
func libst_run_syncthing(configDir string, dataDir string, guiAddress string, guiApiKey string, verbose bool, allowNewerConfig bool, noDefaultConfig bool, skipPortProbing bool, ensureConfigDirExists bool, ensureDataDirExists bool) int {
|
||||||
|
// return if already running (for simplicity we only allow one Syncthing instance at at time for now)
|
||||||
|
if theApp != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// set specified GUI address and API key
|
||||||
|
if guiAddress != "" {
|
||||||
|
os.Setenv("STGUIADDRESS", guiAddress)
|
||||||
|
}
|
||||||
|
if guiApiKey != "" {
|
||||||
|
os.Setenv("STGUIAPIKEY", guiApiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set specified config dir
|
||||||
|
if configDir != "" {
|
||||||
|
if !filepath.IsAbs(configDir) {
|
||||||
|
var err error
|
||||||
|
configDir, err = filepath.Abs(configDir)
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("Failed to make config path absolute:", err)
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := locations.SetBaseDir(locations.ConfigBaseDir, configDir); err != nil {
|
||||||
|
l.Warnln(err)
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set specified database dir
|
||||||
|
if dataDir != "" {
|
||||||
|
if !filepath.IsAbs(dataDir) {
|
||||||
|
var err error
|
||||||
|
dataDir, err = filepath.Abs(dataDir)
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("Failed to make database path absolute:", err)
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := locations.SetBaseDir(locations.DataBaseDir, dataDir); err != nil {
|
||||||
|
l.Warnln(err)
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure that the config directory exists
|
||||||
|
if ensureConfigDirExists {
|
||||||
|
if err := syncthing.EnsureDir(locations.GetBaseDir(locations.ConfigBaseDir), 0700); err != nil {
|
||||||
|
l.Warnln("Failed to create config directory:", err)
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure that the database directory exists
|
||||||
|
if dataDir != "" && ensureDataDirExists {
|
||||||
|
if err := syncthing.EnsureDir(locations.GetBaseDir(locations.DataBaseDir), 0700); err != nil {
|
||||||
|
l.Warnln("Failed to create database directory:", err)
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure that we have a certificate and key
|
||||||
|
cert, certErr := syncthing.LoadOrGenerateCertificate(
|
||||||
|
locations.Get(locations.CertFile),
|
||||||
|
locations.Get(locations.KeyFile),
|
||||||
|
)
|
||||||
|
if certErr != nil {
|
||||||
|
l.Warnln("Failed to load/generate certificate:", certErr)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// earlyService is a supervisor that runs the services needed for or
|
||||||
|
// before app startup; the event logger, and the config service.
|
||||||
|
spec := svcutil.SpecWithDebugLogger(l)
|
||||||
|
earlyService := suture.New("early", spec)
|
||||||
|
earlyService.ServeBackground(ctx)
|
||||||
|
|
||||||
|
evLogger := events.NewLogger()
|
||||||
|
earlyService.Add(evLogger)
|
||||||
|
|
||||||
|
// load config
|
||||||
|
configLocation := locations.Get(locations.ConfigFile)
|
||||||
|
l.Infoln("Loading config from:", configLocation)
|
||||||
|
cfgWrapper, cfgErr := syncthing.LoadConfigAtStartup(configLocation, cert, evLogger, allowNewerConfig, noDefaultConfig, skipPortProbing)
|
||||||
|
if cfgErr != nil {
|
||||||
|
l.Warnln("Failed to initialize config:", cfgErr)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
if cfgService, ok := cfgWrapper.(suture.Service); ok {
|
||||||
|
earlyService.Add(cfgService)
|
||||||
|
}
|
||||||
|
|
||||||
|
// open database
|
||||||
|
dbFile := locations.Get(locations.Database)
|
||||||
|
l.Infoln("Opening database from:", dbFile)
|
||||||
|
ldb, dbErr := syncthing.OpenDBBackend(dbFile, config.TuningAuto)
|
||||||
|
if dbErr != nil {
|
||||||
|
l.Warnln("Error opening database:", dbErr)
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
|
||||||
|
appOpts := syncthing.Options{
|
||||||
|
ProfilerAddr: os.Getenv("STPROFILER"),
|
||||||
|
NoUpgrade: true,
|
||||||
|
Verbose: verbose,
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
theApp, err = syncthing.New(cfgWrapper, ldb, evLogger, cert, appOpts)
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("Failed to start Syncthing:", err)
|
||||||
|
return svcutil.ExitError.AsInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// start Syncthing and block until it has finished
|
||||||
|
returnCode := 0
|
||||||
|
if err := theApp.Start(); err != nil {
|
||||||
|
returnCode = svcutil.ExitError.AsInt()
|
||||||
|
}
|
||||||
|
returnCode = theApp.Wait().AsInt();
|
||||||
|
theApp = nil
|
||||||
|
return returnCode
|
||||||
|
}
|
||||||
|
|
||||||
|
//export libst_stop_syncthing
|
||||||
|
func libst_stop_syncthing() int {
|
||||||
|
if theApp != nil {
|
||||||
|
return int(theApp.Stop(svcutil.ExitSuccess))
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export libst_reset_database
|
||||||
|
func libst_reset_database() {
|
||||||
|
os.RemoveAll(locations.Get(locations.Database))
|
||||||
|
}
|
||||||
|
|
||||||
|
//export libst_syncthing_version
|
||||||
|
func libst_syncthing_version() *C.char {
|
||||||
|
return C.CString(build.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export libst_long_syncthing_version
|
||||||
|
func libst_long_syncthing_version() *C.char {
|
||||||
|
return C.CString(build.LongVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// prevent "runtime.main_main·f: function main is undeclared in the main package"
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
#ifndef LIBSYNCTHING_INTERNAL_H
|
||||||
|
#define LIBSYNCTHING_INTERNAL_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
// allow registration of callback function
|
||||||
|
typedef void (*libst_logging_callback_function_t) (int logLevel, const char *msg, size_t msgSize);
|
||||||
|
extern libst_logging_callback_function_t libst_logging_callback_function;
|
||||||
|
extern void libst_invoke_logging_callback(int log_level, const char *message, size_t message_size);
|
||||||
|
|
||||||
|
#endif // LIBSYNCTHING_INTERNAL_H
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright (C) 2014 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/syncthing/syncthing/lib/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
l = logger.DefaultLogger.NewFacility("main", "Main package")
|
||||||
|
)
|
|
@ -4,7 +4,7 @@
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
package main
|
package syncthing_main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
package main
|
package syncthing_main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
package main
|
package syncthing_main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
package main
|
package syncthing_main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/syncthing/syncthing/lib/logger"
|
"github.com/syncthing/syncthing/lib/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
l = logger.DefaultLogger.NewFacility("main", "Main package")
|
l = logger.DefaultLogger.NewFacility("syncthing_main", "Syncthing package")
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
package main
|
package syncthing_main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
package main
|
package syncthing_main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -33,7 +33,6 @@ import (
|
||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
"github.com/thejerf/suture/v4"
|
"github.com/thejerf/suture/v4"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/cmd/syncthing/cli"
|
|
||||||
"github.com/syncthing/syncthing/cmd/syncthing/cmdutil"
|
"github.com/syncthing/syncthing/cmd/syncthing/cmdutil"
|
||||||
"github.com/syncthing/syncthing/cmd/syncthing/decrypt"
|
"github.com/syncthing/syncthing/cmd/syncthing/decrypt"
|
||||||
"github.com/syncthing/syncthing/cmd/syncthing/generate"
|
"github.com/syncthing/syncthing/cmd/syncthing/generate"
|
||||||
|
@ -139,7 +138,6 @@ var entrypoint struct {
|
||||||
Serve serveOptions `cmd:"" help:"Run Syncthing"`
|
Serve serveOptions `cmd:"" help:"Run Syncthing"`
|
||||||
Generate generate.CLI `cmd:"" help:"Generate key and config, then exit"`
|
Generate generate.CLI `cmd:"" help:"Generate key and config, then exit"`
|
||||||
Decrypt decrypt.CLI `cmd:"" help:"Decrypt or verify an encrypted folder"`
|
Decrypt decrypt.CLI `cmd:"" help:"Decrypt or verify an encrypted folder"`
|
||||||
Cli struct{} `cmd:"" help:"Command line interface for Syncthing"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveOptions are the options for the `syncthing serve` command.
|
// serveOptions are the options for the `syncthing serve` command.
|
||||||
|
@ -212,23 +210,10 @@ func defaultVars() kong.Vars {
|
||||||
return vars
|
return vars
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func RunWithArgs(args []string) error {
|
||||||
// The "cli" subcommand uses a different command line parser, and e.g. help
|
|
||||||
// gets mangled when integrating it as a subcommand -> detect it here at the
|
|
||||||
// beginning.
|
|
||||||
if len(os.Args) > 1 && os.Args[1] == "cli" {
|
|
||||||
if err := cli.Run(); err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// First some massaging of the raw command line to fit the new model.
|
// First some massaging of the raw command line to fit the new model.
|
||||||
// Basically this means adding the default command at the front, and
|
// Basically this means adding the default command at the front, and
|
||||||
// converting -options to --options.
|
// converting -options to --options.
|
||||||
|
|
||||||
args := os.Args[1:]
|
|
||||||
switch {
|
switch {
|
||||||
case len(args) == 0:
|
case len(args) == 0:
|
||||||
// Empty command line is equivalent to just calling serve
|
// Empty command line is equivalent to just calling serve
|
||||||
|
@ -259,6 +244,7 @@ func main() {
|
||||||
ctx.BindTo(l, (*logger.Logger)(nil)) // main logger available to subcommands
|
ctx.BindTo(l, (*logger.Logger)(nil)) // main logger available to subcommands
|
||||||
err = ctx.Run()
|
err = ctx.Run()
|
||||||
parser.FatalIfErrorf(err)
|
parser.FatalIfErrorf(err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func helpHandler(options kong.HelpOptions, ctx *kong.Context) error {
|
func helpHandler(options kong.HelpOptions, ctx *kong.Context) error {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
package main
|
package syncthing_main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
package main
|
package syncthing_main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
//go:build !windows
|
//go:build !windows
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|
||||||
package main
|
package syncthing_main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
//go:build windows
|
//go:build windows
|
||||||
// +build windows
|
// +build windows
|
||||||
|
|
||||||
package main
|
package syncthing_main
|
||||||
|
|
||||||
import "os/exec"
|
import "os/exec"
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
//go:build !solaris && !windows
|
//go:build !solaris && !windows
|
||||||
// +build !solaris,!windows
|
// +build !solaris,!windows
|
||||||
|
|
||||||
package main
|
package syncthing_main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
//go:build solaris || windows
|
//go:build solaris || windows
|
||||||
// +build solaris windows
|
// +build solaris windows
|
||||||
|
|
||||||
package main
|
package syncthing_main
|
||||||
|
|
||||||
func startPerfStats() {
|
func startPerfStats() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
//go:build go1.7
|
//go:build go1.7
|
||||||
// +build go1.7
|
// +build go1.7
|
||||||
|
|
||||||
package main
|
package syncthing_main
|
||||||
|
|
||||||
import "runtime/debug"
|
import "runtime/debug"
|
||||||
|
|
||||||
|
|
|
@ -38,9 +38,8 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var deviceIDShort = metadata.deviceID.substr(0, 5);
|
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token-' + metadata.deviceIDShort;
|
||||||
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token-' + deviceIDShort;
|
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token-' + metadata.deviceIDShort;
|
||||||
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token-' + deviceIDShort;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// @TODO: extract global level functions into separate service(s)
|
// @TODO: extract global level functions into separate service(s)
|
||||||
|
|
|
@ -365,15 +365,15 @@ func (s *service) Serve(ctx context.Context) error {
|
||||||
|
|
||||||
// Wrap everything in CSRF protection. The /rest prefix should be
|
// Wrap everything in CSRF protection. The /rest prefix should be
|
||||||
// protected, other requests will grant cookies.
|
// protected, other requests will grant cookies.
|
||||||
var handler http.Handler = newCsrfManager(s.id.String()[:5], "/rest", guiCfg, mux, locations.Get(locations.CsrfTokens))
|
var handler http.Handler = newCsrfManager(s.id.Short().String(), "/rest", guiCfg, mux, locations.Get(locations.CsrfTokens))
|
||||||
|
|
||||||
// Add our version and ID as a header to responses
|
// Add our version and ID as a header to responses
|
||||||
handler = withDetailsMiddleware(s.id, handler)
|
handler = withDetailsMiddleware(s.id, handler)
|
||||||
|
|
||||||
// Wrap everything in basic auth, if user/password is set.
|
// Wrap everything in basic auth, if user/password is set.
|
||||||
if guiCfg.IsAuthEnabled() {
|
if guiCfg.IsAuthEnabled() {
|
||||||
sessionCookieName := "sessionid-" + s.id.String()[:5]
|
sessionCookieName := "sessionid-" + s.id.Short().String()
|
||||||
handler = basicAuthAndSessionMiddleware(sessionCookieName, guiCfg, s.cfg.LDAP(), handler, s.evLogger)
|
handler = basicAuthAndSessionMiddleware(sessionCookieName, s.id.Short().String(), guiCfg, s.cfg.LDAP(), handler, s.evLogger)
|
||||||
handlePasswordAuth := passwordAuthHandler(sessionCookieName, guiCfg, s.cfg.LDAP(), s.evLogger)
|
handlePasswordAuth := passwordAuthHandler(sessionCookieName, guiCfg, s.cfg.LDAP(), s.evLogger)
|
||||||
restMux.Handler(http.MethodPost, "/rest/noauth/auth/password", handlePasswordAuth)
|
restMux.Handler(http.MethodPost, "/rest/noauth/auth/password", handlePasswordAuth)
|
||||||
|
|
||||||
|
@ -719,6 +719,7 @@ func (*service) getSystemPaths(w http.ResponseWriter, _ *http.Request) {
|
||||||
func (s *service) getJSMetadata(w http.ResponseWriter, _ *http.Request) {
|
func (s *service) getJSMetadata(w http.ResponseWriter, _ *http.Request) {
|
||||||
meta, _ := json.Marshal(map[string]interface{}{
|
meta, _ := json.Marshal(map[string]interface{}{
|
||||||
"deviceID": s.id.String(),
|
"deviceID": s.id.String(),
|
||||||
|
"deviceIDShort": s.id.Short().String(),
|
||||||
"authenticated": true,
|
"authenticated": true,
|
||||||
})
|
})
|
||||||
w.Header().Set("Content-Type", "application/javascript")
|
w.Header().Set("Content-Type", "application/javascript")
|
||||||
|
|
|
@ -42,8 +42,8 @@ func antiBruteForceSleep() {
|
||||||
time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
|
time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unauthorized(w http.ResponseWriter) {
|
func unauthorized(w http.ResponseWriter, shortID string) {
|
||||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
|
w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="Authorization Required (%s)"`, shortID))
|
||||||
http.Error(w, "Not Authorized", http.StatusUnauthorized)
|
http.Error(w, "Not Authorized", http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,21 +78,26 @@ func isNoAuthPath(path string) bool {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func basicAuthAndSessionMiddleware(cookieName string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration, next http.Handler, evLogger events.Logger) http.Handler {
|
func basicAuthAndSessionMiddleware(cookieName, shortID string, guiCfg config.GUIConfiguration, ldapCfg config.LDAPConfiguration, next http.Handler, evLogger events.Logger) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if hasValidAPIKeyHeader(r, guiCfg) {
|
if hasValidAPIKeyHeader(r, guiCfg) {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie, err := r.Cookie(cookieName)
|
for _, cookie := range r.Cookies() {
|
||||||
if err == nil && cookie != nil {
|
// We iterate here since there may, historically, be multiple
|
||||||
sessionsMut.Lock()
|
// cookies with the same name but different path. Any "old" ones
|
||||||
_, ok := sessions[cookie.Value]
|
// won't match an existing session and will be ignored, then
|
||||||
sessionsMut.Unlock()
|
// later removed on logout or when timing out.
|
||||||
if ok {
|
if cookie.Name == cookieName {
|
||||||
next.ServeHTTP(w, r)
|
sessionsMut.Lock()
|
||||||
return
|
_, ok := sessions[cookie.Value]
|
||||||
|
sessionsMut.Unlock()
|
||||||
|
if ok {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +117,7 @@ func basicAuthAndSessionMiddleware(cookieName string, guiCfg config.GUIConfigura
|
||||||
// Some browsers don't send the Authorization request header unless prompted by a 401 response.
|
// Some browsers don't send the Authorization request header unless prompted by a 401 response.
|
||||||
// This enables https://user:pass@localhost style URLs to keep working.
|
// This enables https://user:pass@localhost style URLs to keep working.
|
||||||
if guiCfg.SendBasicAuthPrompt {
|
if guiCfg.SendBasicAuthPrompt {
|
||||||
unauthorized(w)
|
unauthorized(w, shortID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,21 +203,26 @@ func createSession(cookieName string, username string, guiCfg config.GUIConfigur
|
||||||
|
|
||||||
func handleLogout(cookieName string) http.Handler {
|
func handleLogout(cookieName string) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
cookie, err := r.Cookie(cookieName)
|
for _, cookie := range r.Cookies() {
|
||||||
if err == nil && cookie != nil {
|
// We iterate here since there may, historically, be multiple
|
||||||
sessionsMut.Lock()
|
// cookies with the same name but different path. We drop them
|
||||||
delete(sessions, cookie.Value)
|
// all.
|
||||||
sessionsMut.Unlock()
|
if cookie.Name == cookieName {
|
||||||
}
|
sessionsMut.Lock()
|
||||||
// else: If there is no session cookie, that's also a successful logout in terms of user experience.
|
delete(sessions, cookie.Value)
|
||||||
|
sessionsMut.Unlock()
|
||||||
|
|
||||||
|
// Delete the cookie
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: cookieName,
|
||||||
|
Value: "",
|
||||||
|
MaxAge: -1,
|
||||||
|
Secure: cookie.Secure,
|
||||||
|
Path: cookie.Path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: cookieName,
|
|
||||||
Value: "",
|
|
||||||
MaxAge: -1,
|
|
||||||
Secure: true,
|
|
||||||
Path: "/",
|
|
||||||
})
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,15 @@ import (
|
||||||
"github.com/syncthing/syncthing/lib/sha256"
|
"github.com/syncthing/syncthing/lib/sha256"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DeviceIDLength = 32
|
const (
|
||||||
|
DeviceIDLength = 32
|
||||||
|
ShortIDStringLength = 7
|
||||||
|
)
|
||||||
|
|
||||||
type DeviceID [DeviceIDLength]byte
|
type (
|
||||||
type ShortID uint64
|
DeviceID [DeviceIDLength]byte
|
||||||
|
ShortID uint64
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
LocalDeviceID = repeatedDeviceID(0xff)
|
LocalDeviceID = repeatedDeviceID(0xff)
|
||||||
|
@ -94,7 +99,7 @@ func (s ShortID) String() string {
|
||||||
}
|
}
|
||||||
var bs [8]byte
|
var bs [8]byte
|
||||||
binary.BigEndian.PutUint64(bs[:], uint64(s))
|
binary.BigEndian.PutUint64(bs[:], uint64(s))
|
||||||
return base32.StdEncoding.EncodeToString(bs[:])[:7]
|
return base32.StdEncoding.EncodeToString(bs[:])[:ShortIDStringLength]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *DeviceID) UnmarshalText(bs []byte) error {
|
func (n *DeviceID) UnmarshalText(bs []byte) error {
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { environment } from '../environments/environment'
|
import { environment } from '../environments/environment'
|
||||||
|
|
||||||
export const deviceID = (): String => {
|
export const deviceID = (): String => {
|
||||||
const dID: String = environment.production ? globalThis.metadata['deviceID'] : '12345';
|
return environment.production ? globalThis.metadata['deviceIDShort'] : '1234567';
|
||||||
return dID.substring(0, 5)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiURL: String = '/'
|
export const apiURL: String = '/'
|
||||||
|
|
|
@ -173,7 +173,7 @@ func TestHTTPPOSTWithoutCSRF(t *testing.T) {
|
||||||
}
|
}
|
||||||
res.Body.Close()
|
res.Body.Close()
|
||||||
hdr := res.Header.Get("Set-Cookie")
|
hdr := res.Header.Get("Set-Cookie")
|
||||||
id := res.Header.Get("X-Syncthing-ID")[:5]
|
id := res.Header.Get("X-Syncthing-ID")[:protocol.ShortIDStringLength]
|
||||||
if !strings.Contains(hdr, "CSRF-Token") {
|
if !strings.Contains(hdr, "CSRF-Token") {
|
||||||
t.Error("Missing CSRF-Token in", hdr)
|
t.Error("Missing CSRF-Token in", hdr)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue