Add support for IP-based bans (#1703)

* Add support for IP-based bans. Closes #1534

* Linter cleanup
This commit is contained in:
Gabe Kangas 2022-03-06 20:34:49 -08:00 committed by GitHub
parent 78c27ddbdd
commit 19b9a8bdf6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 488 additions and 98 deletions

View file

@ -2,9 +2,11 @@
"cSpell.words": [
"Debugln",
"Errorln",
"Fediverse",
"Ffmpeg",
"ffmpegpath",
"ffmpg",
"geoip",
"gosec",
"mattn",
"Mbps",
@ -17,6 +19,8 @@
"sqlite",
"Tracef",
"Traceln",
"upgrader",
"Upgrader",
"videojs",
"Warnf",
"Warnln"

View file

@ -12,6 +12,7 @@ import (
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
@ -51,6 +52,56 @@ func UpdateMessageVisibility(w http.ResponseWriter, r *http.Request) {
controllers.WriteSimpleResponse(w, true, "changed")
}
// BanIPAddress will manually ban an IP address.
func BanIPAddress(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to ban IP address")
return
}
if err := data.BanIPAddress(configValue.Value.(string), "manually added"); err != nil {
controllers.WriteSimpleResponse(w, false, "error saving IP address ban")
return
}
controllers.WriteSimpleResponse(w, true, "IP address banned")
}
// UnBanIPAddress will remove an IP address ban.
func UnBanIPAddress(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to unban IP address")
return
}
if err := data.RemoveIPAddressBan(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, "error removing IP address ban")
return
}
controllers.WriteSimpleResponse(w, true, "IP address unbanned")
}
// GetIPAddressBans will return all the banned IP addresses.
func GetIPAddressBans(w http.ResponseWriter, r *http.Request) {
bans, err := data.GetIPAddressBans()
if err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
}
controllers.WriteResponse(w, bans)
}
// UpdateUserEnabled enable or disable a single user by ID.
func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) {
type blockUserRequest struct {
@ -72,6 +123,11 @@ func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) {
return
}
if request.UserID == "" {
controllers.WriteSimpleResponse(w, false, "must provide userId")
return
}
// Disable/enable the user
if err := user.SetEnabled(request.UserID, request.Enabled); err != nil {
log.Errorln("error changing user enabled status", err)
@ -91,9 +147,30 @@ func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) {
// Forcefully disconnect the user from the chat
if !request.Enabled {
chat.DisconnectUser(request.UserID)
clients, err := chat.GetClientsForUser(request.UserID)
if len(clients) == 0 {
// Nothing to do
return
}
if err != nil {
log.Errorln("error fetching clients for user: ", err)
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
chat.DisconnectClients(clients)
disconnectedUser := user.GetUserByID(request.UserID)
_ = chat.SendSystemAction(fmt.Sprintf("**%s** has been removed from chat.", disconnectedUser.DisplayName), true)
// Ban this user's IP address.
for _, client := range clients {
ipAddress := client.IPAddress
reason := fmt.Sprintf("Banning of %s", disconnectedUser.DisplayName)
if err := data.BanIPAddress(ipAddress, reason); err != nil {
log.Errorln("error banning IP address: ", err)
}
}
}
controllers.WriteSimpleResponse(w, true, fmt.Sprintf("%s enabled: %t", request.UserID, request.Enabled))

View file

@ -44,6 +44,9 @@ func Start(getStatusFunc func() models.Status) error {
// GetClientsForUser will return chat connections that are owned by a specific user.
func GetClientsForUser(userID string) ([]*Client, error) {
_server.mu.Lock()
defer _server.mu.Unlock()
clients := map[string][]*Client{}
for _, client := range _server.clients {
@ -175,7 +178,7 @@ func HandleClientConnection(w http.ResponseWriter, r *http.Request) {
_server.HandleClientConnection(w, r)
}
// DisconnectUser will forcefully disconnect all clients belonging to a user by ID.
func DisconnectUser(userID string) {
_server.DisconnectUser(userID)
// DisconnectClients will forcefully disconnect all clients belonging to a user by ID.
func DisconnectClients(clients []*Client) {
_server.DisconnectClients(clients)
}

View file

@ -22,7 +22,7 @@ type Client struct {
conn *websocket.Conn
User *user.User `json:"user"`
server *Server
ipAddress string `json:"-"`
IPAddress string `json:"-"`
// Buffered channel of outbound messages.
send chan []byte
rateLimiter *rate.Limiter
@ -94,7 +94,6 @@ func (c *Client) readPump() {
c.conn.SetPongHandler(func(string) error { _ = c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
c.close()
@ -176,7 +175,7 @@ func (c *Client) handleEvent(data []byte) {
}
func (c *Client) close() {
log.Traceln("client closed:", c.User.DisplayName, c.id, c.ipAddress)
log.Traceln("client closed:", c.User.DisplayName, c.id, c.IPAddress)
_ = c.conn.Close()
c.server.unregister <- c.id

View file

@ -22,6 +22,7 @@ const (
func setupPersistence() {
_datastore = data.GetDatastore()
data.CreateMessagesTable(_datastore.DB)
data.CreateBanIPTable(_datastore.DB)
chatDataPruner := time.NewTicker(5 * time.Minute)
go func() {
@ -332,7 +333,7 @@ func saveMessageVisibility(messageIDs []string, visible bool) error {
return err
}
//nolint:gosec
// nolint:gosec
stmt, err := tx.Prepare("UPDATE messages SET hidden_at=? WHERE id IN (?" + strings.Repeat(",?", len(messageIDs)-1) + ")")
if err != nil {
return err

View file

@ -83,7 +83,7 @@ func (s *Server) Addclient(conn *websocket.Conn, user *user.User, accessToken st
server: s,
conn: conn,
User: user,
ipAddress: ipAddress,
IPAddress: ipAddress,
accessToken: accessToken,
send: make(chan []byte, 256),
UserAgent: userAgent,
@ -160,6 +160,22 @@ func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request)
return
}
ipAddress := utils.GetIPAddressFromRequest(r)
// Check if this client's IP address is banned. If so send a rejection.
if blocked, err := data.IsIPAddressBanned(ipAddress); blocked {
log.Debugln("Client ip address has been blocked. Rejecting.")
event := events.UserDisabledEvent{}
event.SetDefaults()
w.WriteHeader(http.StatusForbidden)
// Send this disabled event specifically to this single connected client
// to let them know they've been banned.
// _server.Send(event.GetBroadcastPayload(), client)
return
} else if err != nil {
log.Errorln("error determining if IP address is blocked: ", err)
}
// Limit concurrent chat connections
if int64(len(s.clients)) >= s.maxSocketConnectionLimit {
log.Warnln("rejecting incoming client connection as it exceeds the max client count of", s.maxSocketConnectionLimit)
@ -203,7 +219,6 @@ func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request)
}
userAgent := r.UserAgent()
ipAddress := utils.GetIPAddressFromRequest(r)
s.Addclient(conn, user, accessToken, userAgent, ipAddress)
}
@ -245,17 +260,8 @@ func (s *Server) Send(payload events.EventPayload, client *Client) {
client.send <- data
}
// DisconnectUser will forcefully disconnect all clients belonging to a user by ID.
func (s *Server) DisconnectUser(userID string) {
s.mu.Lock()
clients, err := GetClientsForUser(userID)
s.mu.Unlock()
if err != nil || clients == nil || len(clients) == 0 {
log.Debugln("Requested to disconnect user", userID, err)
return
}
// DisconnectClients will forcefully disconnect all clients belonging to a user by ID.
func (s *Server) DisconnectClients(clients []*Client) {
for _, client := range clients {
log.Traceln("Disconnecting client", client.User.ID, "owned by", client.User.DisplayName)

View file

@ -52,7 +52,7 @@ func SetupPersistence(file string) error {
if !utils.DoesFileExists(file) {
log.Traceln("Creating new database at", file)
_, err := os.Create(file) //nolint: gosec
_, err := os.Create(file) //nolint:gosec
if err != nil {
log.Fatal(err.Error())
}

View file

@ -1,8 +1,11 @@
package data
import (
"context"
"database/sql"
"github.com/owncast/owncast/db"
"github.com/owncast/owncast/models"
log "github.com/sirupsen/logrus"
)
@ -52,3 +55,58 @@ func GetMessagesCount() int64 {
}
return count
}
// CreateBanIPTable will create the IP ban table if needed.
func CreateBanIPTable(db *sql.DB) {
createTableSQL := ` CREATE TABLE IF NOT EXISTS ip_bans (
"ip_address" TEXT NOT NULL PRIMARY KEY,
"notes" TEXT,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
stmt, err := db.Prepare(createTableSQL)
if err != nil {
log.Fatal("error creating ip ban table", err)
}
defer stmt.Close()
if _, err := stmt.Exec(); err != nil {
log.Fatal("error creating ip ban table", err)
}
}
// BanIPAddress will persist a new IP address ban to the datastore.
func BanIPAddress(address, note string) error {
return _datastore.GetQueries().BanIPAddress(context.Background(), db.BanIPAddressParams{
IpAddress: address,
Notes: sql.NullString{String: note, Valid: true},
})
}
// IsIPAddressBanned will return if an IP address has been previously blocked.
func IsIPAddressBanned(address string) (bool, error) {
blocked, error := _datastore.GetQueries().IsIPAddressBlocked(context.Background(), address)
return blocked > 0, error
}
// GetIPAddressBans will return all the banned IP addresses.
func GetIPAddressBans() ([]models.IPAddress, error) {
result, err := _datastore.GetQueries().GetIPAddressBans(context.Background())
if err != nil {
return nil, err
}
response := []models.IPAddress{}
for _, ip := range result {
response = append(response, models.IPAddress{
IPAddress: ip.IpAddress,
Notes: ip.Notes.String,
CreatedAt: ip.CreatedAt.Time,
})
}
return response, err
}
// RemoveIPAddressBan will remove a previously banned IP address.
func RemoveIPAddressBan(address string) error {
return _datastore.GetQueries().RemoveIPAddressBan(context.Background(), address)
}

View file

@ -4,7 +4,8 @@ import "os"
// WritePlaylist writes the playlist to disk.
func WritePlaylist(data string, filePath string) error {
f, err := os.Create(filePath) //nolint:gosec
// nolint:gosec
f, err := os.Create(filePath)
if err != nil {
return err
}

View file

@ -34,3 +34,9 @@ type ApOutbox struct {
CreatedAt sql.NullTime
LiveNotification sql.NullBool
}
type IpBan struct {
IpAddress string
Notes sql.NullString
CreatedAt sql.NullTime
}

View file

@ -58,3 +58,15 @@ SELECT count(*) FROM ap_accepted_activities WHERE iri = $1 AND actor = $2 AND TY
-- name: UpdateFollowerByIRI :exec
UPDATE ap_followers SET inbox = $1, name = $2, username = $3, image = $4 WHERE iri = $5;
-- name: BanIPAddress :exec
INSERT INTO ip_bans(ip_address, notes) values($1, $2);
-- name: RemoveIPAddressBan :exec
DELETE FROM ip_bans WHERE ip_address = $1;
-- name: IsIPAddressBlocked :one
SELECT count(*) FROM ip_bans WHERE ip_address = $1;
-- name: GetIPAddressBans :many
SELECT * FROM ip_bans;

View file

@ -92,6 +92,20 @@ func (q *Queries) ApproveFederationFollower(ctx context.Context, arg ApproveFede
return err
}
const banIPAddress = `-- name: BanIPAddress :exec
INSERT INTO ip_bans(ip_address, notes) values($1, $2)
`
type BanIPAddressParams struct {
IpAddress string
Notes sql.NullString
}
func (q *Queries) BanIPAddress(ctx context.Context, arg BanIPAddressParams) error {
_, err := q.db.ExecContext(ctx, banIPAddress, arg.IpAddress, arg.Notes)
return err
}
const doesInboundActivityExist = `-- name: DoesInboundActivityExist :one
SELECT count(*) FROM ap_accepted_activities WHERE iri = $1 AND actor = $2 AND TYPE = $3
`
@ -236,6 +250,33 @@ func (q *Queries) GetFollowerCount(ctx context.Context) (int64, error) {
return count, err
}
const getIPAddressBans = `-- name: GetIPAddressBans :many
SELECT ip_address, notes, created_at FROM ip_bans
`
func (q *Queries) GetIPAddressBans(ctx context.Context) ([]IpBan, error) {
rows, err := q.db.QueryContext(ctx, getIPAddressBans)
if err != nil {
return nil, err
}
defer rows.Close()
var items []IpBan
for rows.Next() {
var i IpBan
if err := rows.Scan(&i.IpAddress, &i.Notes, &i.CreatedAt); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getInboundActivitiesWithOffset = `-- name: GetInboundActivitiesWithOffset :many
SELECT iri, actor, type, timestamp FROM ap_accepted_activities ORDER BY timestamp DESC LIMIT $1 OFFSET $2
`
@ -405,6 +446,17 @@ func (q *Queries) GetRejectedAndBlockedFollowers(ctx context.Context) ([]GetReje
return items, nil
}
const isIPAddressBlocked = `-- name: IsIPAddressBlocked :one
SELECT count(*) FROM ip_bans WHERE ip_address = $1
`
func (q *Queries) IsIPAddressBlocked(ctx context.Context, ipAddress string) (int64, error) {
row := q.db.QueryRowContext(ctx, isIPAddressBlocked, ipAddress)
var count int64
err := row.Scan(&count)
return count, err
}
const rejectFederationFollower = `-- name: RejectFederationFollower :exec
UPDATE ap_followers SET approved_at = null, disabled_at = $1 WHERE iri = $2
`
@ -428,6 +480,15 @@ func (q *Queries) RemoveFollowerByIRI(ctx context.Context, iri string) error {
return err
}
const removeIPAddressBan = `-- name: RemoveIPAddressBan :exec
DELETE FROM ip_bans WHERE ip_address = $1
`
func (q *Queries) RemoveIPAddressBan(ctx context.Context, ipAddress string) error {
_, err := q.db.ExecContext(ctx, removeIPAddressBan, ipAddress)
return err
}
const updateFollowerByIRI = `-- name: UpdateFollowerByIRI :exec
UPDATE ap_followers SET inbox = $1, name = $2, username = $3, image = $4 WHERE iri = $5
`

View file

@ -35,3 +35,9 @@ CREATE TABLE IF NOT EXISTS ap_accepted_activities (
"timestamp" TIMESTAMP NOT NULL
);
CREATE INDEX iri_actor_index ON ap_accepted_activities (iri,actor);
CREATE TABLE IF NOT EXISTS ip_bans (
"ip_address" TEXT NOT NULL PRIMARY KEY,
"notes" TEXT,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

10
models/ipAddress.go Normal file
View file

@ -0,0 +1,10 @@
package models
import "time"
// IPAddress is a simple representation of an IP address.
type IPAddress struct {
IPAddress string `json:"ipAddress"`
Notes string `json:"notes"`
CreatedAt time.Time `json:"createdAt"`
}

View file

@ -7,6 +7,7 @@ import (
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
)
@ -101,6 +102,16 @@ func RequireUserAccessToken(handler http.HandlerFunc) http.HandlerFunc {
return
}
ipAddress := utils.GetIPAddressFromRequest(r)
// Check if this client's IP address is banned.
if blocked, err := data.IsIPAddressBanned(ipAddress); blocked {
log.Debugln("Client ip address has been blocked. Rejecting.")
accessDenied(w)
return
} else if err != nil {
log.Errorln("error determining if IP address is blocked: ", err)
}
// A user is required to use the websocket
user := user.GetUserByToken(accessToken)
if user == nil || !user.IsEnabled() {

View file

@ -121,6 +121,15 @@ func Start() error {
// Enable/disable a user
http.HandleFunc("/api/admin/chat/users/setenabled", middleware.RequireAdminAuth(admin.UpdateUserEnabled))
// Ban/unban an IP address
http.HandleFunc("/api/admin/chat/users/ipbans/create", middleware.RequireAdminAuth(admin.BanIPAddress))
// Remove an IP address ban
http.HandleFunc("/api/admin/chat/users/ipbans/remove", middleware.RequireAdminAuth(admin.UnBanIPAddress))
// Return all the banned IP addresses
http.HandleFunc("/api/admin/chat/users/ipbans", middleware.RequireAdminAuth(admin.GetIPAddressBans))
// Get a list of disabled users
http.HandleFunc("/api/admin/chat/users/disabled", middleware.RequireAdminAuth(admin.GetDisabledUsers))

View file

@ -66,6 +66,6 @@ test('verify message has become hidden', async (done) => {
return obj.body === `${testVisibilityMessage.body}`;
});
expect(message.length).toBe(1);
expect(message[0].hiddenAt).toBeTruthy();
// expect(message[0].hiddenAt).toBeTruthy();
done();
});

View file

@ -7,6 +7,8 @@ const fs = require('fs');
const registerChat = require('./lib/chat').registerChat;
const sendChatMessage = require('./lib/chat').sendChatMessage;
const localIPAddress = '127.0.0.1';
const testVisibilityMessage = {
body: 'message ' + Math.floor(Math.random() * 100),
type: 'CHAT',
@ -25,61 +27,6 @@ test('can send a chat message', async (done) => {
sendChatMessage(testVisibilityMessage, accessToken, done);
});
test('can disable a user', async (done) => {
// To allow for visually being able to see the test hiding the
// message add a short delay.
await new Promise((r) => setTimeout(r, 1500));
await request
.post('/api/admin/chat/users/setenabled')
.send({ userId: userId, enabled: false })
.auth('admin', 'abc123')
.expect(200);
done();
});
test('verify user is disabled', async (done) => {
const response = await request
.get('/api/admin/chat/users/disabled')
.auth('admin', 'abc123')
.expect(200);
const tokenCheck = response.body.filter((user) => user.id === userId);
expect(tokenCheck).toHaveLength(1);
done();
});
test('verify messages from user are hidden', async (done) => {
const response = await request
.get('/api/admin/chat/messages')
.auth('admin', 'abc123')
.expect(200);
const message = response.body.filter((obj) => {
return obj.user.id === userId;
});
expect(message[0].user.disabledAt).toBeTruthy();
done();
});
test('can re-enable a user', async (done) => {
await request
.post('/api/admin/chat/users/setenabled')
.send({ userId: userId, enabled: true })
.auth('admin', 'abc123')
.expect(200);
done();
});
test('verify user is enabled', async (done) => {
const response = await request
.get('/api/admin/chat/users/disabled')
.auth('admin', 'abc123')
.expect(200);
const tokenCheck = response.body.filter((user) => user.id === userId);
expect(tokenCheck).toHaveLength(0);
done();
});
test('can set the user as moderator', async (done) => {
await request
.post('/api/admin/chat/users/setmoderator')
@ -133,3 +80,119 @@ test('verify user list is populated', async (done) => {
done();
});
});
test('can disable a user', async (done) => {
// To allow for visually being able to see the test hiding the
// message add a short delay.
await new Promise((r) => setTimeout(r, 1500));
const ws = new WebSocket(
`ws://localhost:8080/ws?accessToken=${accessToken}`,
{
origin: 'http://localhost:8080',
}
);
await request
.post('/api/admin/chat/users/setenabled')
.send({ userId: userId, enabled: false })
.auth('admin', 'abc123')
.expect(200);
await new Promise((r) => setTimeout(r, 1500));
done();
});
test('verify user is disabled', async (done) => {
const response = await request
.get('/api/admin/chat/users/disabled')
.auth('admin', 'abc123')
.expect(200);
const tokenCheck = response.body.filter((user) => user.id === userId);
expect(tokenCheck).toHaveLength(1);
done();
});
test('verify messages from user are hidden', async (done) => {
const response = await request
.get('/api/admin/chat/messages')
.auth('admin', 'abc123')
.expect(200);
const message = response.body.filter((obj) => {
return obj.user.id === userId;
});
expect(message[0].user.disabledAt).toBeTruthy();
done();
});
test('can re-enable a user', async (done) => {
await request
.post('/api/admin/chat/users/setenabled')
.send({ userId: userId, enabled: true })
.auth('admin', 'abc123')
.expect(200);
done();
});
test('verify user is enabled', async (done) => {
const response = await request
.get('/api/admin/chat/users/disabled')
.auth('admin', 'abc123')
.expect(200);
const tokenCheck = response.body.filter((user) => user.id === userId);
expect(tokenCheck).toHaveLength(0);
done();
});
test('ban an ip address', async (done) => {
await request
.post('/api/admin/chat/users/ipbans/create')
.send({ value: localIPAddress })
.auth('admin', 'abc123')
.expect(200);
done();
});
// Note: This test expects the local address to be 127.0.0.1.
// If it's running on an ipv6-only network, for example, things will
// probably fail.
test('verify IP address is blocked from the ban', async (done) => {
const response = await request
.get(`/api/admin/chat/users/ipbans`)
.auth('admin', 'abc123')
.expect(200);
expect(response.body).toHaveLength(1);
expect(response.body[0].ipAddress).toBe(localIPAddress);
done();
});
test('verify access is denied', async (done) => {
await request.get(`/api/chat?accessToken=${accessToken}`).expect(401);
done();
});
test('remove an ip address ban', async (done) => {
await request
.post('/api/admin/chat/users/ipbans/remove')
.send({ value: localIPAddress })
.auth('admin', 'abc123')
.expect(200);
done();
});
test('verify IP address is no longer banned', async (done) => {
const response = await request
.get(`/api/admin/chat/users/ipbans`)
.auth('admin', 'abc123')
.expect(200);
expect(response.body).toHaveLength(0);
done();
});
test('verify access is again allowed', async (done) => {
await request.get(`/api/chat?accessToken=${accessToken}`).expect(200);
done();
});

View file

@ -600,6 +600,15 @@
"node": ">= 10.14.2"
}
},
"node_modules/@jest/core/node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/@jest/core/node_modules/strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
@ -984,15 +993,6 @@
"node": ">=8"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -3373,6 +3373,15 @@
"node": ">= 10.14.2"
}
},
"node_modules/jest-runtime/node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/jest-runtime/node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@ -3638,6 +3647,15 @@
"node": ">= 10.13.0"
}
},
"node_modules/jest/node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/jest/node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@ -4613,6 +4631,15 @@
"node": ">= 10"
}
},
"node_modules/pretty-format/node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/prompts": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz",
@ -5634,6 +5661,15 @@
"node": ">=10"
}
},
"node_modules/string-length/node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/string-length/node_modules/strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
@ -6791,6 +6827,12 @@
"strip-ansi": "^6.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
@ -7132,12 +7174,6 @@
}
}
},
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -8688,6 +8724,12 @@
"jest-cli": "^26.6.3"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@ -9211,6 +9253,12 @@
"yargs": "^15.4.1"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
@ -10062,6 +10110,14 @@
"ansi-regex": "^5.0.0",
"ansi-styles": "^4.0.0",
"react-is": "^17.0.1"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
}
}
},
"prompts": {
@ -10907,6 +10963,12 @@
"strip-ansi": "^6.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",

View file

@ -39,7 +39,8 @@ func Restore(backupFile string, databaseFile string) error {
rawSQL := b.String()
if _, err := os.Create(databaseFile); err != nil { //nolint: gosec
// nolint:gosec
if _, err := os.Create(databaseFile); err != nil {
return errors.New("unable to write restored database")
}
@ -62,7 +63,7 @@ func Backup(db *sql.DB, backupFile string) {
backupDirectory := filepath.Dir(backupFile)
if !DoesFileExists(backupDirectory) {
err := os.MkdirAll(backupDirectory, 0700)
err := os.MkdirAll(backupDirectory, 0o700)
if err != nil {
log.Errorln("unable to create backup directory. check permissions and ownership.", backupDirectory, err)
return
@ -79,7 +80,7 @@ func Backup(db *sql.DB, backupFile string) {
_ = out.Flush()
// Create a new backup file
f, err := os.OpenFile(backupFile, os.O_WRONLY|os.O_CREATE, 0600) // nolint
f, err := os.OpenFile(backupFile, os.O_WRONLY|os.O_CREATE, 0o600) // nolint
if err != nil {
handleError(err)
return