Skip to content
Merged
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
9 changes: 7 additions & 2 deletions cmd/curio/rpc/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"net/url"
"os"
"path/filepath"
"strings"
"time"

"github.com/gbrlsnchs/jwt/v3"
Expand Down Expand Up @@ -490,9 +491,13 @@ func ListenAndServe(ctx context.Context, dependencies *deps.Deps, shutdownChan c
}()

uiAddress := dependencies.Cfg.Subsystems.GuiAddress
if uiAddress == "" || uiAddress[0] == ':' {
uiAddress = "localhost" + uiAddress
if uiAddress == "" || uiAddress[0] == ':' || uiAddress == "0.0.0.0:4701" {
split := strings.Split(uiAddress, ":")
if len(split) == 2 {
uiAddress = "localhost:" + split[1]
}
}

log.Infof("GUI: http://%s", uiAddress)
eg.Go(web.ListenAndServe)
}
Expand Down
7 changes: 2 additions & 5 deletions cuhttp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ type ServiceDeps struct {
DealMarket *storage_market.CurioStorageDealMarket
}

// This starts the public-facing server for market calls.
func StartHTTPServer(ctx context.Context, d *deps.Deps, sd *ServiceDeps) error {
cfg := d.Cfg.HTTP

Expand All @@ -152,11 +153,7 @@ func StartHTTPServer(ctx context.Context, d *deps.Deps, sd *ServiceDeps) error {
chiRouter.Use(middleware.Recoverer)
chiRouter.Use(handlers.ProxyHeaders) // Handle reverse proxy headers like X-Forwarded-For
chiRouter.Use(secureHeaders(cfg.CSP))
chiRouter.Use(corsHeaders)

if cfg.EnableCORS {
chiRouter.Use(handlers.CORS(handlers.AllowedOrigins([]string{"https://" + cfg.DomainName})))
}
chiRouter.Use(corsHeaders) // allows market calls from other domains

// Set up the compression middleware with custom compression levels
compressionMw, err := compressionMiddleware(&cfg.CompressionLevels)
Expand Down
9 changes: 6 additions & 3 deletions deps/config/doc_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions deps/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func DefaultCurioConfig() *CurioConfig {
ReadTimeout: time.Second * 10,
IdleTimeout: time.Hour,
ReadHeaderTimeout: time.Second * 5,
EnableCORS: true,
CORSOrigins: []string{},
CSP: "inline",
CompressionLevels: CompressionConfig{
GzipLevel: 6,
Expand Down Expand Up @@ -862,8 +862,11 @@ type HTTPConfig struct {
// Time duration string (e.g., "1h2m3s") in TOML format. (Default: "5m0s")
ReadHeaderTimeout time.Duration

// EnableCORS indicates whether Cross-Origin Resource Sharing (CORS) is enabled or not.
EnableCORS bool
// CORSOrigins specifies the allowed origins for CORS requests to the Curio admin UI. If empty, CORS is disabled.
// If not empty, only the specified origins will be allowed for CORS requests.
// This is required for third-party UI servers.
// "*" allows everyone, it's best to specify the UI servers' hostname.
CORSOrigins []string

// CSP sets the Content Security Policy for content served via the /piece/ retrieval endpoint.
// Valid values: "off", "self", "inline" (Default: "inline")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,10 +558,13 @@ description: The default curio configuration
# type: time.Duration
#ReadHeaderTimeout = "5s"

# EnableCORS indicates whether Cross-Origin Resource Sharing (CORS) is enabled or not.
# CORSOrigins specifies the allowed origins for CORS requests to the Curio admin UI. If empty, CORS is disabled.
# If not empty, only the specified origins will be allowed for CORS requests.
# This is required for third-party UI servers.
# "*" allows everyone, it's best to specify the UI servers' hostname.
#
# type: bool
#EnableCORS = true
# type: []string
#CORSOrigins = []

# CSP sets the Content Security Policy for content served via the /piece/ retrieval endpoint.
# Valid values: "off", "self", "inline" (Default: "inline")
Expand Down
4 changes: 2 additions & 2 deletions documentation/en/curio-market/curio-http-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ The Curio HTTP Server can be customized using the `HTTPConfig` structure, which
Default: `2 minutes` — Prevents resources from being consumed by idle connections. If your application expects longer periods of inactivity, such as in long polling or WebSocket connections, this value should be adjusted accordingly.
* **ReadHeaderTimeout**: The time allowed to read the request headers from the client.\
Default: `5 seconds` — Prevents slow clients from keeping connections open without sending complete headers. For standard web traffic, this value is sufficient, but it may need adjustment for certain client environments.
* **EnableCORS**: A boolean flag to enable or disable Cross-Origin Resource Sharing (CORS).\
Default: `true` — This allows cross-origin requests, which is important for web applications that might make API calls from different domains.
* **CORSOrigins**: Specifies the allowed origins for CORS requests. If empty, CORS is disabled.\
Default: `[]` (empty array) — This disables CORS by default for security. To enable CORS, specify the allowed origins (e.g., `["https://example.com", "https://app.example.com"]`). This is required for third-party UI servers.
* **CompressionLevels**: Defines the compression levels for GZIP, Brotli, and Deflate, which are used to optimize the response size. The defaults balance performance and bandwidth savings:
* **GzipLevel**: Default: `6` — A moderate compression level that balances speed and compression ratio, suitable for general-purpose use.
* **BrotliLevel**: Default: `4` — A moderate Brotli compression level, which provides better compression than GZIP but is more CPU-intensive. This level is good for text-heavy responses like HTML or JSON.
Expand Down
50 changes: 45 additions & 5 deletions harmony/harmonydb/harmonydb.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,23 +92,45 @@ func NewFromConfigWithITestID(t *testing.T, id ITestID) (*DB, error) {
func New(hosts []string, username, password, database, port string, loadBalance bool, itestID ITestID) (*DB, error) {
itest := string(itestID)

// Join hosts with the port
hostPortPairs := make([]string, len(hosts))
for i, host := range hosts {
hostPortPairs[i] = fmt.Sprintf("%s:%s", host, port)
if len(hosts) == 0 {
return nil, xerrors.Errorf("no hosts provided")
}

// Debug: Log which path we're taking
logger.Infof("Yugabyte connection config: loadBalance=%v, hosts=%v, port=%s", loadBalance, hosts, port)

// When load balancing is disabled, use only the first host to prevent
// Yugabyte client from discovering internal Docker IPs via topology discovery
var connectionHost string
if loadBalance {
// Join all hosts with the port for load balancing
hostPortPairs := make([]string, len(hosts))
for i, host := range hosts {
hostPortPairs[i] = fmt.Sprintf("%s:%s", host, port)
}
connectionHost = strings.Join(hostPortPairs, ",")
} else {
// Use only the first host when load balancing is disabled
// This prevents topology discovery that would return internal Docker IPs
connectionHost = fmt.Sprintf("%s:%s", hosts[0], port)
}

// Construct the connection string
connString := fmt.Sprintf(
"postgresql://%s:%s@%s/%s?sslmode=disable",
username,
password,
strings.Join(hostPortPairs, ","),
connectionHost,
database,
)

if loadBalance {
connString += "&load_balance=true"
} else {
// When load balancing is disabled, explicitly disable it
// fallback_to_topology_keys_only=true ensures client only uses specified nodes
// Note: Don't set topology_keys= (empty) as Yugabyte rejects empty topology_keys format
connString += "&load_balance=false&fallback_to_topology_keys_only=true"
}

schema := "curio"
Expand All @@ -124,6 +146,24 @@ func New(hosts []string, username, password, database, port string, loadBalance
return nil, err
}

// When load balancing is disabled, restrict the pool to only use the specified host
// This prevents Yugabyte client from discovering and connecting to internal Docker IPs
if !loadBalance {
// Parse port as integer
portInt, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return nil, xerrors.Errorf("invalid port: %w", err)
}

// Override the connection config to use only our specified host
cfg.ConnConfig.Host = hosts[0]
cfg.ConnConfig.Port = uint16(portInt)

// Note: Yugabyte-specific connection parameters (load_balance, fallback_to_topology_keys_only)
// must be set in the connection string, not as runtime parameters.
// The connection string already has these parameters set above.
}

cfg.ConnConfig.OnNotice = func(conn *pgconn.PgConn, n *pgconn.Notice) {
logger.Debug("database notice: " + n.Message + ": " + n.Detail)
DBMeasures.Errors.M(1)
Expand Down
86 changes: 68 additions & 18 deletions web/srv.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,73 @@ var webDev = os.Getenv("CURIO_WEB_DEV") == "1"

func GetSrv(ctx context.Context, deps *deps.Deps, devMode bool) (*http.Server, error) {
mx := mux.NewRouter()
mx.Use(corsMiddleware)

// Single CORS middleware that handles all CORS logic
// Wrap the entire router to ensure middleware runs for all requests including unmatched routes
corsHandler := func(next http.Handler) http.Handler {
for _, ao := range deps.Cfg.HTTP.CORSOrigins {
if ao == "*" {
log.Infof("This CORS configuration allows any website to call irreversable APIs on the Curio node: %s", ao)
}
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Handle OPTIONS preflight requests - always return 204, even if CORS not configured
if r.Method == http.MethodOptions {
if len(deps.Cfg.HTTP.CORSOrigins) > 0 {
origin := r.Header.Get("Origin")
var allowedOrigin string
allowed := false

// Check if origin is allowed
for _, ao := range deps.Cfg.HTTP.CORSOrigins {
if ao == "*" || ao == origin {
allowedOrigin = ao
allowed = true
break
}
}

if allowed {
if allowedOrigin == "*" {

w.Header().Set("Access-Control-Allow-Origin", "*")
} else {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
}
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
requestHeaders := r.Header.Get("Access-Control-Request-Headers")
if requestHeaders != "" {
w.Header().Set("Access-Control-Allow-Headers", requestHeaders)
} else {
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept, Accept-Encoding")
}
w.Header().Set("Access-Control-Max-Age", "86400")
}
w.WriteHeader(http.StatusNoContent)
return
}

// Set CORS headers for non-OPTIONS requests if CORS is configured
if len(deps.Cfg.HTTP.CORSOrigins) > 0 {
origin := r.Header.Get("Origin")
if origin != "" {
for _, ao := range deps.Cfg.HTTP.CORSOrigins {
if ao == "*" || ao == origin {
if ao == "*" {
w.Header().Set("Access-Control-Allow-Origin", "*")
} else {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
break
}
}
}
}

next.ServeHTTP(w, r)
})
}

if !devMode {
api.Routes(mx.PathPrefix("/api").Subrouter(), deps, webDev)
Expand Down Expand Up @@ -90,7 +156,7 @@ func GetSrv(ctx context.Context, deps *deps.Deps, devMode bool) (*http.Server, e
})

return &http.Server{
Handler: http.HandlerFunc(mx.ServeHTTP),
Handler: corsHandler(mx),
BaseContext: func(listener net.Listener) context.Context {
ctx, _ := tag.New(context.Background(), tag.Upsert(metrics.APIInterface, "curio"))
return ctx
Expand Down Expand Up @@ -278,19 +344,3 @@ func proxyCopy(dst, src *websocket.Conn, errc chan<- error, direction string) {
}
}
}

func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")

if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}

next.ServeHTTP(w, r)
})
}