;============================================================ ; iron_http.hsp — WinHTTP 簡易クライアントラッパ ; ; HSP から GET / POST を 1 行で叩けるようにする薄いラッパ。 ; 内部では Win32 WinHTTP API を直接呼んでいるので .NET 不要。 ; ; API: ; http_get url, body ; GET → body に本文 / stat に HTTP status ; http_post url, send_body, body [, ctype] ; POST → body に本文 / stat に HTTP status ; http_set_timeout sec ; 接続/送受信タイムアウト秒 (デフォルト 30) ; ; 例: ; #include "iron_http.hsp" ; ; http_get "https://api.open-meteo.com/v1/forecast?latitude=35.68&longitude=139.69¤t=temperature_2m", body ; if stat = 200 { ; mes body ; JSON 本文を表示 ; } else { ; mes "HTTP error: " + stat ; } ; ; POST 例: ; http_post "https://example.com/api", "{\"key\":\"value\"}", body, "application/json" ; mes body ; ; 注意: ; - HTTPS は自動で TLS ハンドシェイクされる (WinHTTP はシステム証明書ストアを使う) ; - リダイレクト (3xx) は WinHTTP デフォルトで自動追尾される (~5 hop まで) ; - User-Agent は省略時 "IronHSP/1.0" ; - 文字エンコーディングは応答 body をそのまま返すので、UTF-8 JSON は ; SJIS HSP 環境では mes 表示で化ける可能性あり (cnvuts 等で変換要) ;============================================================ #ifndef __iron_http_hsp__ #define __iron_http_hsp__ #module iron_http ;------------------------------------------------------------ ; WinHTTP API (winhttp.dll, W = Unicode 版) ; ; 内部実装で wstr バッファを扱うので、この層ではあえて intptr (raw ptr) で ; 受けて、ハンドル / バッファ管理を行う。 ;------------------------------------------------------------ #uselib "winhttp.dll" ; HINTERNET WinHttpOpen(LPCWSTR userAgent, DWORD accessType, LPCWSTR proxy, ; LPCWSTR proxyBypass, DWORD flags) #cfunc _http_open "WinHttpOpen" wstr, int, wstr, wstr, int ; HINTERNET WinHttpConnect(HINTERNET session, LPCWSTR serverName, INTERNET_PORT port, DWORD reserved) #cfunc _http_connect "WinHttpConnect" int, wstr, int, int ; HINTERNET WinHttpOpenRequest(HINTERNET hConnect, LPCWSTR verb, LPCWSTR objectName, ; LPCWSTR version, LPCWSTR referer, LPCWSTR* acceptTypes, ; DWORD flags) #cfunc _http_request "WinHttpOpenRequest" int, wstr, wstr, wstr, wstr, int, int ; BOOL WinHttpSendRequest(HINTERNET hRequest, LPCWSTR headers, DWORD headersLen, ; LPVOID optional, DWORD optionalLen, DWORD totalLen, DWORD_PTR context) #cfunc _http_send "WinHttpSendRequest" int, wstr, int, var, int, int, int #cfunc _http_send_nobody "WinHttpSendRequest" int, wstr, int, int, int, int, int ; BOOL WinHttpReceiveResponse(HINTERNET hRequest, LPVOID reserved) #cfunc _http_recv "WinHttpReceiveResponse" int, int ; BOOL WinHttpQueryDataAvailable(HINTERNET hRequest, LPDWORD numberOfBytesAvailable) #cfunc _http_query_avail "WinHttpQueryDataAvailable" int, var ; BOOL WinHttpReadData(HINTERNET hRequest, LPVOID buffer, DWORD numberOfBytesToRead, LPDWORD numberOfBytesRead) #cfunc _http_read "WinHttpReadData" int, var, int, var ; BOOL WinHttpQueryHeaders(HINTERNET hRequest, DWORD info, LPCWSTR name, ; LPVOID buffer, LPDWORD bufferLen, LPDWORD index) #cfunc _http_query_hdr "WinHttpQueryHeaders" int, int, int, var, var, var ; BOOL WinHttpCloseHandle(HINTERNET handle) #func _http_close "WinHttpCloseHandle" int ; BOOL WinHttpSetTimeouts(HINTERNET handle, int resolveTimeout, int connectTimeout, ; int sendTimeout, int receiveTimeout) #func _http_set_to "WinHttpSetTimeouts" int, int, int, int, int ;------------------------------------------------------------ ; URL パース helper (winhttp.dll の WinHttpCrackUrl を使う) ; ; URL_COMPONENTS 構造体は dwStructSize (DWORD) + 14 個のメンバ (各 8 byte) の ; 計 116 byte。ここでは struct を雑にバイト配列で確保し、必要なメンバだけ ; lpoke / lpeek で読み書きする。 ;------------------------------------------------------------ #cfunc _http_crack_url "WinHttpCrackUrlW" wstr, int, int, var ;------------------------------------------------------------ ; モジュール内部の状態 (timeout 等) ;------------------------------------------------------------ #deffunc _http_init if _http_initialized ! 0 : return _http_initialized = 1 _http_timeout_ms = 30000 sdim _http_extra_headers, 1024 _http_extra_headers = "" ; streaming state _http_stream_session = 0 _http_stream_connect = 0 _http_stream_request = 0 return ;------------------------------------------------------------ ; http_set_header "Header: value\r\nHeader2: value2\r\n" ; POST/GET の Content-Type 以外に追加するヘッダ。 ; Authorization, X-API-Key 等を渡すために使用。 ; 空文字でクリア。 ;------------------------------------------------------------ #deffunc http_set_header str hdr _http_init _http_extra_headers = hdr return ;------------------------------------------------------------ ; http_set_timeout sec ; タイムアウト (秒) を設定。 ;------------------------------------------------------------ #deffunc http_set_timeout int sec _http_init _http_timeout_ms = sec * 1000 return ;------------------------------------------------------------ ; 内部関数: URL から host / port / path / scheme を取り出す ; ; 簡易パーサ (WinHTTP に依存しない自前実装)。WinHTTP の WinHttpCrackUrl は ; URL_COMPONENTS 構造体経由でちょっと面倒なので、文字列操作で済ませる。 ; ; in: url ; out: stat(host), refstr(path), refdval(port), prmidx(is_https) ;------------------------------------------------------------ #deffunc _http_parse_url str url, var out_host, var out_path, var out_port, var out_https _u = url out_https = 0 ; scheme if instr(_u, 0, "https://") = 0 { out_https = 1 out_port = 443 _u = strmid(_u, 8, strlen(_u) - 8) } else : if instr(_u, 0, "http://") = 0 { out_https = 0 out_port = 80 _u = strmid(_u, 7, strlen(_u) - 7) } else { out_https = 0 out_port = 80 } ; first '/' splits host[:port] from path _slash = instr(_u, 0, "/") if _slash < 0 { _hp = _u out_path = "/" } else { _hp = strmid(_u, 0, _slash) out_path = strmid(_u, _slash, strlen(_u) - _slash) } ; check for :port in _hp _colon = instr(_hp, 0, ":") if _colon < 0 { out_host = _hp } else { out_host = strmid(_hp, 0, _colon) out_port = int(strmid(_hp, _colon + 1, strlen(_hp) - _colon - 1)) } return ;------------------------------------------------------------ ; 内部関数: HTTP リクエスト実行 ; ; in: url, verb, body, content_type, agent, out_body (var) ; out: out_body に body 本文、return で HTTP status ;------------------------------------------------------------ #deffunc _http_do str url, str verb, str body, str ctype, str agent, var out_body, local _host, local _path, local _port, local _https, local _hSession, local _hConnect, local _hRequest, local _flags, local _headers, local _buf, local _avail, local _read, local _result, local _http_status, local _status_buf, local _status_len, local _status_idx, local _result_body, local _result_body2, local _result_total, local _to_r, local _to_c, local _to_s, local _to_v _http_init sdim out_body, 16 _http_parse_url url, _host, _path, _port, _https _hSession = _http_open(agent, 1, "", "", 0) if _hSession = 0 : return 0 _to_r = _http_timeout_ms _to_c = _http_timeout_ms _to_s = _http_timeout_ms _to_v = _http_timeout_ms _http_set_to _hSession, _to_r, _to_c, _to_s, _to_v _hConnect = _http_connect(_hSession, _host, _port, 0) if _hConnect = 0 { _http_close _hSession return 0 } _flags = 0 if _https ! 0 : _flags = 0x00800000 _hRequest = _http_request(_hConnect, verb, _path, "", "", 0, _flags) if _hRequest = 0 { _http_close _hConnect _http_close _hSession return 0 } _headers = "" if strlen(ctype) > 0 { _headers = "Content-Type: " + ctype + "\r\n" } if strlen(_http_extra_headers) > 0 { _headers = _headers + _http_extra_headers } if strlen(body) > 0 { _result = _http_send(_hRequest, _headers, -1, body, strlen(body), strlen(body), 0) } else { _result = _http_send_nobody(_hRequest, _headers, -1, 0, 0, 0, 0) } if _result = 0 { _http_close _hRequest _http_close _hConnect _http_close _hSession return 0 } _result = _http_recv(_hRequest, 0) if _result = 0 { _http_close _hRequest _http_close _hConnect _http_close _hSession return 0 } ; HTTP ステータス取得: WINHTTP_QUERY_STATUS_CODE(19) | WINHTTP_QUERY_FLAG_NUMBER(0x20000000) sdim _status_buf, 16 _status_len = 16 _status_idx = 0 _result = _http_query_hdr(_hRequest, 19 | 0x20000000, 0, _status_buf, _status_len, _status_idx) if _result ! 0 { _http_status = lpeek(_status_buf, 0) } else { _http_status = 0 } ; 応答 body を読む sdim _result_body, 4096 _result_total = 0 repeat _result = _http_query_avail(_hRequest, _avail) if _result = 0 : break if _avail = 0 : break sdim _buf, _avail + 16 _result = _http_read(_hRequest, _buf, _avail, _read) if _result = 0 : break if _read = 0 : break if _result_total + _read >= varsize(_result_body) { sdim _result_body2, _result_total + _read + 4096 memcpy _result_body2, _result_body, _result_total _result_body = _result_body2 } memcpy _result_body, _buf, _read, _result_total, 0 _result_total += _read loop poke _result_body, _result_total, 0 _http_close _hRequest _http_close _hConnect _http_close _hSession out_body = _result_body return _http_status ;------------------------------------------------------------ ; http_get url, body ; body に本文、stat に HTTP status (200/404/500 等) ; ネットワークエラー時は stat=0, body="" ;------------------------------------------------------------ #deffunc http_get str url, var out_body, local _agent _agent = "IronHSP/1.0" _http_do url, "GET", "", "", _agent, out_body return stat ;------------------------------------------------------------ ; http_post url, body_send, out_body [, content_type] ;------------------------------------------------------------ #deffunc http_post str url, str body, var out_body, str ctype, local _agent, local _t _agent = "IronHSP/1.0" _t = ctype if strlen(_t) = 0 : _t = "application/x-www-form-urlencoded" _http_do url, "POST", body, _t, _agent, out_body return stat ;------------------------------------------------------------ ; 内部: バイナリ body 対応版 _http_do (multipart upload 用) ; ; in: url, verb, body_buf (var), body_len, ctype, agent, out_body (var) ; out: out_body に応答 body、return で HTTP status ;------------------------------------------------------------ #deffunc _http_do_buf str url, str verb, var body_buf, int body_len, str ctype, str agent, var out_body, local _host, local _path, local _port, local _https, local _hSession, local _hConnect, local _hRequest, local _flags, local _headers, local _buf, local _avail, local _read, local _result, local _http_status, local _status_buf, local _status_len, local _status_idx, local _result_body, local _result_body2, local _result_total, local _to_r, local _to_c, local _to_s, local _to_v _http_init sdim out_body, 16 _http_parse_url url, _host, _path, _port, _https _hSession = _http_open(agent, 1, "", "", 0) if _hSession = 0 : return 0 _to_r = _http_timeout_ms _to_c = _http_timeout_ms _to_s = _http_timeout_ms _to_v = _http_timeout_ms _http_set_to _hSession, _to_r, _to_c, _to_s, _to_v _hConnect = _http_connect(_hSession, _host, _port, 0) if _hConnect = 0 { _http_close _hSession return 0 } _flags = 0 if _https ! 0 : _flags = 0x00800000 _hRequest = _http_request(_hConnect, verb, _path, "", "", 0, _flags) if _hRequest = 0 { _http_close _hConnect _http_close _hSession return 0 } _headers = "" if strlen(ctype) > 0 { _headers = "Content-Type: " + ctype + "\r\n" } if strlen(_http_extra_headers) > 0 { _headers = _headers + _http_extra_headers } if body_len > 0 { _result = _http_send(_hRequest, _headers, -1, body_buf, body_len, body_len, 0) } else { _result = _http_send_nobody(_hRequest, _headers, -1, 0, 0, 0, 0) } if _result = 0 { _http_close _hRequest _http_close _hConnect _http_close _hSession return 0 } _result = _http_recv(_hRequest, 0) if _result = 0 { _http_close _hRequest _http_close _hConnect _http_close _hSession return 0 } sdim _status_buf, 16 _status_len = 16 _status_idx = 0 _result = _http_query_hdr(_hRequest, 19 | 0x20000000, 0, _status_buf, _status_len, _status_idx) if _result ! 0 { _http_status = lpeek(_status_buf, 0) } else { _http_status = 0 } sdim _result_body, 4096 _result_total = 0 repeat _result = _http_query_avail(_hRequest, _avail) if _result = 0 : break if _avail = 0 : break sdim _buf, _avail + 16 _result = _http_read(_hRequest, _buf, _avail, _read) if _result = 0 : break if _read = 0 : break if _result_total + _read >= varsize(_result_body) { sdim _result_body2, _result_total + _read + 4096 memcpy _result_body2, _result_body, _result_total _result_body = _result_body2 } memcpy _result_body, _buf, _read, _result_total, 0 _result_total += _read loop poke _result_body, _result_total, 0 _http_close _hRequest _http_close _hConnect _http_close _hSession out_body = _result_body return _http_status ;------------------------------------------------------------ ; http_post_file url, "file.wav", "file_field", "audio/wav", "key1=val1\nkey2=val2", body ; ; multipart/form-data ファイルアップロード。OpenAI Whisper API 等で使用。 ; ; url : POST 先 URL ; file_path : アップロードするローカルファイルパス ; file_field : multipart の name="..." 値 (例 "file") ; file_mime : ファイルの MIME (例 "audio/wav") ; extra_kv : 追加 form field を "key=value" 1 行/行 で指定 ; 例: "model=whisper-1\nlanguage=ja" ; out_body : 応答 body を受け取る var ; ; 戻り値: stat = HTTP status ;------------------------------------------------------------ #deffunc http_post_file str url, str file_path, str file_field, str file_mime, str extra_kv, var out_body, local _file_buf, local _file_len, local _boundary, local _body, local _pre, local _post, local _ctype, local _agent, local _total, local _filename, local _line, local _eq, local _kvkey, local _kvval, local _kv_remain, local _nl, local _pre_len, local _post_len ; ファイル読み込み exist file_path if strsize < 0 : return -1 _file_len = strsize sdim _file_buf, _file_len + 16 bload file_path, _file_buf, _file_len _boundary = "----IronHSPBoundary7MA4YWxkTrZu0gW" ; ファイル名 (パスを除いた basename) _filename = getpath(file_path, 8) ; 前半 (ファイルパート header) sdim _pre, 1024 _pre = "--" + _boundary + "\r\n" _pre += "Content-Disposition: form-data; name=\"" + file_field + "\"; filename=\"" + _filename + "\"\r\n" _pre += "Content-Type: " + file_mime + "\r\n\r\n" ; 後半 (\r\n + 追加 form fields + closing boundary) sdim _post, 4096 _post = "\r\n" ; extra_kv を行ごとに分解 _kv_remain = extra_kv repeat if strlen(_kv_remain) = 0 : break _nl = instr(_kv_remain, 0, "\n") if _nl < 0 { _line = _kv_remain _kv_remain = "" } else { _line = strmid(_kv_remain, 0, _nl) _kv_remain = strmid(_kv_remain, _nl + 1, strlen(_kv_remain) - _nl - 1) } if strlen(_line) = 0 : continue _eq = instr(_line, 0, "=") if _eq < 0 : continue _kvkey = strmid(_line, 0, _eq) _kvval = strmid(_line, _eq + 1, strlen(_line) - _eq - 1) _post += "--" + _boundary + "\r\n" _post += "Content-Disposition: form-data; name=\"" + _kvkey + "\"\r\n\r\n" _post += _kvval + "\r\n" loop _post += "--" + _boundary + "--\r\n" _pre_len = strlen(_pre) _post_len = strlen(_post) _total = _pre_len + _file_len + _post_len ; body buffer 組み立て sdim _body, _total + 16 memcpy _body, _pre, _pre_len, 0, 0 memcpy _body, _file_buf, _file_len, _pre_len, 0 memcpy _body, _post, _post_len, _pre_len + _file_len, 0 _ctype = "multipart/form-data; boundary=" + _boundary _agent = "IronHSP/1.0" _http_do_buf url, "POST", _body, _total, _ctype, _agent, out_body return stat ;------------------------------------------------------------ ; Streaming HTTP (chunk 単位 polling, Server-Sent Events 等に使用) ; ; http_stream_open url, body, ctype ; HTTP request を送信して stream 受信モードに入る ; open 中は同時に 1 本のみ許容 (内部状態がグローバル) ; ; http_stream_read buf, max_bytes ; 次の chunk を読み取る。stat に読めた byte 数 (0 = stream 終端) ; ; http_stream_close ; stream を閉じる ;------------------------------------------------------------ #deffunc http_stream_open str url, str body, str ctype, local _host, local _path, local _port, local _https, local _flags, local _headers, local _agent, local _result _http_init if _http_stream_request ! 0 : http_stream_close _http_parse_url url, _host, _path, _port, _https _agent = "IronHSP/1.0" _http_stream_session = _http_open(_agent, 1, "", "", 0) if _http_stream_session = 0 : return 0 _http_set_to _http_stream_session, _http_timeout_ms, _http_timeout_ms, _http_timeout_ms, _http_timeout_ms _http_stream_connect = _http_connect(_http_stream_session, _host, _port, 0) if _http_stream_connect = 0 { _http_close _http_stream_session _http_stream_session = 0 return 0 } _flags = 0 if _https ! 0 : _flags = 0x00800000 _http_stream_request = _http_request(_http_stream_connect, "POST", _path, "", "", 0, _flags) if _http_stream_request = 0 { _http_close _http_stream_connect _http_close _http_stream_session _http_stream_connect = 0 _http_stream_session = 0 return 0 } _headers = "" if strlen(ctype) > 0 : _headers = "Content-Type: " + ctype + "\r\n" if strlen(_http_extra_headers) > 0 : _headers = _headers + _http_extra_headers if strlen(body) > 0 { _result = _http_send(_http_stream_request, _headers, -1, body, strlen(body), strlen(body), 0) } else { _result = _http_send_nobody(_http_stream_request, _headers, -1, 0, 0, 0, 0) } if _result = 0 { http_stream_close : return 0 } _result = _http_recv(_http_stream_request, 0) if _result = 0 { http_stream_close : return 0 } return 1 #deffunc http_stream_read var out_buf, int max_bytes, local _avail, local _read, local _result, local _chunk if _http_stream_request = 0 : return -1 sdim out_buf, max_bytes + 16 _result = _http_query_avail(_http_stream_request, _avail) if _result = 0 : return 0 if _avail = 0 : return 0 sdim _chunk, _avail + 16 _result = _http_read(_http_stream_request, _chunk, _avail, _read) if _result = 0 : return 0 if _read = 0 : return 0 if _read > max_bytes - 1 : _read = max_bytes - 1 memcpy out_buf, _chunk, _read, 0, 0 poke out_buf, _read, 0 return _read #deffunc http_stream_close if _http_stream_request ! 0 { _http_close _http_stream_request _http_stream_request = 0 } if _http_stream_connect ! 0 { _http_close _http_stream_connect _http_stream_connect = 0 } if _http_stream_session ! 0 { _http_close _http_stream_session _http_stream_session = 0 } return #global #endif