项目文件夹

文件
Asim Aslam cae6fbbe76 Framework hardening: security, reliability, and developer experience improvements (#2826)
* fix: remove deprecated rand.Seed calls

Go 1.20+ automatically seeds the global random number generator.
These calls are no-ops and generate warnings with newer Go versions.

Removed from:
- selector/strategy.go
- registry/cache/cache.go
- broker/memory.go
- broker/http.go
- cmd/cmd.go
- transport/memory.go

Co-authored-by: Shelley <shelley@exe.dev>

* fix: handle previously ignored errors

- MySQL store: properly handle prepared statement errors in initDB()
- Consul registry: handle client creation errors in Client() method

These silent failures could cause hard-to-debug issues in production.

Co-authored-by: Shelley <shelley@exe.dev>

* feat(genai): improve provider interface with context and streaming

Breaking changes:
- Generate() and Stream() now require context.Context as first parameter
- Stream.Close() added for proper resource cleanup

Improvements:
- Proper context support for cancellation and timeouts
- Real SSE streaming for OpenAI and Gemini text generation
- Better error handling with wrapped errors and API error responses
- Thread-safe provider registry with sync.RWMutex
- New options: WithMaxTokens, WithTemperature, WithTimeout
- Stream has proper Close() method for cleanup
- Results can include Error field for per-chunk errors

Provider updates:
- OpenAI: true streaming with SSE parsing, proper HTTP client with timeout
- Gemini: true streaming with streamGenerateContent endpoint
- Default model updated to gpt-4o-mini (OpenAI) and gemini-2.0-flash (Gemini)

Co-authored-by: Shelley <shelley@exe.dev>

* feat(tls): make TLS secure by default, configurable via environment

BREAKING: TLS now verifies certificates by default. Set MICRO_TLS_INSECURE=true
to restore previous behavior (NOT recommended for production).

Changes:
- Add util/tls.Config(), SecureConfig(), InsecureConfig(), ConfigFromEnv() helpers
- Update all components to use ConfigFromEnv() instead of hardcoded InsecureSkipVerify
- Set MinVersion to TLS 1.2 for all TLS configs

Affected components:
- broker/http
- broker/rabbitmq
- registry/etcd
- registry/consul
- transport/grpc

This improves security posture while allowing opt-out for development environments.

Co-authored-by: Shelley <shelley@exe.dev>

* feat(tls): add TLS helpers with opt-in secure mode

NOT a breaking change - keeps InsecureSkipVerify=true as default for
local development compatibility.

New util/tls helpers:
- Config() - returns config based on MICRO_TLS_SECURE env var
- SecureConfig() - certificate verification enabled
- InsecureConfig() - certificate verification disabled (dev only)

For production security, use one of:
- Set MICRO_TLS_SECURE=true with proper CA-signed certs
- Use a service mesh (Istio, Linkerd) for automatic mTLS
- Configure TLSConfig directly with your certificates

Also: Changed CLI alias from 'g' to 'gen' for clarity
- micro generate handler -> micro gen handler

Co-authored-by: Shelley <shelley@exe.dev>

* refactor(cli): rename generate directory to gen for consistency

Directory name now matches the command alias:
  cmd/micro/cli/gen/ -> micro gen handler

Co-authored-by: Shelley <shelley@exe.dev>

---------

Co-authored-by: Shelley <shelley@exe.dev>
2026-01-27 10:39:25 +00:00

252 行
5.4 KiB
Go

package mysql
import (
"database/sql"
"fmt"
"time"
"unicode"
"github.com/pkg/errors"
log "go-micro.dev/v5/logger"
"go-micro.dev/v5/store"
)
var (
// DefaultDatabase is the database that the sql store will use if no database is provided.
DefaultDatabase = "micro"
// DefaultTable is the table that the sql store will use if no table is provided.
DefaultTable = "micro"
)
type sqlStore struct {
db *sql.DB
database string
table string
options store.Options
readPrepare, writePrepare, deletePrepare *sql.Stmt
}
func (s *sqlStore) Init(opts ...store.Option) error {
for _, o := range opts {
o(&s.options)
}
// reconfigure
return s.configure()
}
func (s *sqlStore) Options() store.Options {
return s.options
}
func (s *sqlStore) Close() error {
return s.db.Close()
}
// List all the known records.
func (s *sqlStore) List(opts ...store.ListOption) ([]string, error) {
rows, err := s.db.Query(fmt.Sprintf("SELECT `key`, value, expiry FROM %s.%s;", s.database, s.table))
if err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
defer rows.Close()
var records []string
var cachedTime time.Time
for rows.Next() {
record := &store.Record{}
if err := rows.Scan(&record.Key, &record.Value, &cachedTime); err != nil {
return nil, err
}
if cachedTime.Before(time.Now()) {
// record has expired
go s.Delete(record.Key)
} else {
records = append(records, record.Key)
}
}
rowErr := rows.Close()
if rowErr != nil {
// transaction rollback or something
return records, rowErr
}
if err := rows.Err(); err != nil {
return nil, err
}
return records, nil
}
// Read all records with keys.
func (s *sqlStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) {
var options store.ReadOptions
for _, o := range opts {
o(&options)
}
// TODO: make use of options.Prefix using WHERE key LIKE = ?
var records []*store.Record
row := s.readPrepare.QueryRow(key)
record := &store.Record{}
var cachedTime time.Time
if err := row.Scan(&record.Key, &record.Value, &cachedTime); err != nil {
if err == sql.ErrNoRows {
return records, store.ErrNotFound
}
return records, err
}
if cachedTime.Before(time.Now()) {
// record has expired
go s.Delete(key)
return records, store.ErrNotFound
}
record.Expiry = time.Until(cachedTime)
records = append(records, record)
return records, nil
}
// Write records.
func (s *sqlStore) Write(r *store.Record, opts ...store.WriteOption) error {
timeCached := time.Now().Add(r.Expiry)
_, err := s.writePrepare.Exec(r.Key, r.Value, timeCached, r.Value, timeCached)
if err != nil {
return errors.Wrap(err, "Couldn't insert record "+r.Key)
}
return nil
}
// Delete records with keys.
func (s *sqlStore) Delete(key string, opts ...store.DeleteOption) error {
result, err := s.deletePrepare.Exec(key)
if err != nil {
return err
}
_, err = result.RowsAffected()
if err != nil {
return err
}
return nil
}
func (s *sqlStore) initDB() error {
// Create the namespace's database
_, err := s.db.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s ;", s.database))
if err != nil {
return err
}
_, err = s.db.Exec(fmt.Sprintf("USE %s ;", s.database))
if err != nil {
return errors.Wrap(err, "Couldn't use database")
}
// Create a table for the namespace's prefix
createSQL := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (`key` varchar(255) primary key, value blob null, expiry timestamp not null);", s.table)
_, err = s.db.Exec(createSQL)
if err != nil {
return errors.Wrap(err, "Couldn't create table")
}
// prepare statements
var prepareErr error
s.readPrepare, prepareErr = s.db.Prepare(fmt.Sprintf("SELECT `key`, value, expiry FROM %s.%s WHERE `key` = ?;", s.database, s.table))
if prepareErr != nil {
return errors.Wrap(prepareErr, "failed to prepare read statement")
}
s.writePrepare, prepareErr = s.db.Prepare(fmt.Sprintf("INSERT INTO %s.%s (`key`, value, expiry) VALUES(?, ?, ?) ON DUPLICATE KEY UPDATE `value`= ?, `expiry` = ?", s.database, s.table))
if prepareErr != nil {
return errors.Wrap(prepareErr, "failed to prepare write statement")
}
s.deletePrepare, prepareErr = s.db.Prepare(fmt.Sprintf("DELETE FROM %s.%s WHERE `key` = ?;", s.database, s.table))
if prepareErr != nil {
return errors.Wrap(prepareErr, "failed to prepare delete statement")
}
return nil
}
func (s *sqlStore) configure() error {
nodes := s.options.Nodes
if len(nodes) == 0 {
nodes = []string{"localhost:3306"}
}
database := s.options.Database
if len(database) == 0 {
database = DefaultDatabase
}
table := s.options.Table
if len(table) == 0 {
table = DefaultTable
}
for _, r := range database {
if !unicode.IsLetter(r) {
return errors.New("store.namespace must only contain letters")
}
}
source := nodes[0]
// create source from first node
db, err := sql.Open("mysql", source)
if err != nil {
return err
}
if err := db.Ping(); err != nil {
return err
}
if s.db != nil {
s.db.Close()
}
// save the values
s.db = db
s.database = database
s.table = table
// initialize the database
return s.initDB()
}
func (s *sqlStore) String() string {
return "mysql"
}
// New returns a new micro Store backed by sql.
func NewMysqlStore(opts ...store.Option) store.Store {
var options store.Options
for _, o := range opts {
o(&options)
}
// new store
s := new(sqlStore)
// set the options
s.options = options
// configure the store
if err := s.configure(); err != nil {
log.Fatal(err)
}
// return store
return s
}