返回 DeepSeek-Reasonix
httpserve.go
根目录 / internal / browser / httpserve.go
1 package browser
2
3 import (
4 "context"
5 "crypto/subtle"
6 "encoding/json"
7 "errors"
8 "net/http"
9 "strings"
10 )
11
12 // httpHandler serves the contract over any Executor: constant-time bearer
13 // check, bounded JSON bodies, sentinels as 409 bodies, everything else 500.
14 type httpHandler struct {
15 exec Executor
16 token string
17 mux *http.ServeMux
18 }
19
20 // NewHTTPHandler exposes exec at /v1/browser/<method> behind a bearer token.
21 // GET /v1/browser/health answers 204 while exec is available, 503 otherwise.
22 func NewHTTPHandler(exec Executor, token string) http.Handler {
23 h := &httpHandler{exec: exec, token: strings.TrimSpace(token), mux: http.NewServeMux()}
24 h.mux.HandleFunc("GET "+httpHealthRoute, h.health)
25 h.mux.HandleFunc("POST "+httpRoutePrefix+"tabs", h.tabs)
26 h.mux.HandleFunc("POST "+httpRoutePrefix+"open", h.open)
27 h.mux.HandleFunc("POST "+httpRoutePrefix+"navigate", h.navigate)
28 h.mux.HandleFunc("POST "+httpRoutePrefix+"snapshot", h.snapshot)
29 h.mux.HandleFunc("POST "+httpRoutePrefix+"screenshot", h.screenshot)
30 h.mux.HandleFunc("POST "+httpRoutePrefix+"act", h.act)
31 h.mux.HandleFunc("POST "+httpRoutePrefix+"downloads", h.downloads)
32 h.mux.HandleFunc("POST "+httpRoutePrefix+"close", h.close)
33 h.mux.HandleFunc("POST "+httpRoutePrefix+"capability", h.capability)
34 return h
35 }
36
37 func (h *httpHandler) capability(w http.ResponseWriter, r *http.Request) {
38 var in struct {
39 Name string `json:"name"`
40 Args json.RawMessage `json:"args"`
41 }
42 if !decodeBody(w, r, &in) {
43 return
44 }
45 exec, ok := h.exec.(CapabilityExecutor)
46 if !ok {
47 writeWireError(w, http.StatusNotImplemented, "capability_unsupported", "browser enhancements unavailable")
48 return
49 }
50 // Only published tools can cross this boundary; host RPC names are not input.
51 for _, candidate := range CapabilityTools(h.exec) {
52 if candidate.Name() == "browser_"+in.Name {
53 result, err := exec.BrowserCapability(sessionContext(r), in.Name, in.Args)
54 writeResult(w, result, err)
55 return
56 }
57 }
58 writeWireError(w, http.StatusNotImplemented, "capability_unsupported", "unknown browser capability")
59 }
60
61 func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
62 if !h.authorized(r) {
63 w.Header().Set("WWW-Authenticate", `Bearer realm="reasonix-browser"`)
64 writeWireError(w, http.StatusUnauthorized, "unauthorized", "invalid browser broker token")
65 return
66 }
67 h.mux.ServeHTTP(w, r)
68 }
69
70 func (h *httpHandler) authorized(r *http.Request) bool {
71 prefix, value, ok := strings.Cut(strings.TrimSpace(r.Header.Get("Authorization")), " ")
72 if !ok || !strings.EqualFold(prefix, "Bearer") || h.token == "" {
73 return false
74 }
75 return subtle.ConstantTimeCompare([]byte(strings.TrimSpace(value)), []byte(h.token)) == 1
76 }
77
78 func (h *httpHandler) health(w http.ResponseWriter, r *http.Request) {
79 if a, ok := h.exec.(Availability); ok && !a.Available(sessionContext(r)) {
80 writeWireError(w, http.StatusServiceUnavailable, "unavailable", "no browser is available for this session")
81 return
82 }
83 w.WriteHeader(http.StatusNoContent)
84 }
85
86 func sessionContext(r *http.Request) context.Context {
87 return WithSession(r.Context(), strings.TrimSpace(r.Header.Get(SessionHeader)))
88 }
89
90 // decode reads a bounded JSON body into in; a false return means the reply
91 // was already written.
92 func decodeBody(w http.ResponseWriter, r *http.Request, in any) bool {
93 r.Body = http.MaxBytesReader(w, r.Body, httpMaxRequestBytes)
94 if err := json.NewDecoder(r.Body).Decode(in); err != nil {
95 writeWireError(w, http.StatusBadRequest, "bad_request", "invalid JSON body: "+err.Error())
96 return false
97 }
98 return true
99 }
100
101 func writeWireError(w http.ResponseWriter, status int, code, message string) {
102 w.Header().Set("Content-Type", "application/json")
103 w.WriteHeader(status)
104 _ = json.NewEncoder(w).Encode(wireError{Error: code, Message: message})
105 }
106
107 // writeResult maps err onto the wire: sentinels become 409 with their code,
108 // anything else is a 500 the client reports as a plain error.
109 func writeResult(w http.ResponseWriter, v any, err error) {
110 if err != nil {
111 for _, code := range []string{wireStaleReference, wireTakenOver, wireNoGrant, wireUnknownOutcome} {
112 if errors.Is(err, wireErrorCodes[code]) {
113 writeWireError(w, http.StatusConflict, code, err.Error())
114 return
115 }
116 }
117 writeWireError(w, http.StatusInternalServerError, "failed", err.Error())
118 return
119 }
120 w.Header().Set("Content-Type", "application/json")
121 _ = json.NewEncoder(w).Encode(v)
122 }
123
124 func (h *httpHandler) tabs(w http.ResponseWriter, r *http.Request) {
125 var in struct{}
126 if !decodeBody(w, r, &in) {
127 return
128 }
129 tabs, err := h.exec.Tabs(sessionContext(r))
130 out := wireTabs{Tabs: make([]wireTab, 0, len(tabs))}
131 for _, t := range tabs {
132 out.Tabs = append(out.Tabs, toWireTab(t))
133 }
134 writeResult(w, out, err)
135 }
136
137 func (h *httpHandler) open(w http.ResponseWriter, r *http.Request) {
138 var in wireOpenRequest
139 if !decodeBody(w, r, &in) {
140 return
141 }
142 tab, err := h.exec.Open(sessionContext(r), OpenRequest(in))
143 writeResult(w, toWireTab(tab), err)
144 }
145
146 func (h *httpHandler) navigate(w http.ResponseWriter, r *http.Request) {
147 var in wireNavigateRequest
148 if !decodeBody(w, r, &in) {
149 return
150 }
151 tab, err := h.exec.Navigate(sessionContext(r), NavigateRequest(in))
152 writeResult(w, toWireTab(tab), err)
153 }
154
155 func (h *httpHandler) snapshot(w http.ResponseWriter, r *http.Request) {
156 var in wireSnapshotRequest
157 if !decodeBody(w, r, &in) {
158 return
159 }
160 snap, err := h.exec.Snapshot(sessionContext(r), SnapshotRequest(in))
161 writeResult(w, wireSnapshot(snap), err)
162 }
163
164 func (h *httpHandler) screenshot(w http.ResponseWriter, r *http.Request) {
165 var in wireScreenshotRequest
166 if !decodeBody(w, r, &in) {
167 return
168 }
169 shot, err := h.exec.Screenshot(sessionContext(r), ScreenshotRequest(in))
170 writeResult(w, wireScreenshot(shot), err)
171 }
172
173 func (h *httpHandler) act(w http.ResponseWriter, r *http.Request) {
174 var in wireActRequest
175 if !decodeBody(w, r, &in) {
176 return
177 }
178 res, err := h.exec.Act(sessionContext(r), in.request())
179 writeResult(w, wireActResult(res), err)
180 }
181
182 func (h *httpHandler) downloads(w http.ResponseWriter, r *http.Request) {
183 var in wireDownloadsRequest
184 if !decodeBody(w, r, &in) {
185 return
186 }
187 downloads, err := h.exec.Downloads(sessionContext(r), in.request())
188 out := wireDownloads{Downloads: make([]wireDownload, 0, len(downloads))}
189 for _, d := range downloads {
190 out.Downloads = append(out.Downloads, wireDownload(d))
191 }
192 writeResult(w, out, err)
193 }
194
195 func (h *httpHandler) close(w http.ResponseWriter, r *http.Request) {
196 var in wireCloseRequest
197 if !decodeBody(w, r, &in) {
198 return
199 }
200 writeResult(w, struct{}{}, h.exec.Close(sessionContext(r), CloseRequest(in)))
201 }
202
202 lines GO