main
go 300 lines 7.86 KB
Raw
1 package utils
2
3 import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io"
11 "net/http"
12 "net/url"
13 "strings"
14
15 "github.com/gosuda/portal-tunnel/v2/types"
16 facilitatortypes "github.com/gosuda/x402-facilitator/types"
17 )
18
19 type APIErrorResponse struct {
20 Status int
21 Code string
22 Message string
23 }
24
25 func (resp APIErrorResponse) Write(w http.ResponseWriter) {
26 WriteAPIError(w, resp.Status, resp.Code, resp.Message)
27 }
28
29 func WriteAPIData(w http.ResponseWriter, status int, data any) {
30 w.Header().Set("Content-Type", "application/json")
31 w.WriteHeader(status)
32 _ = json.NewEncoder(w).Encode(types.APIEnvelope[any]{OK: true, Data: data})
33 }
34
35 func WriteAPIError(w http.ResponseWriter, status int, code, message string) {
36 w.Header().Set("Content-Type", "application/json")
37 w.WriteHeader(status)
38 _ = json.NewEncoder(w).Encode(types.APIEnvelope[any]{
39 OK: false,
40 Error: &types.APIError{Code: code, Message: message},
41 })
42 }
43
44 func WritePaymentJSON(w http.ResponseWriter, status int, value any) {
45 w.Header().Set("Content-Type", "application/json")
46 w.Header().Set("Cache-Control", "no-store")
47 w.WriteHeader(status)
48 _ = json.NewEncoder(w).Encode(value)
49 }
50
51 func SetPaymentResponseHeaders(header http.Header, settled *facilitatortypes.PaymentSettleResponse) {
52 if header == nil || settled == nil {
53 return
54 }
55 raw, err := json.Marshal(settled)
56 if err != nil {
57 return
58 }
59 encoded := base64.StdEncoding.EncodeToString(raw)
60 header.Set(types.HeaderPaymentResponse, encoded)
61 header.Set(types.HeaderXPaymentResponse, encoded)
62 }
63
64 func StripPaymentHeaders(header http.Header) {
65 header.Del(types.HeaderXPayment)
66 header.Del(types.HeaderPaymentSignature)
67 header.Del(types.HeaderPaymentRequired)
68 header.Del(types.HeaderXPaymentRequired)
69 header.Del(types.HeaderPaymentResponse)
70 header.Del(types.HeaderXPaymentResponse)
71 }
72
73 func HandleAPICORS(w http.ResponseWriter, r *http.Request) bool {
74 header := w.Header()
75 header.Set("Access-Control-Allow-Origin", "*")
76 header.Set("Access-Control-Allow-Methods", "GET, HEAD, POST, DELETE, OPTIONS")
77 header.Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, "+types.HeaderAccessToken)
78 header.Set("Access-Control-Max-Age", "600")
79 if r.Method != http.MethodOptions {
80 return false
81 }
82 w.WriteHeader(http.StatusNoContent)
83 return true
84 }
85
86 func MethodNotAllowedError() APIErrorResponse {
87 return APIErrorResponse{
88 Status: http.StatusMethodNotAllowed,
89 Code: types.APIErrorCodeMethodNotAllowed,
90 Message: "method not allowed",
91 }
92 }
93
94 func InvalidRequestError(err error) APIErrorResponse {
95 return APIErrorResponse{
96 Status: http.StatusBadRequest,
97 Code: types.APIErrorCodeInvalidRequest,
98 Message: err.Error(),
99 }
100 }
101
102 func RequireMethod(w http.ResponseWriter, r *http.Request, method string) bool {
103 if r.Method == method {
104 return true
105 }
106 MethodNotAllowedError().Write(w)
107 return false
108 }
109
110 // PublicURLForPath resolves a public absolute URL from request forwarding headers.
111 func PublicURLForPath(r *http.Request, path string) string {
112 if r == nil {
113 return ""
114 }
115 scheme, _, _ := strings.Cut(r.Header.Get("X-Forwarded-Proto"), ",")
116 scheme = strings.ToLower(strings.TrimSpace(scheme))
117 if scheme == "" {
118 if r.TLS != nil {
119 scheme = "https"
120 } else {
121 scheme = "http"
122 }
123 }
124 host, _, _ := strings.Cut(r.Header.Get("X-Forwarded-Host"), ",")
125 host = strings.TrimSpace(host)
126 if host == "" {
127 host = strings.TrimSpace(r.Host)
128 }
129 if host == "" {
130 return path
131 }
132 if !strings.HasPrefix(path, "/") {
133 path = "/" + path
134 }
135 return scheme + "://" + host + path
136 }
137
138 func ResolveAPIURL(baseURL *url.URL, path string) *url.URL {
139 ref := &url.URL{Path: path}
140 if baseURL == nil {
141 return ref
142 }
143 return baseURL.ResolveReference(ref)
144 }
145
146 func httpDo(ctx context.Context, client *http.Client, method, rawURL string, body io.Reader, headers http.Header) (*http.Response, error) {
147 if client == nil {
148 client = DefaultHTTPClient
149 }
150
151 req, err := http.NewRequestWithContext(ctx, method, rawURL, body)
152 if err != nil {
153 return nil, err
154 }
155 for key, values := range headers {
156 for _, value := range values {
157 req.Header.Add(key, value)
158 }
159 }
160 return client.Do(req)
161 }
162
163 func HTTPDoJSON(ctx context.Context, client *http.Client, method, rawURL string, payload any, headers http.Header, out any) error {
164 body, reqHeaders, err := httpJSONRequest(payload, headers)
165 if err != nil {
166 return err
167 }
168
169 resp, err := httpDo(ctx, client, method, rawURL, body, reqHeaders)
170 if err != nil {
171 return err
172 }
173 defer resp.Body.Close()
174
175 if out == nil {
176 return nil
177 }
178 return json.NewDecoder(resp.Body).Decode(out)
179 }
180
181 func HTTPDoAPIPath(ctx context.Context, client *http.Client, baseURL *url.URL, method, path string, payload any, headers http.Header, out any) error {
182 body, reqHeaders, err := httpJSONRequest(payload, headers)
183 if err != nil {
184 return err
185 }
186
187 resp, err := httpDo(ctx, client, method, ResolveAPIURL(baseURL, path).String(), body, reqHeaders)
188 if err != nil {
189 return err
190 }
191 defer resp.Body.Close()
192
193 if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
194 return DecodeAPIRequestError(resp)
195 }
196
197 respBody, err := io.ReadAll(resp.Body)
198 if err != nil {
199 return fmt.Errorf("read response: %w", err)
200 }
201 if err := DecodeAPIData(respBody, out); err != nil {
202 var apiErr *types.APIRequestError
203 if errors.As(err, &apiErr) && apiErr.StatusCode == 0 {
204 apiErr.StatusCode = resp.StatusCode
205 }
206 return err
207 }
208 return nil
209 }
210
211 func DecodeAPIData(body []byte, out any) error {
212 var envelope types.APIEnvelope[json.RawMessage]
213 if err := json.Unmarshal(body, &envelope); err != nil {
214 return fmt.Errorf("decode response: %w", err)
215 }
216 if !envelope.OK {
217 if envelope.Error == nil {
218 return &types.APIRequestError{Message: "api response is not ok"}
219 }
220 return &types.APIRequestError{
221 Code: envelope.Error.Code,
222 Message: envelope.Error.Message,
223 }
224 }
225 if out == nil {
226 return nil
227 }
228 return json.Unmarshal(envelope.Data, out)
229 }
230
231 func DecodeAPIRequestError(resp *http.Response) error {
232 body, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<10))
233 var envelope types.APIEnvelope[json.RawMessage]
234 if err := json.Unmarshal(body, &envelope); err == nil && !envelope.OK {
235 if envelope.Error == nil {
236 return &types.APIRequestError{
237 StatusCode: resp.StatusCode,
238 Message: fmt.Sprintf("api request failed with status %d", resp.StatusCode),
239 }
240 }
241 return &types.APIRequestError{
242 StatusCode: resp.StatusCode,
243 Code: envelope.Error.Code,
244 Message: envelope.Error.Message,
245 }
246 }
247
248 return &types.APIRequestError{
249 StatusCode: resp.StatusCode,
250 Message: strings.TrimSpace(string(body)),
251 }
252 }
253
254 func DecodeJSONRequest[T any](w http.ResponseWriter, r *http.Request, maxBytes int64) (T, bool) {
255 dst, err := decodeJSONRequestBody[T](w, r, maxBytes)
256 if err != nil {
257 WriteAPIError(w, http.StatusBadRequest, types.APIErrorCodeInvalidJSON, err.Error())
258 return dst, false
259 }
260 return dst, true
261 }
262
263 func DecodeJSONRequestAs[T any](w http.ResponseWriter, r *http.Request, maxBytes int64, invalid APIErrorResponse) (T, bool) {
264 dst, err := decodeJSONRequestBody[T](w, r, maxBytes)
265 if err != nil {
266 invalid.Write(w)
267 return dst, false
268 }
269 return dst, true
270 }
271
272 func decodeJSONRequestBody[T any](w http.ResponseWriter, r *http.Request, maxBytes int64) (T, error) {
273 var dst T
274 r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
275 defer r.Body.Close()
276 if err := json.NewDecoder(r.Body).Decode(&dst); err != nil {
277 return dst, err
278 }
279 return dst, nil
280 }
281
282 func httpJSONRequest(payload any, headers http.Header) (io.Reader, http.Header, error) {
283 reqHeaders := make(http.Header, len(headers))
284 for key, values := range headers {
285 reqHeaders[key] = append([]string(nil), values...)
286 }
287
288 if payload == nil {
289 return nil, reqHeaders, nil
290 }
291
292 buf, err := json.Marshal(payload)
293 if err != nil {
294 return nil, nil, fmt.Errorf("marshal payload: %w", err)
295 }
296 if reqHeaders.Get("Content-Type") == "" {
297 reqHeaders.Set("Content-Type", "application/json")
298 }
299 return bytes.NewReader(buf), reqHeaders, nil
300 }