Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preliminary OpenBridge support #82

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -241,35 +241,6 @@ jobs:

- run: make benchmark

backend-race-tests:
runs-on: ubuntu-latest
needs: frontend
permissions:
contents: read
checks: write

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version-file: go.mod

- name: Copy built frontend
uses: actions/download-artifact@v3
with:
name: frontend
path: internal/http/frontend/dist

- run: go install github.com/tinylib/msgp
- run: go generate ./...

- name: Race tests
run: |
go test ./... -race

backend-unit-tests:
runs-on: ubuntu-latest
needs: frontend
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ services:
ports:
- 3005:3005
- 62031:62031/udp
- 62032:62032/udp
volumes:
postgres:
19 changes: 15 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type Config struct {
PasswordSalt string
ListenAddr string
DMRPort int
OpenBridgePort int
HTTPPort int
CORSHosts []string
TrustedProxies []string
Expand Down Expand Up @@ -80,6 +81,12 @@ func loadConfig() Config {
httpPort = 0
}

portStr = os.Getenv("OPENBRIDGE_PORT")
openBridgePort, err := strconv.ParseInt(portStr, 10, 0)
if err != nil {
openBridgePort = 0
}

tmpConfig := Config{
RedisHost: os.Getenv("REDIS_HOST"),
postgresUser: os.Getenv("PG_USER"),
Expand All @@ -97,6 +104,7 @@ func loadConfig() Config {
InitialAdminUserPassword: os.Getenv("INIT_ADMIN_USER_PASSWORD"),
RedisPassword: os.Getenv("REDIS_PASSWORD"),
Debug: os.Getenv("DEBUG") != "",
OpenBridgePort: int(openBridgePort),
}
if tmpConfig.RedisHost == "" {
tmpConfig.RedisHost = "localhost:6379"
Expand All @@ -119,23 +127,26 @@ func loadConfig() Config {
tmpConfig.PostgresDSN = "host=" + tmpConfig.postgresHost + " port=" + strconv.FormatInt(int64(tmpConfig.postgresPort), 10) + " user=" + tmpConfig.postgresUser + " dbname=" + tmpConfig.postgresDatabase + " password=" + tmpConfig.postgresPassword
if tmpConfig.strSecret == "" {
tmpConfig.strSecret = "secret"
logging.GetLogger(logging.Error).Log(loadConfig, "Session secret not set, using INSECURE default")
logging.GetLogger(logging.Error).Log(loadConfig, "SECRET not set, using INSECURE default")
}
if tmpConfig.PasswordSalt == "" {
tmpConfig.PasswordSalt = "salt"
logging.GetLogger(logging.Error).Log(loadConfig, "Password salt not set, using INSECURE default")
logging.GetLogger(logging.Error).Log(loadConfig, "PASSWORD_SALT not set, using INSECURE default")
}
if tmpConfig.ListenAddr == "" {
tmpConfig.ListenAddr = "0.0.0.0"
}
if tmpConfig.DMRPort == 0 {
tmpConfig.DMRPort = 62031
}
if tmpConfig.OpenBridgePort == 0 {
logging.GetLogger(logging.Error).Log(loadConfig, "OPENBRIDGE_PORT not set, disabling OpenBridge support")
}
if tmpConfig.HTTPPort == 0 {
tmpConfig.HTTPPort = 3005
}
if tmpConfig.InitialAdminUserPassword == "" {
logging.GetLogger(logging.Error).Log(loadConfig, "Initial admin user password not set, using auto-generated password")
logging.GetLogger(logging.Error).Log(loadConfig, "INIT_ADMIN_USER_PASSWORD not set, using auto-generated password")
const randLen = 15
const randNums = 4
const randSpecial = 2
Expand All @@ -147,7 +158,7 @@ func loadConfig() Config {
}
if tmpConfig.RedisPassword == "" {
tmpConfig.RedisPassword = "password"
logging.GetLogger(logging.Error).Log(loadConfig, "Redis password not set, using INSECURE default")
logging.GetLogger(logging.Error).Log(loadConfig, "REDIS_PASSWORD not set, using INSECURE default")
}
// CORS_HOSTS is a comma separated list of hosts that are allowed to access the API
corsHosts := os.Getenv("CORS_HOSTS")
Expand Down
2 changes: 1 addition & 1 deletion internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func MakeDB() *gorm.DB {
}
}

err = db.AutoMigrate(&models.AppSettings{}, &models.Call{}, &models.Repeater{}, &models.Talkgroup{}, &models.User{})
err = db.AutoMigrate(&models.AppSettings{}, &models.Call{}, &models.Peer{}, &models.Repeater{}, &models.Talkgroup{}, &models.User{})
if err != nil {
logging.GetLogger(logging.Error).Logf(MakeDB, "Could not migrate database: %s", err)
os.Exit(1)
Expand Down
104 changes: 104 additions & 0 deletions internal/db/models/peer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// DMRHub - Run a DMR network server in a single binary
// Copyright (C) 2023 Jacob McSwain
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// The source code is available at <https://github.com/USA-RedDragon/DMRHub>

package models

import (
"encoding/json"
"time"

"gorm.io/gorm"
"k8s.io/klog/v2"
)

// Peer is the model for an OpenBridge DMR peer
//
//go:generate msgp
type Peer struct {
ID uint `json:"id" gorm:"primaryKey" msg:"id"`
LastPing time.Time `json:"last_ping_time" msg:"last_ping"`
IP string `json:"-" gorm:"-" msg:"ip"`
Port int `json:"-" gorm:"-" msg:"port"`
Password string `json:"-" msg:"-"`
Owner User `json:"owner" gorm:"foreignKey:OwnerID" msg:"-"`
OwnerID uint `json:"-" msg:"-"`
Ingress bool `json:"ingress" msg:"-"`
Egress bool `json:"egress" msg:"-"`
CreatedAt time.Time `json:"created_at" msg:"-"`
UpdatedAt time.Time `json:"-" msg:"-"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index" msg:"-"`
}

func (p *Peer) String() string {
jsn, err := json.Marshal(p)
if err != nil {
klog.Errorf("Failed to marshal peer to json: %s", err)
return ""
}
return string(jsn)
}

func ListPeers(db *gorm.DB) []Peer {
var peers []Peer
db.Preload("Owner").Order("id asc").Find(&peers)
return peers
}

func CountPeers(db *gorm.DB) int {
var count int64
db.Model(&Peer{}).Count(&count)
return int(count)
}

func GetUserPeers(db *gorm.DB, id uint) []Peer {
var peers []Peer
db.Preload("Owner").Where("owner_id = ?", id).Order("id asc").Find(&peers)
return peers
}

func CountUserPeers(db *gorm.DB, id uint) int {
var count int64
db.Model(&Peer{}).Where("owner_id = ?", id).Count(&count)
return int(count)
}

func FindPeerByID(db *gorm.DB, id uint) Peer {
var peer Peer
db.Preload("Owner").First(&peer, id)
return peer
}

func PeerExists(db *gorm.DB, peer Peer) bool {
var count int64
db.Model(&Peer{}).Where("id = ?", peer.ID).Limit(1).Count(&count)
return count > 0
}

func PeerIDExists(db *gorm.DB, id uint) bool {
var count int64
db.Model(&Peer{}).Where("id = ?", id).Limit(1).Count(&count)
return count > 0
}

func DeletePeer(db *gorm.DB, id uint) {
tx := db.Unscoped().Delete(&Peer{ID: id})
if tx.Error != nil {
klog.Errorf("Error deleting repeater: %s", tx.Error)
}
}
68 changes: 68 additions & 0 deletions internal/dmr/servers/openbridge/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// DMRHub - Run a DMR network server in a single binary
// Copyright (C) 2023 Jacob McSwain
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// The source code is available at <https://github.com/USA-RedDragon/DMRHub>

package openbridge

import (
"context"
"errors"
"fmt"

"github.com/USA-RedDragon/DMRHub/internal/db/models"
"github.com/redis/go-redis/v9"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"k8s.io/klog/v2"
)

type redisClient struct {
Redis *redis.Client
Tracer trace.Tracer
}

var (
errNoSuchPeer = errors.New("no such peer")
errUnmarshalPeer = errors.New("unmarshal peer")
errCastPeer = errors.New("unable to cast peer id")
)

func makeRedisClient(redis *redis.Client) redisClient {
return redisClient{
Redis: redis,
Tracer: otel.Tracer("openbridge-redis"),
}
}

func (s *redisClient) getPeer(ctx context.Context, peerID uint) (models.Peer, error) {
ctx, span := otel.Tracer("DMRHub").Start(ctx, "Server.handlePacket")
defer span.End()

peerBits, err := s.Redis.Get(ctx, fmt.Sprintf("openbridge:peer:%d", peerID)).Result()
if err != nil {
klog.Errorf("Error getting peer from redis", err)
return models.Peer{}, errNoSuchPeer
}
var peer models.Peer
_, err = peer.UnmarshalMsg([]byte(peerBits))
if err != nil {
klog.Errorf("Error unmarshalling peer", err)
return models.Peer{}, errUnmarshalPeer
}
return peer, nil
}
Loading