Merge aa8709359f582ca8b93db5ad95453832e0f6fbf3 into 5cc379fde09edf0753be8d8a871ca5a3003913f7

This commit is contained in:
Mads L. Nielsen 2026-08-10 16:30:31 +02:00 committed by GitHub
commit 0dfe833ea7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 179 additions and 80 deletions

View File

@ -300,7 +300,10 @@ func (a *Api) RegisterNumber(c *gin.Context) {
var req RegisterNumberRequest var req RegisterNumberRequest
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
buf.ReadFrom(c.Request.Body) if _, err := buf.ReadFrom(c.Request.Body); err != nil {
c.JSON(400, Error{Msg: "Couldn't process request - failed to read body."})
return
}
if buf.String() != "" { if buf.String() != "" {
err := json.Unmarshal(buf.Bytes(), &req) err := json.Unmarshal(buf.Bytes(), &req)
if err != nil { if err != nil {
@ -352,7 +355,10 @@ func (a *Api) UnregisterNumber(c *gin.Context) {
deleteAccount := false deleteAccount := false
deleteLocalData := false deleteLocalData := false
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
buf.ReadFrom(c.Request.Body) if _, err := buf.ReadFrom(c.Request.Body); err != nil {
c.JSON(400, Error{Msg: "Couldn't process request - failed to read body."})
return
}
if buf.String() != "" { if buf.String() != "" {
var req UnregisterNumberRequest var req UnregisterNumberRequest
err := json.Unmarshal(buf.Bytes(), &req) err := json.Unmarshal(buf.Bytes(), &req)
@ -432,7 +438,10 @@ func (a *Api) VerifyRegisteredNumber(c *gin.Context) {
pin := "" pin := ""
var req VerifyNumberSettings var req VerifyNumberSettings
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
buf.ReadFrom(c.Request.Body) if _, err := buf.ReadFrom(c.Request.Body); err != nil {
c.JSON(400, Error{Msg: "Couldn't process request - failed to read body."})
return
}
if buf.String() != "" { if buf.String() != "" {
err := json.Unmarshal(buf.Bytes(), &req) err := json.Unmarshal(buf.Bytes(), &req)
if err != nil { if err != nil {
@ -583,7 +592,9 @@ func (a *Api) handleSignalReceive(ws *websocket.Conn, number string, stop chan s
select { select {
case <-stop: case <-stop:
a.signalClient.RemoveReceiveChannel(channelUuid) a.signalClient.RemoveReceiveChannel(channelUuid)
ws.Close() if err := ws.Close(); err != nil {
log.Debug("Error closing websocket: ", err.Error())
}
return return
case msg := <-receiveChannel: case msg := <-receiveChannel:
var data string = string(msg.Params) var data string = string(msg.Params)
@ -640,7 +651,9 @@ func (a *Api) handleSignalReceive(ws *websocket.Conn, number string, stop chan s
func wsPong(ws *websocket.Conn, stop chan struct{}) { func wsPong(ws *websocket.Conn, stop chan struct{}) {
defer func() { defer func() {
close(stop) close(stop)
ws.Close() if err := ws.Close(); err != nil {
log.Debug("Error closing websocket: ", err.Error())
}
}() }()
ws.SetReadLimit(512) ws.SetReadLimit(512)
@ -658,7 +671,9 @@ func (a *Api) wsPing(ws *websocket.Conn, stop chan struct{}) {
for { for {
select { select {
case <-stop: case <-stop:
ws.Close() if err := ws.Close(); err != nil {
log.Debug("Error closing websocket: ", err.Error())
}
return return
case <-pingTicker.C: case <-pingTicker.C:
a.wsMutex.Lock() a.wsMutex.Lock()

View File

@ -9,6 +9,7 @@ import (
"github.com/gabriel-vasile/mimetype" "github.com/gabriel-vasile/mimetype"
uuid "github.com/gofrs/uuid" uuid "github.com/gofrs/uuid"
log "github.com/sirupsen/logrus"
) )
type AttachmentEntry struct { type AttachmentEntry struct {
@ -93,7 +94,7 @@ func (attachmentEntry *AttachmentEntry) storeBase64AsTemporaryFile() error {
attachmentEntry.DirName = dirNameUuid.String() attachmentEntry.DirName = dirNameUuid.String()
dirPath := attachmentEntry.attachmentTmpDir + attachmentEntry.DirName dirPath := attachmentEntry.attachmentTmpDir + attachmentEntry.DirName
if err := os.Mkdir(dirPath, os.ModePerm); err != nil { if err := os.Mkdir(dirPath, 0750); err != nil {
return err return err
} }
@ -103,29 +104,41 @@ func (attachmentEntry *AttachmentEntry) storeBase64AsTemporaryFile() error {
if err != nil { if err != nil {
return err return err
} }
defer f.Close()
if _, err := f.Write(dec); err != nil { if _, err := f.Write(dec); err != nil {
if closeErr := f.Close(); closeErr != nil {
log.Error("Couldn't close attachment tmp file: ", closeErr.Error())
}
attachmentEntry.cleanUp() attachmentEntry.cleanUp()
return err return err
} }
if err := f.Sync(); err != nil { if err := f.Sync(); err != nil {
if closeErr := f.Close(); closeErr != nil {
log.Error("Couldn't close attachment tmp file: ", closeErr.Error())
}
attachmentEntry.cleanUp()
return err
}
if err := f.Close(); err != nil {
attachmentEntry.cleanUp() attachmentEntry.cleanUp()
return err return err
} }
f.Close()
return nil return nil
} }
func (attachmentEntry *AttachmentEntry) cleanUp() { func (attachmentEntry *AttachmentEntry) cleanUp() {
if strings.Compare(attachmentEntry.FilePath, "") != 0 { if strings.Compare(attachmentEntry.FilePath, "") != 0 {
os.Remove(attachmentEntry.FilePath) if err := os.Remove(attachmentEntry.FilePath); err != nil {
log.Error("Couldn't remove file ", attachmentEntry.FilePath, ": ", err.Error())
}
} }
if strings.Compare(attachmentEntry.DirName, "") != 0 { if strings.Compare(attachmentEntry.DirName, "") != 0 {
dirPath := attachmentEntry.attachmentTmpDir + attachmentEntry.DirName dirPath := attachmentEntry.attachmentTmpDir + attachmentEntry.DirName
os.Remove(dirPath) if err := os.Remove(dirPath); err != nil {
log.Error("Couldn't remove directory ", dirPath, ": ", err.Error())
}
} }
} }

View File

@ -4,11 +4,13 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"errors" "errors"
utils "github.com/bbernhard/signal-cli-rest-api/utils" "fmt"
log "github.com/sirupsen/logrus"
"os/exec" "os/exec"
"strings" "strings"
"time" "time"
utils "github.com/bbernhard/signal-cli-rest-api/utils"
log "github.com/sirupsen/logrus"
) )
type CliClient struct { type CliClient struct {
@ -104,7 +106,12 @@ func (s *CliClient) Execute(wait bool, args []string, stdin string) (string, err
cmdTimeout = 120 cmdTimeout = 120
} }
cmd := exec.Command(signalCliBinary, args...) resolvedBinary, err := exec.LookPath(signalCliBinary)
if err != nil {
return "", fmt.Errorf("signal-cli binary '%s' not found in PATH: %w", signalCliBinary, err)
}
cmd := exec.Command(resolvedBinary, args...) // #nosec G204 -- resolvedBinary is the LookPath-resolved path of a hardcoded binary name ("signal-cli" or "signal-cli-native"), not user-controlled
if stdin != "" { if stdin != "" {
cmd.Stdin = strings.NewReader(stdin) cmd.Stdin = strings.NewReader(stdin)
} }
@ -161,7 +168,9 @@ func (s *CliClient) Execute(wait bool, args []string, stdin string) (string, err
if err != nil { if err != nil {
return "", err return "", err
} }
cmd.Start() if err := cmd.Start(); err != nil {
return "", err
}
buf := bufio.NewReader(stdout) // Notice that this is not in a loop buf := bufio.NewReader(stdout) // Notice that this is not in a loop
line, _, _ := buf.ReadLine() line, _, _ := buf.ReadLine()
return string(line), nil return string(line), nil

View File

@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -260,7 +259,9 @@ type ListDevicesResponse struct {
func cleanupTmpFiles(paths []string) { func cleanupTmpFiles(paths []string) {
for _, path := range paths { for _, path := range paths {
os.Remove(path) if err := os.Remove(path); err != nil {
log.Error("Couldn't remove tmp file ", path, ": ", err.Error())
}
} }
} }
@ -760,7 +761,7 @@ func (s *SignalClient) About() About {
BuildNr: 2, BuildNr: 2,
Mode: getSignalCliModeString(s.signalCliMode), Mode: getSignalCliModeString(s.signalCliMode),
Version: utils.GetEnv("BUILD_VERSION", "unset"), Version: utils.GetEnv("BUILD_VERSION", "unset"),
Capabilities: map[string][]string{"v2/send": []string{"quotes", "mentions"}}, Capabilities: map[string][]string{"v2/send": {"quotes", "mentions"}},
} }
return about return about
} }
@ -1154,8 +1155,7 @@ func (s *SignalClient) CreateGroup(number string, name string, members []string,
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
} }
var resp Response var resp Response
json.Unmarshal([]byte(rawData), &resp) if err := json.Unmarshal([]byte(rawData), &resp); err != nil {
if err != nil {
return "", err return "", err
} }
internalGroupId = resp.GroupId internalGroupId = resp.GroupId
@ -1758,7 +1758,9 @@ func (s *SignalClient) finishLinkAsync(jsonRpc2Client *JsonRpc2Client, deviceNam
return return
} }
log.Debug("Linking device result: ", result) log.Debug("Linking device result: ", result)
s.signalCliApiConfig.Load(s.signalCliApiConfigPath) if err := s.signalCliApiConfig.Load(s.signalCliApiConfigPath); err != nil {
log.Error("Couldn't reload signal-cli API config after linking device: ", err.Error())
}
}() }()
} }
@ -1851,7 +1853,7 @@ func (s *SignalClient) GetAttachment(attachment string) ([]byte, error) {
return []byte{}, &NotFoundError{Description: "No attachment with that name found"} return []byte{}, &NotFoundError{Description: "No attachment with that name found"}
} }
attachmentBytes, err := ioutil.ReadFile(path) attachmentBytes, err := os.ReadFile(path) // #nosec G304 -- path is secured by securejoin.SecureJoin above
if err != nil { if err != nil {
return []byte{}, &InternalError{Description: "Couldn't read attachment - please try again later"} return []byte{}, &InternalError{Description: "Couldn't read attachment - please try again later"}
} }
@ -1878,23 +1880,38 @@ func (s *SignalClient) UpdateProfile(number string, profileName string, base64Av
return err return err
} }
avatarTmpPath = s.avatarTmpDir + u.String() + "." + fType.Extension avatarFilename := u.String() + "." + fType.Extension
avatarTmpPath = filepath.Join(s.avatarTmpDir, avatarFilename)
f, err := os.Create(avatarTmpPath) avatarRoot, err := os.OpenRoot(s.avatarTmpDir)
if err != nil {
return err
}
defer avatarRoot.Close()
f, err := avatarRoot.Create(avatarFilename)
if err != nil { if err != nil {
return err return err
} }
defer f.Close()
if _, err := f.Write(avatarBytes); err != nil { if _, err := f.Write(avatarBytes); err != nil {
if closeErr := f.Close(); closeErr != nil {
log.Error("Couldn't close avatar tmp file: ", closeErr.Error())
}
cleanupTmpFiles([]string{avatarTmpPath}) cleanupTmpFiles([]string{avatarTmpPath})
return err return err
} }
if err := f.Sync(); err != nil { if err := f.Sync(); err != nil {
if closeErr := f.Close(); closeErr != nil {
log.Error("Couldn't close avatar tmp file: ", closeErr.Error())
}
cleanupTmpFiles([]string{avatarTmpPath})
return err
}
if err := f.Close(); err != nil {
cleanupTmpFiles([]string{avatarTmpPath}) cleanupTmpFiles([]string{avatarTmpPath})
return err return err
} }
f.Close()
} }
if s.signalCliMode == JsonRpc { if s.signalCliMode == JsonRpc {
@ -2093,23 +2110,38 @@ func (s *SignalClient) UpdateGroup(number string, groupId string, base64Avatar *
return err return err
} }
avatarTmpPath = s.avatarTmpDir + u.String() + "." + fType.Extension avatarFilename := u.String() + "." + fType.Extension
avatarTmpPath = filepath.Join(s.avatarTmpDir, avatarFilename)
f, err := os.Create(avatarTmpPath) avatarRoot, err := os.OpenRoot(s.avatarTmpDir)
if err != nil {
return err
}
defer avatarRoot.Close()
f, err := avatarRoot.Create(avatarFilename)
if err != nil { if err != nil {
return err return err
} }
defer f.Close()
if _, err := f.Write(avatarBytes); err != nil { if _, err := f.Write(avatarBytes); err != nil {
if closeErr := f.Close(); closeErr != nil {
log.Error("Couldn't close avatar tmp file: ", closeErr.Error())
}
cleanupTmpFiles([]string{avatarTmpPath}) cleanupTmpFiles([]string{avatarTmpPath})
return err return err
} }
if err := f.Sync(); err != nil { if err := f.Sync(); err != nil {
if closeErr := f.Close(); closeErr != nil {
log.Error("Couldn't close avatar tmp file: ", closeErr.Error())
}
cleanupTmpFiles([]string{avatarTmpPath})
return err
}
if err := f.Close(); err != nil {
cleanupTmpFiles([]string{avatarTmpPath}) cleanupTmpFiles([]string{avatarTmpPath})
return err return err
} }
f.Close()
} }
if s.signalCliMode == JsonRpc { if s.signalCliMode == JsonRpc {

View File

@ -224,7 +224,9 @@ func (r *JsonRpc2Client) ReceiveData(number string, receiveWebhookUrl string) {
str, err := connbuf.ReadString('\n') str, err := connbuf.ReadString('\n')
if err != nil { if err != nil {
log.Error("Lost connection to signal-cli...attempting to reconnect (", err.Error(), ")") log.Error("Lost connection to signal-cli...attempting to reconnect (", err.Error(), ")")
r.conn.Close() if err := r.conn.Close(); err != nil {
log.Warn("Error closing connection to signal-cli: ", err.Error())
}
err = r.Dial(r.address, 15) err = r.Dial(r.address, 15)
if err != nil { if err != nil {
log.Fatal("Unable to reconnect to signal-cli: ", err.Error(), "...aborting") log.Fatal("Unable to reconnect to signal-cli: ", err.Error(), "...aborting")
@ -236,7 +238,10 @@ func (r *JsonRpc2Client) ReceiveData(number string, receiveWebhookUrl string) {
log.Debug("json-rpc received data: ", str) log.Debug("json-rpc received data: ", str)
var resp1 JsonRpc2ReceivedMessage var resp1 JsonRpc2ReceivedMessage
json.Unmarshal([]byte(str), &resp1) if err := json.Unmarshal([]byte(str), &resp1); err != nil {
log.Warn("Couldn't parse received message: ", err.Error())
continue
}
if resp1.Method == "receive" { if resp1.Method == "receive" {
r.receivedMessagesMutex.Lock() r.receivedMessagesMutex.Lock()
for _, c := range r.receivedMessagesChannels { for _, c := range r.receivedMessagesChannels {

View File

@ -3,9 +3,10 @@ package main
import ( import (
"encoding/json" "encoding/json"
"flag" "flag"
"io/ioutil" "io"
"net/http" "net/http"
"os" "os"
"path/filepath"
"plugin" "plugin"
"strconv" "strconv"
@ -75,7 +76,9 @@ func main() {
err := utils.SetLogLevel(logLevel) err := utils.SetLogLevel(logLevel)
if err != nil { if err != nil {
log.Error("Couldn't set log level to '", logLevel, "'. Falling back to the info log level") log.Error("Couldn't set log level to '", logLevel, "'. Falling back to the info log level")
utils.SetLogLevel("info") if err := utils.SetLogLevel("info"); err != nil {
log.Error("Couldn't set fallback log level to 'info': ", err.Error())
}
} }
} }
@ -416,9 +419,9 @@ func main() {
c := cron.New() c := cron.New()
c.Schedule(schedule, cron.FuncJob(func() { c.Schedule(schedule, cron.FuncJob(func() {
accountsJsonPath := *signalCliConfig + "/data/accounts.json" accountsJsonPath := filepath.Clean(*signalCliConfig + "/data/accounts.json")
if _, err := os.Stat(accountsJsonPath); err == nil { if _, err := os.Stat(accountsJsonPath); err == nil {
signalCliConfigJsonData, err := ioutil.ReadFile(accountsJsonPath) signalCliConfigJsonData, err := os.ReadFile(accountsJsonPath)
if err != nil { if err != nil {
log.Fatal("AUTO_RECEIVE_SCHEDULE: Couldn't read accounts.json: ", err.Error()) log.Fatal("AUTO_RECEIVE_SCHEDULE: Couldn't read accounts.json: ", err.Error())
} }
@ -452,8 +455,10 @@ func main() {
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
jsonResp, err := ioutil.ReadAll(resp.Body) jsonResp, err := io.ReadAll(resp.Body)
resp.Body.Close() if closeErr := resp.Body.Close(); closeErr != nil {
log.Error("AUTO_RECEIVE_SCHEDULE: Couldn't close response body: ", closeErr.Error())
}
if err != nil { if err != nil {
log.Error("AUTO_RECEIVE_SCHEDULE: Couldn't read json response: ", err.Error()) log.Error("AUTO_RECEIVE_SCHEDULE: Couldn't read json response: ", err.Error())
continue continue
@ -479,5 +484,7 @@ func main() {
c.Start() c.Start()
} }
router.Run() if err := router.Run(); err != nil {
log.Fatal("Couldn't start HTTP router: ", err.Error())
}
} }

View File

@ -2,7 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil" "os"
"os/exec" "os/exec"
"strings" "strings"
@ -104,7 +104,7 @@ func main() {
signalCliIgnoreAvatars, signalCliIgnoreStickers, tcpPort, signalCliIgnoreAvatars, signalCliIgnoreStickers, tcpPort,
supervisorctlProgramName, supervisorctlProgramName) supervisorctlProgramName, supervisorctlProgramName)
err = ioutil.WriteFile(supervisorctlConfigFilename, []byte(supervisorctlConfig), 0644) err = os.WriteFile(supervisorctlConfigFilename, []byte(supervisorctlConfig), 0600)
if err != nil { if err != nil {
log.Fatal("Couldn't write ", supervisorctlConfigFilename, ": ", err.Error()) log.Fatal("Couldn't write ", supervisorctlConfigFilename, ": ", err.Error())
} }

View File

@ -2,9 +2,10 @@ package utils
import ( import (
"errors" "errors"
"gopkg.in/yaml.v2"
"io/ioutil"
"os" "os"
"path/filepath"
"gopkg.in/yaml.v2"
) )
type SignalCliTrustMode int type SignalCliTrustMode int
@ -55,9 +56,10 @@ func NewSignalCliApiConfig() *SignalCliApiConfig {
} }
func (c *SignalCliApiConfig) Load(path string) error { func (c *SignalCliApiConfig) Load(path string) error {
c.path = path cleanPath := filepath.Clean(path)
if _, err := os.Stat(path); err == nil { c.path = cleanPath
data, err := ioutil.ReadFile(path) if _, err := os.Stat(cleanPath); err == nil {
data, err := os.ReadFile(cleanPath)
if err != nil { if err != nil {
return err return err
} }
@ -92,5 +94,5 @@ func (c *SignalCliApiConfig) Persist() error {
return err return err
} }
return ioutil.WriteFile(c.path, out, 0644) return os.WriteFile(c.path, out, 0600)
} }

View File

@ -2,7 +2,8 @@ package utils
import ( import (
"errors" "errors"
"io/ioutil" "os"
"path/filepath"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -26,7 +27,7 @@ func NewJsonRpc2ClientConfig() *JsonRpc2ClientConfig {
} }
func (c *JsonRpc2ClientConfig) Load(path string) error { func (c *JsonRpc2ClientConfig) Load(path string) error {
data, err := ioutil.ReadFile(path) data, err := os.ReadFile(filepath.Clean(path))
if err != nil { if err != nil {
return err return err
} }
@ -69,5 +70,5 @@ func (c *JsonRpc2ClientConfig) Persist(path string) error {
return err return err
} }
return ioutil.WriteFile(path, out, 0644) return os.WriteFile(filepath.Clean(path), out, 0600)
} }

View File

@ -1,7 +1,8 @@
package utils package utils
import ( import (
"io/ioutil" "io"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -25,33 +26,48 @@ type PluginConfigs struct {
} }
func (c *PluginConfigs) Load(baseDirectory string) error { func (c *PluginConfigs) Load(baseDirectory string) error {
baseDirectory = filepath.Clean(baseDirectory)
root, err := os.OpenRoot(baseDirectory)
if err != nil {
return err
}
defer root.Close()
err := filepath.Walk(baseDirectory, func(path string, info os.FileInfo, err error) error { err = filepath.WalkDir(baseDirectory, func(path string, d fs.DirEntry, err error) error {
if info.IsDir() { if err != nil {
return err
}
if d.IsDir() {
return nil return nil
} }
if filepath.Ext(path) != ".def" { if filepath.Ext(path) != ".def" {
return nil return nil
} }
if _, err := os.Stat(path); err == nil { relPath, err := filepath.Rel(baseDirectory, path)
data, err := ioutil.ReadFile(path) if err != nil {
return err
}
f, err := root.Open(relPath)
if err != nil {
return err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil { if err != nil {
return err return err
} }
var pluginConfig PluginConfig var pluginConfig PluginConfig
pluginConfig.Version = 1 pluginConfig.Version = 1 // default; overridden by yaml if present
err = yaml.Unmarshal(data, &pluginConfig) if err = yaml.Unmarshal(data, &pluginConfig); err != nil {
if err != nil {
return err return err
} }
pluginConfig.ScriptPath = strings.TrimSuffix(path, filepath.Ext(path)) + ".lua" pluginConfig.ScriptPath = strings.TrimSuffix(path, filepath.Ext(path)) + ".lua"
c.Configs = append(c.Configs, pluginConfig) c.Configs = append(c.Configs, pluginConfig)
}
return nil return nil
}) })
return err return err
} }

View File

@ -1,10 +1,10 @@
package utils package utils
import ( import (
"os"
"strconv"
"errors" "errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"os"
"strconv"
) )
func GetEnv(key string, defaultVal string) string { func GetEnv(key string, defaultVal string) string {

View File

@ -15,7 +15,6 @@ func TestIsPhoneNumber(t *testing.T) {
expectEqual(t, res, true) expectEqual(t, res, true)
} }
func TestIsPhoneNumberWithSpaces(t *testing.T) { func TestIsPhoneNumberWithSpaces(t *testing.T) {
res := IsPhoneNumber("+ 12345678") res := IsPhoneNumber("+ 12345678")
expectEqual(t, res, true) expectEqual(t, res, true)