Files
five/hbrtl/httpx.go
CharlesKWON 675eaa4def feat(hbrtl): FV_HTTPGET / FV_HTTPPOST / FV_ZIP* / FV_XML_ROWS
New Five-native HTTP / ZIP / XML primitives so PRG code can do
HTTPS fetch, ZIP container reads, and streaming XML row extraction
without dropping into BEGINDUMP. FV_ prefix marks Five-original
RTL (distinct from Harbour-inherited HB_ surface).

FV_HTTPGET(cUrl [, hOpts]) / FV_HTTPPOST(cUrl, cBody [, hOpts])
  hOpts:   { headers: {=>}, timeout: nSec, tls_legacy: .T./.F. }
  Result:  { status, body, error, headers }
  tls_legacy re-enables TLS_RSA cipher suites for legacy
  endpoints (DART OpenAPI pins them).

FV_ZIPENTRIES(cZipBytes) / FV_ZIPREAD(cZipBytes, cEntryName)
  Read ZIP archives held in memory (e.g. from FV_HTTPGET).

FV_XML_ROWS(cXml, cRowTag)
  Streaming reader for repeating-record XML. Each row becomes a
  flat hash of immediate-child element name -> text. Verified
  against DART corpCode.xml: 30 MB / 118k rows in seconds, no
  full-tree allocation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 08:47:34 +09:00

149 lines
4.3 KiB
Go

// 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)
}