Skip to content

Commit 52f698d

Browse files
authored
Feat: Add api proxy dev server (#180)
1 parent 3b2c321 commit 52f698d

File tree

4 files changed

+184
-16
lines changed

4 files changed

+184
-16
lines changed

cmd/curio/rpc/rpc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ func ListenAndServe(ctx context.Context, dependencies *deps.Deps, shutdownChan c
296296
eg.Go(srv.ListenAndServe)
297297

298298
if dependencies.Cfg.Subsystems.EnableWebGui {
299-
web, err := web.GetSrv(ctx, dependencies)
299+
web, err := web.GetSrv(ctx, dependencies, false)
300300
if err != nil {
301301
return err
302302
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ require (
3737
github.com/google/go-cmp v0.6.0
3838
github.com/google/uuid v1.6.0
3939
github.com/gorilla/mux v1.8.1
40+
github.com/gorilla/websocket v1.5.3
4041
github.com/hashicorp/go-multierror v1.1.1
4142
github.com/hashicorp/golang-lru/v2 v2.0.7
4243
github.com/icza/backscanner v0.0.0-20210726202459-ac2ffc679f94
@@ -158,7 +159,6 @@ require (
158159
github.com/golang/snappy v0.0.4 // indirect
159160
github.com/google/gopacket v1.1.19 // indirect
160161
github.com/google/pprof v0.0.0-20240509144519-723abb6459b7 // indirect
161-
github.com/gorilla/websocket v1.5.3 // indirect
162162
github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 // indirect
163163
github.com/hannahhoward/go-pubsub v0.0.0-20200423002714-8d62886cc36e // indirect
164164
github.com/hashicorp/errwrap v1.1.0 // indirect

web/devsrv/main.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/filecoin-project/curio/deps"
8+
"github.com/filecoin-project/curio/deps/config"
9+
"github.com/filecoin-project/curio/web"
10+
)
11+
12+
func main() {
13+
srv, err := web.GetSrv(context.Background(),
14+
&deps.Deps{
15+
Cfg: &config.CurioConfig{
16+
Subsystems: config.CurioSubsystemsConfig{GuiAddress: ":4701"},
17+
}},
18+
true)
19+
20+
if err != nil {
21+
panic(err)
22+
}
23+
fmt.Println("Running on: ", srv.Addr)
24+
if err := srv.ListenAndServe(); err != nil {
25+
panic(err)
26+
}
27+
}

web/srv.go

Lines changed: 155 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@ import (
88
"fmt"
99
"io"
1010
"io/fs"
11-
"log"
1211
"net"
1312
"net/http"
13+
"net/http/httputil"
14+
"net/url"
1415
"os"
1516
"path"
1617
"strings"
1718
"time"
1819

1920
"github.com/gorilla/mux"
21+
"github.com/gorilla/websocket"
22+
logging "github.com/ipfs/go-log/v2"
2023
"go.opencensus.io/tag"
2124

2225
"github.com/filecoin-project/curio/deps"
@@ -25,35 +28,37 @@ import (
2528
"github.com/filecoin-project/lotus/metrics"
2629
)
2730

31+
var log = logging.Logger("web")
32+
2833
//go:embed static
2934
var static embed.FS
3035

3136
var basePath = "/static/"
3237

33-
// An dev mode hack for no-restart changes to static and templates.
34-
// You still need to recomplie the binary for changes to go code.
38+
// A dev mode hack for no-restart changes to static and templates.
39+
// You still need to recompile the binary for changes to go code.
3540
var webDev = os.Getenv("CURIO_WEB_DEV") == "1"
3641

37-
func GetSrv(ctx context.Context, deps *deps.Deps) (*http.Server, error) {
42+
func GetSrv(ctx context.Context, deps *deps.Deps, devMode bool) (*http.Server, error) {
3843
mx := mux.NewRouter()
39-
api.Routes(mx.PathPrefix("/api").Subrouter(), deps, webDev)
44+
if !devMode {
45+
api.Routes(mx.PathPrefix("/api").Subrouter(), deps, webDev)
46+
} else {
47+
if err := setupDevModeProxy(mx); err != nil {
48+
return nil, fmt.Errorf("failed to setup dev mode proxy: %v", err)
49+
}
50+
}
4051

4152
var static fs.FS = static
42-
if webDev {
53+
if webDev || devMode {
4354
basePath = ""
4455
static = os.DirFS("web/static")
4556
mx.Use(func(next http.Handler) http.Handler {
4657
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
47-
// Log the request
48-
log.Printf("Rcv request: %s %s", r.Method, r.URL.Path)
49-
58+
log.Debugf("Rcv request: %s %s", r.Method, r.URL.Path)
5059
x := &interceptResponseWriter{ResponseWriter: w}
51-
52-
// Call the next handler
5360
next.ServeHTTP(x, r)
54-
55-
// Log the response
56-
log.Printf("HTTP %s %s returned %d bytes with status code %d", r.Method, r.URL.Path, x.Length(), x.StatusCode())
61+
log.Debugf("HTTP %s %s returned %d bytes with status code %d", r.Method, r.URL.Path, x.Length(), x.StatusCode())
5762
})
5863
})
5964
}
@@ -131,3 +136,139 @@ func (w *interceptResponseWriter) Length() int {
131136
func (w *interceptResponseWriter) StatusCode() int {
132137
return w.status
133138
}
139+
140+
func setupDevModeProxy(mx *mux.Router) error {
141+
log.Debugf("Setting up dev mode proxy")
142+
apiSrv := os.Getenv("CURIO_API_SRV")
143+
if apiSrv == "" {
144+
return fmt.Errorf("CURIO_API_SRV environment variable is not set")
145+
}
146+
147+
apiURL, err := url.Parse(apiSrv)
148+
if err != nil {
149+
return fmt.Errorf("invalid CURIO_API_SRV URL: %v", err)
150+
}
151+
log.Debugf("Parsed API URL: %s", apiURL.String())
152+
153+
proxy := createReverseProxy(apiURL)
154+
155+
mx.PathPrefix("/api").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
156+
log.Debugf("Received request: %s %s", r.Method, r.URL.Path)
157+
if websocket.IsWebSocketUpgrade(r) {
158+
websocketProxy(apiURL, w, r)
159+
} else {
160+
proxy.ServeHTTP(w, r)
161+
}
162+
})
163+
164+
log.Infof("Dev mode proxy setup complete")
165+
return nil
166+
}
167+
168+
func createReverseProxy(target *url.URL) *httputil.ReverseProxy {
169+
log.Debugf("Creating reverse proxy")
170+
proxy := httputil.NewSingleHostReverseProxy(target)
171+
172+
originalDirector := proxy.Director
173+
proxy.Director = func(req *http.Request) {
174+
log.Debugf("Directing request: %s %s", req.Method, req.URL.Path)
175+
originalDirector(req)
176+
req.URL.Path = path.Join(target.Path, req.URL.Path)
177+
178+
if !strings.HasPrefix(req.URL.Path, "/") {
179+
req.URL.Path = "/" + req.URL.Path
180+
}
181+
182+
req.URL.Scheme = target.Scheme
183+
req.URL.Host = target.Host
184+
}
185+
186+
proxy.ModifyResponse = func(resp *http.Response) error {
187+
log.Debugf("Modifying response: %d %s", resp.StatusCode, resp.Status)
188+
resp.Header.Del("Connection")
189+
resp.Header.Del("Upgrade")
190+
return nil
191+
}
192+
193+
proxy.Transport = &http.Transport{
194+
Proxy: http.ProxyFromEnvironment,
195+
DialContext: (&net.Dialer{
196+
Timeout: 30 * time.Second,
197+
KeepAlive: 30 * time.Second,
198+
}).DialContext,
199+
ForceAttemptHTTP2: true,
200+
MaxIdleConns: 100,
201+
IdleConnTimeout: 90 * time.Second,
202+
TLSHandshakeTimeout: 10 * time.Second,
203+
ExpectContinueTimeout: 1 * time.Second,
204+
}
205+
206+
log.Infof("Reverse proxy created")
207+
return proxy
208+
}
209+
210+
func websocketProxy(target *url.URL, w http.ResponseWriter, r *http.Request) {
211+
log.Debugf("Starting WebSocket proxy")
212+
d := websocket.Dialer{
213+
Proxy: http.ProxyFromEnvironment,
214+
HandshakeTimeout: 45 * time.Second,
215+
}
216+
217+
// Preserve the original path and query
218+
wsTarget := *target
219+
wsTarget.Scheme = "ws"
220+
wsTarget.Path = path.Join(wsTarget.Path, r.URL.Path)
221+
222+
if !strings.HasPrefix(wsTarget.Path, "/") {
223+
wsTarget.Path = "/" + wsTarget.Path
224+
}
225+
226+
wsTarget.RawQuery = r.URL.RawQuery
227+
backendConn, resp, err := d.Dial(wsTarget.String(), nil)
228+
if err != nil {
229+
log.Errorf("Failed to connect to backend: %v", err)
230+
if resp != nil {
231+
log.Debugf("Backend response: %d %s", resp.StatusCode, resp.Status)
232+
}
233+
http.Error(w, "Failed to connect to backend", http.StatusServiceUnavailable)
234+
return
235+
}
236+
defer backendConn.Close()
237+
238+
upgrader := websocket.Upgrader{
239+
CheckOrigin: func(r *http.Request) bool {
240+
return true // Implement a more secure check in production
241+
},
242+
}
243+
clientConn, err := upgrader.Upgrade(w, r, nil)
244+
if err != nil {
245+
log.Errorf("Failed to upgrade connection: %v", err)
246+
http.Error(w, "Failed to upgrade connection", http.StatusInternalServerError)
247+
return
248+
}
249+
defer clientConn.Close()
250+
251+
errc := make(chan error, 2)
252+
go proxyCopy(clientConn, backendConn, errc, "client -> backend")
253+
go proxyCopy(backendConn, clientConn, errc, "backend -> client")
254+
255+
err = <-errc
256+
log.Debugf("WebSocket proxy ended: %v", err)
257+
}
258+
259+
func proxyCopy(dst, src *websocket.Conn, errc chan<- error, direction string) {
260+
for {
261+
messageType, p, err := src.ReadMessage()
262+
if err != nil {
263+
log.Errorf("Error reading message (%s): %v", direction, err)
264+
errc <- err
265+
return
266+
}
267+
log.Debugf("Proxying message (%s): type %d, size %d bytes", direction, messageType, len(p))
268+
if err := dst.WriteMessage(messageType, p); err != nil {
269+
log.Errorf("Error writing message (%s): %v", direction, err)
270+
errc <- err
271+
return
272+
}
273+
}
274+
}

0 commit comments

Comments
 (0)