Skip to content

Commit c074c2b

Browse files
committed
bugfixes, new features
1 parent f742978 commit c074c2b

9 files changed

Lines changed: 118 additions & 62 deletions

File tree

CHANGELOG

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
1.4.4:
2+
3+
- Security: Error responses now return safe HTTP status text instead of detailed database error messages to prevent information leakage
4+
- Security: Added MAX_CONNECTIONS environment variable to limit concurrent database connections and prevent resource exhaustion (DoS protection)
5+
- Fix: Fixed race condition in GetById() when upgrading from read lock to write lock
6+
- Fix: Fixed memory leak in RunMaintenance() where prepared statement deletions were not persisted to the connection pool
7+
- Feature: Error responses now use consistent JSON format with api_version, error, and status fields
8+
19
1.4.3:
210

311
- Fix: minor bugfixes

Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
PROJECT_NAME := sql-proxy
2-
BUILD_VERSION := 1.4.3
2+
BUILD_VERSION := 1.4.4
33
BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
44
BUILD_DIR := build
55
GO_FILES := src/main.go
@@ -18,6 +18,8 @@ GOAMD64 := v2
1818
BIND_PORT := 8080
1919
BIND_ADDR := localhost
2020
MAX_ROWS := 10000
21+
MAX_CONNECTIONS := 100
22+
2123
DEBUG_LOG := true
2224
#TLS_CERT := $(BUILD_DIR)/server.crt
2325
#TLS_KEY := $(BUILD_DIR)/server.key
@@ -50,7 +52,7 @@ debug: clean
5052
# Run
5153
run: debug
5254
@echo "Running $(PROJECT_NAME) in debug mode..."
53-
BIND_ADDR=$(BIND_ADDR) BIND_PORT=$(BIND_PORT) MAX_ROWS=$(MAX_ROWS) TLS_CERT=$(TLS_CERT) TLS_KEY=$(TLS_KEY) DEBUG_LOG=$(DEBUG_LOG) $(BUILD_DIR)/$(PROJECT_NAME)-debug
55+
BIND_ADDR=$(BIND_ADDR) BIND_PORT=$(BIND_PORT) MAX_ROWS=$(MAX_ROWS) MAX_CONNECTIONS=$(MAX_CONNECTIONS) TLS_CERT=$(TLS_CERT) TLS_KEY=$(TLS_KEY) DEBUG_LOG=$(DEBUG_LOG) $(BUILD_DIR)/$(PROJECT_NAME)-debug
5456

5557
# Run test
5658
test:

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Note that this service is not limited to 1C and can be utilized in other context
3131
* Secure Credential Management : Does not store SQL credentials, ensuring sensitive information remains protected;
3232
* Secure Communication : Supports HTTPS for secure data transmission;
3333
* Efficient Connection Pooling : Utilizes a shared, reusable SQL connection pool with automated maintenance tasks to remove stale or dead connections;
34+
* Connection Limiting : Configurable maximum number of concurrent database connections to prevent resource exhaustion;
3435
* Command Support : Currently supports all SQL commands with no limitation. The SELECT command returns query results as a flexible JSON-formatted recordset;
3536
* Result Limitation : Allows configuration to limit the number of rows returned by SELECT statements;
3637
* Prepared Statements : supported;
@@ -48,7 +49,7 @@ Current API version is 1.2. See Swagger OpenAPI 3.0 specification in /docs/api
4849

4950
## How to compile
5051

51-
Current version is 1.4.3. Execute in the command line:
52+
Current version is 1.4.4. Execute in the command line:
5253

5354
```
5455
make prod
@@ -59,7 +60,7 @@ make prod
5960
Just run the binary. Settings may be passed with environment variables, see Makefile for details and default values:
6061

6162
```
62-
BIND_ADDR=localhost BIND_PORT=8081 MAX_ROWS=10000 sql-proxy
63+
BIND_ADDR=localhost BIND_PORT=8081 MAX_ROWS=10000 MAX_CONNECTIONS=100 sql-proxy
6364
```
6465

6566
or install it as a systemd service with install.sh script. Parameters may be changed later in sql-proxy.service file.

README.ru.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
+ Безопасное управление учетными данными: не хранит данные учетных записей, гарантируя защиту конфиденциальной информации;
3737
+ Защищённое соединение: при необходимости, поддерживает HTTPS для безопасной передачи данных;
3838
+ Пул соединений: использует общий переиспользуемый пул SQL-соединений с регламентными задачами обслуживания для удаления устаревших или зависших соединений;
39+
+ Ограничение соединений: настраиваемый лимит одновременных подключений к базе данных для предотвращения исчерпания ресурсов;
3940
+ Поддержка языка SQL: поддерживает любые SQL-команды без ограничений. Команда SELECT возвращает результаты запроса в виде гибкого JSON-формата набора записей;
4041
+ Ограничение результатов: позволяет настраивать ограничения на количество строк, возвращаемых командами SELECT;
4142
+ Поддержка подготовленных выражений: реализована;
@@ -53,7 +54,7 @@
5354

5455
## Как скомпилировать
5556

56-
Номер текущей версии: 1.4.3. Выполнить в командной строке:
57+
Номер текущей версии: 1.4.4. Выполнить в командной строке:
5758

5859
```
5960
make prod
@@ -64,7 +65,7 @@ make prod
6465
Просто запустите бинарник. Все параметры передаются через переменные окружения, см. Makefile для детальной информации и значений настроек по умолчанию:
6566

6667
```
67-
BIND_ADDR=localhost BIND_PORT=8081 MAX_ROWS=10000 sql-proxy
68+
BIND_ADDR=localhost BIND_PORT=8081 MAX_ROWS=10000 MAX_CONNECTIONS=100 sql-proxy
6869
```
6970

7071
или установите как службу systemd с помощью скрипта install.sh. Параметры можно изменить прямо в этом скрипте перед установкой, или отредактировать потом файл sql-proxy.service.

src/db/dblist.go

Lines changed: 59 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import (
99

1010
"time"
1111

12-
"slices"
13-
1412
"github.com/google/uuid"
1513
)
1614

15+
var ErrConnectionLimit = fmt.Errorf("connection pool limit reached")
16+
1717
// Init map
1818
func (o *DbList) Init() {
1919

@@ -22,23 +22,33 @@ func (o *DbList) Init() {
2222

2323
}
2424

25+
// Returns current pool size
26+
func (o *DbList) Size() int {
27+
o.mu.RLock()
28+
defer o.mu.RUnlock()
29+
return len(o.items)
30+
}
31+
2532
// Gets SQL server connection by GUID
2633
func (o *DbList) GetById(id string, updateTimestamp bool) (*sql.DB, bool) {
2734

28-
o.mu.RLock()
35+
if updateTimestamp {
36+
o.mu.Lock()
37+
defer o.mu.Unlock()
2938

30-
if dbConn, ok := o.items[id]; ok {
31-
if updateTimestamp {
32-
o.mu.RUnlock()
33-
o.mu.Lock()
39+
if dbConn, ok := o.items[id]; ok {
3440
dbConn.Timestamp = time.Now()
3541
o.items[id] = dbConn
36-
o.mu.Unlock()
42+
return dbConn.DB, true
3743
}
38-
return dbConn.DB, true
39-
}
44+
} else {
45+
o.mu.RLock()
46+
defer o.mu.RUnlock()
4047

41-
o.mu.RUnlock()
48+
if dbConn, ok := o.items[id]; ok {
49+
return dbConn.DB, true
50+
}
51+
}
4252

4353
app.Logger.Errorf("SQL connection with guid='%s' not found", id)
4454
return nil, false
@@ -47,12 +57,11 @@ func (o *DbList) GetById(id string, updateTimestamp bool) (*sql.DB, bool) {
4757

4858
// Gets the new SQL server connection with parameters given.
4959
// First lookups in pool, if fails opens new one and returns GUID value
50-
func (o *DbList) GetByParams(connInfo *DbConnInfo) (string, bool) {
60+
func (o *DbList) GetByParams(connInfo *DbConnInfo) (string, error) {
5161
hash, err := connInfo.GetHash()
5262
if err != nil {
53-
errMsg := "Hash calculation failed"
54-
app.Logger.Error(errMsg)
55-
return errMsg, false
63+
app.Logger.Error("Hash calculation failed")
64+
return "", fmt.Errorf("hash calculation failed")
5665
}
5766

5867
guid := ""
@@ -70,7 +79,7 @@ func (o *DbList) GetByParams(connInfo *DbConnInfo) (string, bool) {
7079
if err = dbConn.DB.Ping(); err == nil {
7180
o.mu.RUnlock()
7281
// Everything is ok, return guid
73-
return guid, true
82+
return guid, nil
7483
} else {
7584
// Bad connection, need to clean
7685
o.mu.RUnlock()
@@ -90,11 +99,17 @@ func (o *DbList) GetByParams(connInfo *DbConnInfo) (string, bool) {
9099
}
91100

92101
// Creates the new SQL connection regarding concurrency
93-
func (o *DbList) getNewConnection(connInfo *DbConnInfo, hash [32]byte) (string, bool) {
102+
func (o *DbList) getNewConnection(connInfo *DbConnInfo, hash [32]byte) (string, error) {
94103

95104
o.mu.Lock()
96105
defer o.mu.Unlock()
97106

107+
// Check connection pool limit
108+
if len(o.items) >= MaxConnections {
109+
app.Logger.Errorf("Connection pool limit reached: %d", MaxConnections)
110+
return "", ErrConnectionLimit
111+
}
112+
98113
// Prepare DSN string
99114
var dsn string
100115

@@ -115,9 +130,9 @@ func (o *DbList) getNewConnection(connInfo *DbConnInfo, hash [32]byte) (string,
115130
dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s",
116131
connInfo.User, encodedPassword, connInfo.Host, connInfo.Port, connInfo.DbName)
117132
default:
118-
errMsg := fmt.Sprintf("No suitable driver implemented for server type '%s'", connInfo.DbType)
133+
errMsg := fmt.Sprintf("unsupported database type '%s'", connInfo.DbType)
119134
app.Logger.Error(errMsg)
120-
return errMsg, false
135+
return "", fmt.Errorf("%s", errMsg)
121136
}
122137

123138
// Open new SQL server connection
@@ -128,16 +143,14 @@ func (o *DbList) getNewConnection(connInfo *DbConnInfo, hash [32]byte) (string,
128143

129144
// Check for failure
130145
if err != nil {
131-
errMsg := "Error establishing SQL server connection"
132-
app.Logger.Error(errMsg)
133-
return errMsg, false
146+
app.Logger.Errorf("Error establishing SQL server connection: %v", err)
147+
return "", fmt.Errorf("error establishing SQL server connection")
134148
}
135149

136150
// Check if alive
137151
if err = newDb.Ping(); err != nil {
138-
errMsg := "Just created SQL connection is dead"
139-
app.Logger.Error(errMsg)
140-
return errMsg, false
152+
app.Logger.Errorf("Just created SQL connection is dead: %v", err)
153+
return "", fmt.Errorf("error establishing SQL server connection")
141154
}
142155

143156
// Insert into pool
@@ -161,7 +174,7 @@ func (o *DbList) getNewConnection(connInfo *DbConnInfo, hash [32]byte) (string,
161174
newId,
162175
)
163176

164-
return newId, true
177+
return newId, nil
165178
}
166179

167180
// Deletes SQL server connection
@@ -230,15 +243,17 @@ func (o *DbList) ClosePreparedStatement(connId, stmtId string) bool {
230243
if !ok {
231244
return false
232245
}
233-
for i := range dbConn.Stmt {
234-
if dbConn.Stmt[i].Id == stmtId {
235-
dbConn.Stmt[i].Stmt.Close()
236-
dbConn.Stmt = slices.Delete(dbConn.Stmt, i, i+1)
237-
break
246+
247+
for i, stmt := range dbConn.Stmt {
248+
if stmt.Id == stmtId {
249+
stmt.Stmt.Close()
250+
dbConn.Stmt = append(dbConn.Stmt[:i], dbConn.Stmt[i+1:]...)
251+
o.items[connId] = dbConn
252+
return true
238253
}
239254
}
240255

241-
return true
256+
return false
242257

243258
}
244259

@@ -258,40 +273,37 @@ func (o *DbList) RunMaintenance() {
258273
o.mu.Lock()
259274

260275
for key, dbConn := range o.items {
261-
262-
var lostStmts []string
263276
countConn++
264277

278+
isDead := false
265279
if err := dbConn.DB.Ping(); err != nil {
266280
// dead connection
267281
deadItems = append(deadItems, key)
268282
countDeadConn++
283+
isDead = true
269284
} else if time.Since(dbConn.Timestamp).Abs().Minutes() > 20 {
270285
// connection not used for last 20 minutes
271286
deadItems = append(deadItems, key)
272287
countDeadConn++
288+
isDead = true
273289
}
274290

275-
// check prepared statements
291+
// Close and remove expired prepared statements
292+
activeStmts := make([]DbStmt, 0, len(dbConn.Stmt))
276293
for _, stmt := range dbConn.Stmt {
277-
// prepared statements not used last 20 minutes
278294
if time.Since(stmt.Timestamp).Abs().Minutes() > 20 {
279-
lostStmts = append(lostStmts, stmt.Id)
295+
stmt.Stmt.Close()
280296
countStmt++
297+
} else {
298+
activeStmts = append(activeStmts, stmt)
281299
}
282300
}
301+
dbConn.Stmt = activeStmts
283302

284-
// delete lost prepared statements
285-
for _, lost := range lostStmts {
286-
for i := range dbConn.Stmt {
287-
if dbConn.Stmt[i].Id == lost {
288-
dbConn.Stmt[i].Stmt.Close()
289-
dbConn.Stmt = slices.Delete(dbConn.Stmt, i, i+1)
290-
break
291-
}
292-
}
303+
// Update connection in pool (without timestamp change)
304+
if !isDead {
305+
o.items[key] = dbConn
293306
}
294-
295307
}
296308

297309
// remove dead connections

src/db/vars.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package db
22

33
var (
4-
Handler DbList
5-
MaxRows uint32 = 10000
4+
Handler DbList
5+
MaxRows uint32 = 10000
6+
MaxConnections int = 100
67
)

src/handlers/common.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,25 @@ type ResponseEnvelope struct {
1717
Rows []map[string]any `json:"rows"`
1818
}
1919

20+
type ErrorEnvelope struct {
21+
ApiVersion string `json:"api_version"`
22+
Error string `json:"error"`
23+
Status int `json:"status"`
24+
}
25+
2026
func checkApiVersion(w http.ResponseWriter, r *http.Request) bool {
2127

2228
apiVersion := r.Header.Get("API-Version")
2329
if apiVersion != app.ApiVersion {
2430
message := "Unsupported API version"
2531
app.Logger.Error(message)
26-
http.Error(w, message, http.StatusNotImplemented)
32+
w.Header().Set("Content-Type", "application/json")
33+
w.WriteHeader(http.StatusNotImplemented)
34+
json.NewEncoder(w).Encode(ErrorEnvelope{
35+
ApiVersion: app.ApiVersion,
36+
Error: message,
37+
Status: http.StatusNotImplemented,
38+
})
2739
return false
2840
} else {
2941
return true
@@ -34,7 +46,14 @@ func checkApiVersion(w http.ResponseWriter, r *http.Request) bool {
3446
func errorResponce(w http.ResponseWriter, message string, httpStatus int) {
3547

3648
app.Logger.Error(message)
37-
http.Error(w, message, httpStatus)
49+
w.Header().Set("Content-Type", "application/json")
50+
w.WriteHeader(httpStatus)
51+
errResp := ErrorEnvelope{
52+
ApiVersion: app.ApiVersion,
53+
Error: http.StatusText(httpStatus),
54+
Status: httpStatus,
55+
}
56+
json.NewEncoder(w).Encode(errResp)
3857

3958
}
4059

src/handlers/connection.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package handlers
22

33
import (
44
"encoding/json"
5+
"errors"
56
"net/http"
67
"sql-proxy/src/db"
78
)
@@ -19,9 +20,17 @@ func CreateConnection(w http.ResponseWriter, r *http.Request) {
1920
return
2021
}
2122

22-
if connGuid, ok := db.Handler.GetByParams(&dbConnInfo); !ok {
23-
errorResponce(w, "Failed to get SQL connection", http.StatusInternalServerError)
24-
} else if _, err := w.Write([]byte(connGuid)); err != nil {
23+
connGuid, err := db.Handler.GetByParams(&dbConnInfo)
24+
if err != nil {
25+
if errors.Is(err, db.ErrConnectionLimit) {
26+
errorResponce(w, err.Error(), http.StatusTooManyRequests)
27+
} else {
28+
errorResponce(w, err.Error(), http.StatusInternalServerError)
29+
}
30+
return
31+
}
32+
33+
if _, err = w.Write([]byte(connGuid)); err != nil {
2534
errorResponce(w, err.Error(), http.StatusInternalServerError)
2635
}
2736

0 commit comments

Comments
 (0)