rdio-scanner/server/options.go
Chrystian Huot 2fe0758c22 Add API Access Policy and update copyright information
- Introduced a new document, API_ACCESS_POLICY.md, outlining the terms of use for the WebSocket API, including license, scope of restriction, authorized access, authentication, prohibited use, enforcement, and contact information.
- Updated README.md to highlight the restricted nature of the WebSocket API and link to the new access policy.
- Changed copyright information in multiple server files to reflect the new email address for Chrystian Huot.
- Added comments in server files to indicate the WebSocket API access policy and its restrictions.
2026-01-03 09:24:21 -05:00

424 lines
11 KiB
Go

// Copyright (C) 2019-2026 Chrystian Huot <chrystian.huot@saubeo.solutions>
//
// 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 <http://www.gnu.org/licenses/>
//
// WebSocket API Access Policy:
// This WebSocket API is reserved exclusively for Saubeo Solutions and its native applications.
// Unauthorized access is strictly prohibited.
// See API_ACCESS_POLICY.md for full terms.
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"math/rand"
"sync"
"golang.org/x/crypto/bcrypt"
)
type Options struct {
AudioConversion uint `json:"audioConversion"`
AutoPopulate bool `json:"autoPopulate"`
Branding string `json:"branding"`
DimmerDelay uint `json:"dimmerDelay"`
DisableDuplicateDetection bool `json:"disableDuplicateDetection"`
DuplicateDetectionTimeFrame uint `json:"duplicateDetectionTimeFrame"`
Email string `json:"email"`
KeypadBeeps string `json:"keypadBeeps"`
MaxClients uint `json:"maxClients"`
PlaybackGoesLive bool `json:"playbackGoesLive"`
PruneDays uint `json:"pruneDays"`
ShowListenersCount bool `json:"showListenersCount"`
SortTalkgroups bool `json:"sortTalkgroups"`
Time12hFormat bool `json:"time12hFormat"`
adminPassword string
adminPasswordNeedChange bool
mutex sync.Mutex
secret string
}
const (
AUDIO_CONVERSION_DISABLED = 0
AUDIO_CONVERSION_ENABLED = 1
AUDIO_CONVERSION_ENABLED_NORM = 2
AUDIO_CONVERSION_ENABLED_LOUD_NORM = 3
)
func NewOptions() *Options {
return &Options{
mutex: sync.Mutex{},
}
}
func (options *Options) FromMap(m map[string]any) *Options {
options.mutex.Lock()
defer options.mutex.Unlock()
switch v := m["audioConversion"].(type) {
case float64:
options.AudioConversion = uint(v)
default:
options.MaxClients = defaults.options.audioConversion
}
switch v := m["autoPopulate"].(type) {
case bool:
options.AutoPopulate = v
default:
options.AutoPopulate = defaults.options.autoPopulate
}
switch v := m["branding"].(type) {
case string:
options.Branding = v
}
switch v := m["dimmerDelay"].(type) {
case float64:
options.DimmerDelay = uint(v)
default:
options.DimmerDelay = defaults.options.dimmerDelay
}
switch v := m["disableAudioConversion"].(type) {
case bool:
if v {
options.AudioConversion = 2
} else {
options.AudioConversion = 0
}
}
switch v := m["disableDuplicateDetection"].(type) {
case bool:
options.DisableDuplicateDetection = v
default:
options.DisableDuplicateDetection = defaults.options.disableDuplicateDetection
}
switch v := m["duplicateDetectionTimeFrame"].(type) {
case float64:
options.DuplicateDetectionTimeFrame = uint(v)
default:
options.DuplicateDetectionTimeFrame = defaults.options.duplicateDetectionTimeFrame
}
switch v := m["email"].(type) {
case string:
options.Email = v
}
switch v := m["keypadBeeps"].(type) {
case string:
options.KeypadBeeps = v
default:
options.KeypadBeeps = defaults.options.keypadBeeps
}
switch v := m["maxClients"].(type) {
case float64:
options.MaxClients = uint(v)
default:
options.MaxClients = defaults.options.maxClients
}
switch v := m["playbackGoesLive"].(type) {
case bool:
options.PlaybackGoesLive = v
}
switch v := m["pruneDays"].(type) {
case float64:
options.PruneDays = uint(v)
default:
options.PruneDays = defaults.options.pruneDays
}
switch v := m["showListenersCount"].(type) {
case bool:
options.ShowListenersCount = v
default:
options.ShowListenersCount = defaults.options.showListenersCount
}
switch v := m["sortTalkgroups"].(type) {
case bool:
options.SortTalkgroups = v
default:
options.SortTalkgroups = defaults.options.sortTalkgroups
}
switch v := m["time12hFormat"].(type) {
case bool:
options.Time12hFormat = v
default:
options.Time12hFormat = defaults.options.time12hFormat
}
return options
}
func (options *Options) Read(db *Database) error {
var (
defaultPassword []byte
err error
f any
query string
rows *sql.Rows
key sql.NullString
value sql.NullString
)
options.mutex.Lock()
defer options.mutex.Unlock()
defaultPassword, _ = bcrypt.GenerateFromPassword([]byte(defaults.adminPassword), bcrypt.DefaultCost)
options.adminPassword = string(defaultPassword)
options.adminPasswordNeedChange = defaults.adminPasswordNeedChange
options.AudioConversion = defaults.options.audioConversion
options.AutoPopulate = defaults.options.autoPopulate
options.DimmerDelay = defaults.options.dimmerDelay
options.DisableDuplicateDetection = defaults.options.disableDuplicateDetection
options.DuplicateDetectionTimeFrame = defaults.options.duplicateDetectionTimeFrame
options.KeypadBeeps = defaults.options.keypadBeeps
options.MaxClients = defaults.options.maxClients
options.PlaybackGoesLive = defaults.options.playbackGoesLive
options.PruneDays = defaults.options.pruneDays
options.ShowListenersCount = defaults.options.showListenersCount
options.SortTalkgroups = defaults.options.sortTalkgroups
formatError := errorFormatter("options", "read")
newSecret := func(n uint) string {
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&")
s := make([]rune, n)
for i := range s {
s[i] = letters[rand.Intn(len(letters))]
}
return string(s)
}
query = `SELECT "key", "value" FROM "options"`
if rows, err = db.Sql.Query(query); err != nil {
return formatError(err, query)
}
for rows.Next() {
if err = rows.Scan(&key, &value); err != nil {
continue
}
if !key.Valid || !value.Valid {
continue
}
switch key.String {
case "adminPassword":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case string:
options.adminPassword = v
}
}
case "adminPasswordNeedChange":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case bool:
options.adminPasswordNeedChange = v
}
}
case "audioConversion":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case float64:
options.AudioConversion = uint(v)
}
}
case "autoPopulate":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case bool:
options.AutoPopulate = v
}
}
case "branding":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case string:
options.Branding = v
}
}
case "dimmerDelay":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case float64:
options.DimmerDelay = uint(v)
}
}
case "disableDuplicateDetection":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case bool:
options.DisableDuplicateDetection = v
}
}
case "duplicateDetectionTimeframe":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case float64:
options.DuplicateDetectionTimeFrame = uint(v)
}
}
case "email":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case string:
options.Email = v
}
}
case "keypadBeeps":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case string:
options.KeypadBeeps = v
}
}
case "maxClients":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case float64:
options.MaxClients = uint(v)
}
}
case "playbackGoesLive":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case bool:
options.PlaybackGoesLive = v
}
}
case "pruneDays":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case float64:
options.PruneDays = uint(v)
}
}
case "secret":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
const n = 256
switch v := f.(type) {
case string:
if len(v) == n {
options.secret = v
} else {
options.secret = newSecret(n)
}
default:
options.secret = newSecret(n)
}
}
case "showListenersCount":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case bool:
options.ShowListenersCount = v
}
}
case "sortTalkgroups":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case bool:
options.SortTalkgroups = v
}
}
case "time12hFormat":
if err = json.Unmarshal([]byte(value.String), &f); err == nil {
switch v := f.(type) {
case bool:
options.Time12hFormat = v
}
}
}
}
return nil
}
func (options *Options) Write(db *Database) error {
var (
err error
res sql.Result
tx *sql.Tx
)
options.mutex.Lock()
defer options.mutex.Unlock()
formatError := errorFormatter("options", "write")
set := func(key string, val any) {
if val, err = json.Marshal(val); err == nil {
switch v := val.(type) {
case string:
val = escapeQuotes(v)
}
query := fmt.Sprintf(`UPDATE "options" SET "value" = '%s' WHERE "key" = '%s'`, val, key)
if res, err = tx.Exec(query); err == nil {
if i, err := res.RowsAffected(); err == nil && i == 0 {
query = fmt.Sprintf(`INSERT INTO "options" ("key", "value") VALUES ('%s', '%s')`, key, val)
if _, err = tx.Exec(query); err != nil {
log.Println(formatError(err, query))
}
}
}
}
}
if tx, err = db.Sql.Begin(); err != nil {
return formatError(err, "")
}
set("adminPassword", options.adminPassword)
set("adminPasswordNeedChange", options.adminPasswordNeedChange)
set("autoPopulate", options.AutoPopulate)
set("branding", options.Branding)
set("dimmerDelay", options.DimmerDelay)
set("disableDuplicateDetection", options.DisableDuplicateDetection)
set("duplicateDetectionTimeFrame", options.DuplicateDetectionTimeFrame)
set("email", options.Email)
set("keypadBeeps", options.KeypadBeeps)
set("maxClients", options.MaxClients)
set("playbackGoesLive", options.PlaybackGoesLive)
set("pruneDays", options.PruneDays)
set("secret", options.secret)
set("showListenersCount", options.ShowListenersCount)
set("sortTalkgroups", options.SortTalkgroups)
set("time12hFormat", options.Time12hFormat)
if err = tx.Commit(); err != nil {
tx.Rollback()
return formatError(err, "")
}
return nil
}