返回 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 return h
34 }
35
36 func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
37 if !h.authorized(r) {
38 w.Header().Set("WWW-Authenticate", `Bearer realm="reasonix-browser"`)
39 writeWireError(w, http.StatusUnauthorized, "unauthorized", "invalid browser broker token")
40 return
41 }
42 h.mux.ServeHTTP(w, r)
43 }
44
45 func (h *httpHandler) authorized(r *http.Request) bool {
46 prefix, value, ok := strings.Cut(strings.TrimSpace(r.Header.Get("Authorization")), " ")
47 if !ok || !strings.EqualFold(prefix, "Bearer") || h.token == "" {
48 return false
49 }
50 return subtle.ConstantTimeCompare([]byte(strings.TrimSpace(value)), []byte(h.token)) == 1
51 }
52
53 func (h *httpHandler) health(w http.ResponseWriter, r *http.Request) {
54 if a, ok := h.exec.(Availability); ok && !a.Available(sessionContext(r)) {
55 writeWireError(w, http.StatusServiceUnavailable, "unavailable", "no browser is available for this session")
56 return
57 }
58 w.WriteHeader(http.StatusNoContent)
59 }
60
61 func sessionContext(r *http.Request) context.Context {
62 return WithSession(r.Context(), strings.TrimSpace(r.Header.Get(SessionHeader)))
63 }
64
65 // decode reads a bounded JSON body into in; a false return means the reply
66 // was already written.
67 func decodeBody(w http.ResponseWriter, r *http.Request, in any) bool {
68 r.Body = http.MaxBytesReader(w, r.Body, httpMaxRequestBytes)
69 if err := json.NewDecoder(r.Body).Decode(in); err != nil {
70 writeWireError(w, http.StatusBadRequest, "bad_request", "invalid JSON body: "+err.Error())
71 return false
72 }
73 return true
74 }
75
76 func writeWireError(w http.ResponseWriter, status int, code, message string) {
77 w.Header().Set("Content-Type", "application/json")
78 w.WriteHeader(status)
79 _ = json.NewEncoder(w).Encode(wireError{Error: code, Message: message})
80 }
81
82 // writeResult maps err onto the wire: sentinels become 409 with their code,
83 // anything else is a 500 the client reports as a plain error.
84 func writeResult(w http.ResponseWriter, v any, err error) {
85 if err != nil {
86 for _, code := range []string{wireStaleReference, wireTakenOver, wireNoGrant, wireUnknownOutcome} {
87 if errors.Is(err, wireErrorCodes[code]) {
88 writeWireError(w, http.StatusConflict, code, err.Error())
89 return
90 }
91 }
92 writeWireError(w, http.StatusInternalServerError, "failed", err.Error())
93 return
94 }
95 w.Header().Set("Content-Type", "application/json")
96 _ = json.NewEncoder(w).Encode(v)
97 }
98
99 func (h *httpHandler) tabs(w http.ResponseWriter, r *http.Request) {
100 var in struct{}
101 if !decodeBody(w, r, &in) {
102 return
103 }
104 tabs, err := h.exec.Tabs(sessionContext(r))
105 out := wireTabs{Tabs: make([]wireTab, 0, len(tabs))}
106 for _, t := range tabs {
107 out.Tabs = append(out.Tabs, toWireTab(t))
108 }
109 writeResult(w, out, err)
110 }
111
112 func (h *httpHandler) open(w http.ResponseWriter, r *http.Request) {
113 var in wireOpenRequest
114 if !decodeBody(w, r, &in) {
115 return
116 }
117 tab, err := h.exec.Open(sessionContext(r), OpenRequest(in))
118 writeResult(w, toWireTab(tab), err)
119 }
120
121 func (h *httpHandler) navigate(w http.ResponseWriter, r *http.Request) {
122 var in wireNavigateRequest
123 if !decodeBody(w, r, &in) {
124 return
125 }
126 tab, err := h.exec.Navigate(sessionContext(r), NavigateRequest(in))
127 writeResult(w, toWireTab(tab), err)
128 }
129
130 func (h *httpHandler) snapshot(w http.ResponseWriter, r *http.Request) {
131 var in wireSnapshotRequest
132 if !decodeBody(w, r, &in) {
133 return
134 }
135 snap, err := h.exec.Snapshot(sessionContext(r), SnapshotRequest(in))
136 writeResult(w, wireSnapshot(snap), err)
137 }
138
139 func (h *httpHandler) screenshot(w http.ResponseWriter, r *http.Request) {
140 var in wireScreenshotRequest
141 if !decodeBody(w, r, &in) {
142 return
143 }
144 shot, err := h.exec.Screenshot(sessionContext(r), ScreenshotRequest(in))
145 writeResult(w, wireScreenshot(shot), err)
146 }
147
148 func (h *httpHandler) act(w http.ResponseWriter, r *http.Request) {
149 var in wireActRequest
150 if !decodeBody(w, r, &in) {
151 return
152 }
153 res, err := h.exec.Act(sessionContext(r), in.request())
154 writeResult(w, wireActResult(res), err)
155 }
156
157 func (h *httpHandler) downloads(w http.ResponseWriter, r *http.Request) {
158 var in wireDownloadsRequest
159 if !decodeBody(w, r, &in) {
160 return
161 }
162 downloads, err := h.exec.Downloads(sessionContext(r), in.request())
163 out := wireDownloads{Downloads: make([]wireDownload, 0, len(downloads))}
164 for _, d := range downloads {
165 out.Downloads = append(out.Downloads, wireDownload(d))
166 }
167 writeResult(w, out, err)
168 }
169
170 func (h *httpHandler) close(w http.ResponseWriter, r *http.Request) {
171 var in wireCloseRequest
172 if !decodeBody(w, r, &in) {
173 return
174 }
175 writeResult(w, struct{}{}, h.exec.Close(sessionContext(r), CloseRequest(in)))
176 }
177
177 lines GO