Skip to content

Commit 4dad8ed

Browse files
authored
correct secondary ui server spec (#658)
* xss fix * doc * make gen * formatted * undo yb change * gen * fiximports * fixed * 2 * cors allowed for market * docs * cors fix + connection debug for yb + http * docs * warn on star
1 parent 7022075 commit 4dad8ed

File tree

8 files changed

+142
-41
lines changed

8 files changed

+142
-41
lines changed

cmd/curio/rpc/rpc.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net/url"
1313
"os"
1414
"path/filepath"
15+
"strings"
1516
"time"
1617

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

492493
uiAddress := dependencies.Cfg.Subsystems.GuiAddress
493-
if uiAddress == "" || uiAddress[0] == ':' {
494-
uiAddress = "localhost" + uiAddress
494+
if uiAddress == "" || uiAddress[0] == ':' || uiAddress == "0.0.0.0:4701" {
495+
split := strings.Split(uiAddress, ":")
496+
if len(split) == 2 {
497+
uiAddress = "localhost:" + split[1]
498+
}
495499
}
500+
496501
log.Infof("GUI: http://%s", uiAddress)
497502
eg.Go(web.ListenAndServe)
498503
}

cuhttp/server.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ type ServiceDeps struct {
140140
DealMarket *storage_market.CurioStorageDealMarket
141141
}
142142

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

@@ -152,11 +153,7 @@ func StartHTTPServer(ctx context.Context, d *deps.Deps, sd *ServiceDeps) error {
152153
chiRouter.Use(middleware.Recoverer)
153154
chiRouter.Use(handlers.ProxyHeaders) // Handle reverse proxy headers like X-Forwarded-For
154155
chiRouter.Use(secureHeaders(cfg.CSP))
155-
chiRouter.Use(corsHeaders)
156-
157-
if cfg.EnableCORS {
158-
chiRouter.Use(handlers.CORS(handlers.AllowedOrigins([]string{"https://" + cfg.DomainName})))
159-
}
156+
chiRouter.Use(corsHeaders) // allows market calls from other domains
160157

161158
// Set up the compression middleware with custom compression levels
162159
compressionMw, err := compressionMiddleware(&cfg.CompressionLevels)

deps/config/doc_gen.go

Lines changed: 6 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deps/config/types.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ func DefaultCurioConfig() *CurioConfig {
131131
ReadTimeout: time.Second * 10,
132132
IdleTimeout: time.Hour,
133133
ReadHeaderTimeout: time.Second * 5,
134-
EnableCORS: true,
134+
CORSOrigins: []string{},
135135
CSP: "inline",
136136
CompressionLevels: CompressionConfig{
137137
GzipLevel: 6,
@@ -866,8 +866,11 @@ type HTTPConfig struct {
866866
// Time duration string (e.g., "1h2m3s") in TOML format. (Default: "5m0s")
867867
ReadHeaderTimeout time.Duration
868868

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

872875
// CSP sets the Content Security Policy for content served via the /piece/ retrieval endpoint.
873876
// Valid values: "off", "self", "inline" (Default: "inline")

documentation/en/configuration/default-curio-configuration.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -554,10 +554,13 @@ description: The default curio configuration
554554
# type: time.Duration
555555
#ReadHeaderTimeout = "5s"
556556

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

562565
# CSP sets the Content Security Policy for content served via the /piece/ retrieval endpoint.
563566
# Valid values: "off", "self", "inline" (Default: "inline")

documentation/en/curio-market/curio-http-server.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ The Curio HTTP Server can be customized using the `HTTPConfig` structure, which
114114
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.
115115
* **ReadHeaderTimeout**: The time allowed to read the request headers from the client.\
116116
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.
117-
* **EnableCORS**: A boolean flag to enable or disable Cross-Origin Resource Sharing (CORS).\
118-
Default: `true` — This allows cross-origin requests, which is important for web applications that might make API calls from different domains.
117+
* **CORSOrigins**: Specifies the allowed origins for CORS requests. If empty, CORS is disabled.\
118+
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.
119119
* **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:
120120
* **GzipLevel**: Default: `6` — A moderate compression level that balances speed and compression ratio, suitable for general-purpose use.
121121
* **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.

harmony/harmonydb/harmonydb.go

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,23 +111,45 @@ func NewFromConfigWithITestID(t *testing.T, id ITestID) (*DB, error) {
111111
func New(hosts []string, username, password, database, port string, loadBalance bool, itestID ITestID) (*DB, error) {
112112
itest := string(itestID)
113113

114-
// Join hosts with the port
115-
hostPortPairs := make([]string, len(hosts))
116-
for i, host := range hosts {
117-
hostPortPairs[i] = fmt.Sprintf("%s:%s", host, port)
114+
if len(hosts) == 0 {
115+
return nil, xerrors.Errorf("no hosts provided")
116+
}
117+
118+
// Debug: Log which path we're taking
119+
logger.Infof("Yugabyte connection config: loadBalance=%v, hosts=%v, port=%s", loadBalance, hosts, port)
120+
121+
// When load balancing is disabled, use only the first host to prevent
122+
// Yugabyte client from discovering internal Docker IPs via topology discovery
123+
var connectionHost string
124+
if loadBalance {
125+
// Join all hosts with the port for load balancing
126+
hostPortPairs := make([]string, len(hosts))
127+
for i, host := range hosts {
128+
hostPortPairs[i] = fmt.Sprintf("%s:%s", host, port)
129+
}
130+
connectionHost = strings.Join(hostPortPairs, ",")
131+
} else {
132+
// Use only the first host when load balancing is disabled
133+
// This prevents topology discovery that would return internal Docker IPs
134+
connectionHost = fmt.Sprintf("%s:%s", hosts[0], port)
118135
}
119136

120137
// Construct the connection string
121138
connString := fmt.Sprintf(
122139
"postgresql://%s:%s@%s/%s?sslmode=disable",
123140
username,
124141
password,
125-
strings.Join(hostPortPairs, ","),
142+
connectionHost,
126143
database,
127144
)
128145

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

133155
schema := "curio"
@@ -143,6 +165,24 @@ func New(hosts []string, username, password, database, port string, loadBalance
143165
return nil, err
144166
}
145167

168+
// When load balancing is disabled, restrict the pool to only use the specified host
169+
// This prevents Yugabyte client from discovering and connecting to internal Docker IPs
170+
if !loadBalance {
171+
// Parse port as integer
172+
portInt, err := strconv.ParseUint(port, 10, 16)
173+
if err != nil {
174+
return nil, xerrors.Errorf("invalid port: %w", err)
175+
}
176+
177+
// Override the connection config to use only our specified host
178+
cfg.ConnConfig.Host = hosts[0]
179+
cfg.ConnConfig.Port = uint16(portInt)
180+
181+
// Note: Yugabyte-specific connection parameters (load_balance, fallback_to_topology_keys_only)
182+
// must be set in the connection string, not as runtime parameters.
183+
// The connection string already has these parameters set above.
184+
}
185+
146186
cfg.ConnConfig.OnNotice = func(conn *pgconn.PgConn, n *pgconn.Notice) {
147187
logger.Debug("database notice: " + n.Message + ": " + n.Detail)
148188
DBMeasures.Errors.M(1)

web/srv.go

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,73 @@ var webDev = os.Getenv("CURIO_WEB_DEV") == "1"
4141

4242
func GetSrv(ctx context.Context, deps *deps.Deps, devMode bool) (*http.Server, error) {
4343
mx := mux.NewRouter()
44-
mx.Use(corsMiddleware)
44+
45+
// Single CORS middleware that handles all CORS logic
46+
// Wrap the entire router to ensure middleware runs for all requests including unmatched routes
47+
corsHandler := func(next http.Handler) http.Handler {
48+
for _, ao := range deps.Cfg.HTTP.CORSOrigins {
49+
if ao == "*" {
50+
log.Infof("This CORS configuration allows any website to call irreversable APIs on the Curio node: %s", ao)
51+
}
52+
}
53+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
54+
// Handle OPTIONS preflight requests - always return 204, even if CORS not configured
55+
if r.Method == http.MethodOptions {
56+
if len(deps.Cfg.HTTP.CORSOrigins) > 0 {
57+
origin := r.Header.Get("Origin")
58+
var allowedOrigin string
59+
allowed := false
60+
61+
// Check if origin is allowed
62+
for _, ao := range deps.Cfg.HTTP.CORSOrigins {
63+
if ao == "*" || ao == origin {
64+
allowedOrigin = ao
65+
allowed = true
66+
break
67+
}
68+
}
69+
70+
if allowed {
71+
if allowedOrigin == "*" {
72+
73+
w.Header().Set("Access-Control-Allow-Origin", "*")
74+
} else {
75+
w.Header().Set("Access-Control-Allow-Origin", origin)
76+
}
77+
}
78+
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
79+
requestHeaders := r.Header.Get("Access-Control-Request-Headers")
80+
if requestHeaders != "" {
81+
w.Header().Set("Access-Control-Allow-Headers", requestHeaders)
82+
} else {
83+
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept, Accept-Encoding")
84+
}
85+
w.Header().Set("Access-Control-Max-Age", "86400")
86+
}
87+
w.WriteHeader(http.StatusNoContent)
88+
return
89+
}
90+
91+
// Set CORS headers for non-OPTIONS requests if CORS is configured
92+
if len(deps.Cfg.HTTP.CORSOrigins) > 0 {
93+
origin := r.Header.Get("Origin")
94+
if origin != "" {
95+
for _, ao := range deps.Cfg.HTTP.CORSOrigins {
96+
if ao == "*" || ao == origin {
97+
if ao == "*" {
98+
w.Header().Set("Access-Control-Allow-Origin", "*")
99+
} else {
100+
w.Header().Set("Access-Control-Allow-Origin", origin)
101+
}
102+
break
103+
}
104+
}
105+
}
106+
}
107+
108+
next.ServeHTTP(w, r)
109+
})
110+
}
45111

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

92158
return &http.Server{
93-
Handler: http.HandlerFunc(mx.ServeHTTP),
159+
Handler: corsHandler(mx),
94160
BaseContext: func(listener net.Listener) context.Context {
95161
ctx, _ := tag.New(context.Background(), tag.Upsert(metrics.APIInterface, "curio"))
96162
return ctx
@@ -278,19 +344,3 @@ func proxyCopy(dst, src *websocket.Conn, errc chan<- error, direction string) {
278344
}
279345
}
280346
}
281-
282-
func corsMiddleware(next http.Handler) http.Handler {
283-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
284-
w.Header().Set("Access-Control-Allow-Origin", "*")
285-
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
286-
w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
287-
w.Header().Set("Access-Control-Allow-Credentials", "true")
288-
289-
if r.Method == http.MethodOptions {
290-
w.WriteHeader(http.StatusOK)
291-
return
292-
}
293-
294-
next.ServeHTTP(w, r)
295-
})
296-
}

0 commit comments

Comments
 (0)