// httpx.go — Five-native HTTP client (FV_HTTPGET / FV_HTTPPOST). // // Distinct from the existing JSONHTTPGET/POST helpers in this package: // - bytes-safe body (ZIP/binary returns survive the round trip) // - response headers exposed as a hash // - per-request options (custom headers, timeout, TLS legacy ciphers) // // The tls_legacy option re-enables the older TLS_RSA cipher suites // that some Korean government endpoints (e.g. DART OpenAPI) still // require. Off by default — only flip it for servers you control or // know need it. package hbrtl import ( "crypto/tls" "io" "net/http" "strings" "time" "five/hbrt" ) // FV_HTTPGET(cUrl [, hOpts]) -> { status, body, error, headers } func FvHttpGet(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() url := t.Local(1).AsString() var opts hbrt.Value if nParams >= 2 { opts = t.Local(2) } t.RetVal(doHttp("GET", url, nil, opts)) } // FV_HTTPPOST(cUrl, cBody [, hOpts]) -> { status, body, error, headers } func FvHttpPost(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() url := t.Local(1).AsString() body := []byte(t.Local(2).AsString()) var opts hbrt.Value if nParams >= 3 { opts = t.Local(3) } t.RetVal(doHttp("POST", url, body, opts)) } func doHttp(method, url string, body []byte, opts hbrt.Value) hbrt.Value { hdrs, timeout, tlsLegacy := readHttpOpts(opts) client := &http.Client{Timeout: time.Duration(timeout) * time.Second} if tlsLegacy { client.Transport = legacyTlsTransport() } var bodyReader io.Reader if body != nil { bodyReader = strings.NewReader(string(body)) } req, err := http.NewRequest(method, url, bodyReader) if err != nil { return makeFvHttpResult(0, nil, nil, err.Error()) } for k, v := range hdrs { req.Header.Set(k, v) } if body != nil && req.Header.Get("Content-Type") == "" { req.Header.Set("Content-Type", "application/octet-stream") } resp, err := client.Do(req) if err != nil { return makeFvHttpResult(0, nil, nil, err.Error()) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return makeFvHttpResult(resp.StatusCode, nil, nil, err.Error()) } return makeFvHttpResult(resp.StatusCode, respBody, resp.Header, "") } // readHttpOpts parses the optional options hash. Unknown keys are // ignored so callers can pass extra metadata without us caring. func readHttpOpts(opts hbrt.Value) (headers map[string]string, timeoutSec int, tlsLegacy bool) { headers = map[string]string{} timeoutSec = 30 tlsLegacy = false if opts.IsNil() || !opts.IsHash() { return } h := opts.AsHash() if v := h.HashGet(hbrt.MakeString("timeout")); !v.IsNil() { timeoutSec = v.AsInt() } if v := h.HashGet(hbrt.MakeString("tls_legacy")); !v.IsNil() { tlsLegacy = v.AsBool() } if v := h.HashGet(hbrt.MakeString("headers")); !v.IsNil() && v.IsHash() { hh := v.AsHash() for i, k := range hh.Keys { headers[k.AsString()] = hh.Values[i].AsString() } } return } // legacyTlsTransport — TLS 1.2 with both modern ECDHE suites and the // older TLS_RSA fallbacks. Required by some Korean government / // financial endpoints (notably DART OpenAPI) that pin TLS_RSA. func legacyTlsTransport() *http.Transport { cfg := &tls.Config{ MinVersion: tls.VersionTLS12, CipherSuites: []uint16{ tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, }, } return &http.Transport{TLSClientConfig: cfg} } // makeFvHttpResult — { status, body, error, headers }. body is byte-safe. func makeFvHttpResult(status int, body []byte, respHeaders http.Header, errMsg string) hbrt.Value { hdrHash := &hbrt.HbHash{} for k, vals := range respHeaders { hdrHash.Append(hbrt.MakeString(strings.ToLower(k)), hbrt.MakeString(strings.Join(vals, ", "))) } out := &hbrt.HbHash{} out.Append(hbrt.MakeString("status"), hbrt.MakeInt(status)) out.Append(hbrt.MakeString("body"), hbrt.MakeStringBytes(body)) out.Append(hbrt.MakeString("error"), hbrt.MakeString(errMsg)) out.Append(hbrt.MakeString("headers"), hbrt.MakeHashFrom(hdrHash)) return hbrt.MakeHashFrom(out) }