Module: Winhttp

Defined in:
lib/winhttp.rb,
lib/winhttp/version.rb,
ext/winhttp/winhttp.c

Overview

winhttp — HTTP for Ruby on the stack Windows already ships: WinHTTP in async mode (system TLS via Schannel, the OS certificate store, the user's proxy/PAC settings, HTTP/2, and transparent gzip), behind a thin client that parks fibers under winloop and blocks plainly without it.

require "winhttp"

r = Winhttp.get("https://example.com/")
r.status                    # => 200
r.headers["content-type"]   # => "text/html; charset=UTF-8"
r.text[0, 15]               # => "<!doctype html>"

A Session pools connections; reuse one for many requests. The same code path blocks a plain thread and parks a fiber under a Fiber::Scheduler (winloop) — there are no scheduler branches anywhere in winhttp.

Defined Under Namespace

Classes: Canceled, Closed, ConnectError, Error, OSError, ProtocolError, RedirectError, Request, ResolveError, Response, Session, TimeoutError, TlsError

Constant Summary collapse

TOKEN_RE =

---- HTTP token grammar (RFC 7230) for method + header names --------------

/\A[!#$%&'*+\-.^_`|~0-9A-Za-z]+\z/.freeze
MANAGED_HEADERS =

Header names WinHTTP manages itself; supplying them is an error.

%w[host content-length connection].freeze
REVOCATION_INT =

Revocation policy symbol <-> the C integer the session probe understands.

{ none: 0, best_effort: 1, strict: 2 }.freeze
REVOCATION_SYM =
{ 0 => :none, 1 => :best_effort, 2 => :strict }.freeze
VERSION =
"0.1.0"
EV_SEND =

Event-kind constants (consumed by the Ruby state machine).

INT2FIX(EV_SEND)
EV_HEADERS =
INT2FIX(EV_HEADERS)
EV_READ =
INT2FIX(EV_READ)
EV_ERROR =
INT2FIX(EV_ERROR)
EV_SECURE =
INT2FIX(EV_SECURE)
EV_LOST =
INT2FIX(EV_LOST)
ACCESS_AUTOMATIC =

Access-type constants for Session._open.

UINT2NUM(WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY)
ACCESS_NO_PROXY =
UINT2NUM(WINHTTP_ACCESS_TYPE_NO_PROXY)
ACCESS_NAMED =
UINT2NUM(WINHTTP_ACCESS_TYPE_NAMED_PROXY)
REDIRECT_DISALLOW_HTTPS_TO_HTTP =

Redirect-policy constants.

UINT2NUM(WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP)
REDIRECT_NEVER =
UINT2NUM(WINHTTP_OPTION_REDIRECT_POLICY_NEVER)

Class Method Summary collapse

Class Method Details

._crack(vurl) ⇒ Object

=====================================================================

URL cracking — Winhttp._crack(url) -> [secure, host, port, path]



1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
# File 'ext/winhttp/winhttp.c', line 1043

static VALUE
url_crack(VALUE mod, VALUE vurl)
{
    URL_COMPONENTS uc;
    WCHAR user[2], pass[2];
    WCHAR *url, *host = NULL, *path = NULL, *extra = NULL;
    size_t urllen, cap;
    int secure, err = 0;
    VALUE host_s, path_s;
    (void)mod;

    url = to_wide(vurl);

    /* A cracked component can never be longer than the URL itself, so sizing
     * host/path/extra at (urllen + 1) WCHARs each is always sufficient — this
     * is what makes legitimate long URLs (big OAuth/JWT/base64 query strings,
     * signed URLs) crack instead of failing ERROR_INSUFFICIENT_BUFFER against
     * the old fixed 256/4096-WCHAR stack buffers. */
    urllen = wcslen(url);
    cap = urllen + 1;
    host  = (WCHAR *)xmalloc(sizeof(WCHAR) * cap);
    path  = (WCHAR *)xmalloc(sizeof(WCHAR) * cap);
    extra = (WCHAR *)xmalloc(sizeof(WCHAR) * cap);

    memset(&uc, 0, sizeof(uc));
    uc.dwStructSize = sizeof(uc);
    uc.lpszHostName = host;       uc.dwHostNameLength = (DWORD)cap;
    uc.lpszUrlPath = path;        uc.dwUrlPathLength = (DWORD)cap;
    uc.lpszExtraInfo = extra;     uc.dwExtraInfoLength = (DWORD)cap;
    /* Non-zero length pointers so userinfo presence is detectable. */
    uc.lpszUserName = user;       uc.dwUserNameLength = (DWORD)(sizeof(user) / sizeof(WCHAR));
    uc.lpszPassword = pass;       uc.dwPasswordLength = (DWORD)(sizeof(pass) / sizeof(WCHAR));

    if (!WinHttpCrackUrl(url, 0, 0, &uc)) err = 1;
    xfree(url);

    /* Free the component buffers before any rb_raise (longjmp): copy out what
     * we still need into Ruby Strings first, then xfree, then raise/return. */
    if (err) {
        xfree(host); xfree(path); xfree(extra);
        rb_raise(rb_eArgError, "winhttp: could not parse URL");
    }

    if (uc.nScheme != INTERNET_SCHEME_HTTP && uc.nScheme != INTERNET_SCHEME_HTTPS) {
        xfree(host); xfree(path); xfree(extra);
        rb_raise(rb_eArgError, "winhttp: URL scheme must be http or https");
    }

    /* Reject embedded credentials (user:pass@host — never supported). */
    if (uc.dwUserNameLength > 0 || uc.dwPasswordLength > 0) {
        xfree(host); xfree(path); xfree(extra);
        rb_raise(rb_eArgError, "winhttp: credentials in URL are not supported");
    }

    secure = (uc.nScheme == INTERNET_SCHEME_HTTPS);
    host_s = wide_to_str(uc.lpszHostName, (int)uc.dwHostNameLength);

    /* path + query reassembled (OpenRequest takes path+extra together). */
    {
        VALUE p = wide_to_str(uc.lpszUrlPath, (int)uc.dwUrlPathLength);
        VALUE e = (uc.dwExtraInfoLength > 0)
                  ? wide_to_str(uc.lpszExtraInfo, (int)uc.dwExtraInfoLength)
                  : rb_utf8_str_new("", 0);
        if (RSTRING_LEN(p) == 0) p = rb_utf8_str_new("/", 1);
        path_s = rb_str_plus(p, e);
    }

    xfree(host); xfree(path); xfree(extra);

    return rb_ary_new3(4, secure ? Qtrue : Qfalse, host_s,
                       UINT2NUM(uc.nPort), path_s);
}

._live_countObject



1030
1031
1032
1033
1034
1035
1036
1037
# File 'ext/winhttp/winhttp.c', line 1030

static VALUE
live_count(VALUE mod)
{
    long acc = 0;
    (void)mod;
    st_foreach(g_reqs, live_count_i, (st_data_t)&acc);
    return LONG2NUM(acc);
}

._pump_drainObject

Winhttp._pump_drain — detach the event list (g_cs held only for the pointer swap), then per node (GVL held): EV_CLOSING bookkeeping in C (never routed), else build a [id, kind, a, b] tuple. Returns the tuple array. Also emits one EV_LOST tuple with id 0 when g_dropped > 0 (the Ruby pump fans it out to all mailboxes).



965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
# File 'ext/winhttp/winhttp.c', line 965

static VALUE
pump_drain(VALUE mod)
{
    evnode *head, *n;
    VALUE out = rb_ary_new();
    LONG dropped;
    (void)mod;

    EnterCriticalSection(&g_cs);
    head = g_head;
    g_head = g_tail = NULL;
    LeaveCriticalSection(&g_cs);

    n = head;
    while (n) {
        evnode *next = n->next;
        if (n->kind == EV_CLOSING) {
            /* Drain-side lifetime bookkeeping for id n->id (GVL held). */
            st_data_t v;
            if (st_lookup(g_reqs, (st_data_t)n->id, &v)) {
                whreq *r = (whreq *)v;
                whses *ses = r->ses;
                r->os_done = 1;
                if (r->hconn) { WinHttpCloseHandle(r->hconn); r->hconn = NULL; }
                if (ses && ses->live > 0) ses->live--;
                if (ses && ses->wrapper_gone && ses->live == 0 && !ses->closed) {
                    if (ses->hsession) WinHttpCloseHandle(ses->hsession);
                    free(ses);
                    r->ses = NULL;
                }
                whreq_maybe_free(r);
            }
            /* EV_CLOSING is consumed in C and not routed to any mailbox. */
        } else {
            rb_ary_push(out, rb_ary_new3(4,
                ULL2NUM(n->id), INT2NUM(n->kind),
                ULONG2NUM(n->a), ULONG2NUM(n->b)));
        }
        free(n);
        n = next;
    }

    dropped = InterlockedExchange(&g_dropped, 0);
    if (dropped > 0) {
        /* id 0 sentinel — the Ruby pump fans EV_LOST out to every mailbox. */
        rb_ary_push(out, rb_ary_new3(4, ULL2NUM(0), INT2NUM(EV_LOST),
                                     INT2NUM(0), INT2NUM(0)));
    }
    return out;
}

._pump_waitObject

Winhttp._pump_wait — block (GVL released) until the callback signals.



952
953
954
955
956
957
958
# File 'ext/winhttp/winhttp.c', line 952

static VALUE
pump_wait(VALUE mod)
{
    (void)mod;
    rb_thread_call_without_gvl(pump_wait_fn, NULL, pump_wait_ubf, NULL);
    return Qnil;
}

._raise_os(vapi, vcode, vsecure) ⇒ Object

Winhttp._raise_os(api, code, secure_flags) — raise the mapped subclass from Ruby (the state machine routes EV_ERROR here). For TlsError, decode the captured SECURE_FAILURE flag bits into the @details symbol array.



1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
# File 'ext/winhttp/winhttp.c', line 1119

static VALUE
raise_os(VALUE mod, VALUE vapi, VALUE vcode, VALUE vsecure)
{
    DWORD code = (DWORD)NUM2ULONG(vcode);
    VALUE klass = class_for_code(code);
    const char *api = NIL_P(vapi) ? "WinHttp" : StringValueCStr(vapi);
    (void)mod;

    if (klass == eTls && !NIL_P(vsecure)) {
        DWORD flags = (DWORD)NUM2ULONG(vsecure);
        VALUE details = rb_ary_new();
        VALUE exc, msg;
        WCHAR *buf = NULL;
        char detail[512];
        DWORD n;
        HMODULE hwh = GetModuleHandleW(L"winhttp.dll");

        if (flags & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REV_FAILED)
            rb_ary_push(details, ID2SYM(rb_intern("cert_rev_failed")));
        if (flags & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CERT)
            rb_ary_push(details, ID2SYM(rb_intern("invalid_cert")));
        if (flags & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REVOKED)
            rb_ary_push(details, ID2SYM(rb_intern("cert_revoked")));
        if (flags & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CA)
            rb_ary_push(details, ID2SYM(rb_intern("invalid_ca")));
        if (flags & WINHTTP_CALLBACK_STATUS_FLAG_CERT_CN_INVALID)
            rb_ary_push(details, ID2SYM(rb_intern("cert_cn_invalid")));
        if (flags & WINHTTP_CALLBACK_STATUS_FLAG_CERT_DATE_INVALID)
            rb_ary_push(details, ID2SYM(rb_intern("cert_date_invalid")));
        if (flags & WINHTTP_CALLBACK_STATUS_FLAG_SECURITY_CHANNEL_ERROR)
            rb_ary_push(details, ID2SYM(rb_intern("security_channel_error")));

        detail[0] = 0;
        n = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
                           FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS,
                           hwh, code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                           (LPWSTR)&buf, 0, NULL);
        if (n && buf) {
            while (n && (buf[n-1] == L'\r' || buf[n-1] == L'\n' || buf[n-1] == L'.')) buf[--n] = 0;
            WideCharToMultiByte(CP_UTF8, 0, buf, -1, detail, (int)sizeof(detail), NULL, NULL);
            detail[sizeof(detail) - 1] = 0;
        }
        if (buf) LocalFree(buf);
        if (detail[0])
            msg = rb_sprintf("%s: %s (error %lu)", api, detail, (unsigned long)code);
        else
            msg = rb_sprintf("%s failed (error %lu)", api, (unsigned long)code);
        exc = rb_exc_new_str(eTls, msg);
        rb_iv_set(exc, "@code", ULONG2NUM(code));
        rb_iv_set(exc, "@details", details);
        rb_exc_raise(exc);
    }

    raise_code(klass, api, code);
    return Qnil; /* unreachable */
}

.default_sessionObject

The lazily-created, process-lifetime default Session (all-default options). Creation is mutex-guarded. It is never closed; the OS reclaims it at exit.



119
120
121
# File 'lib/winhttp.rb', line 119

def default_session
  @default_session || @default_mutex.synchronize { @default_session ||= Session.new }
end

.get(url, headers: nil, timeout: nil, &chunk) ⇒ Object

Thin delegations to the default session (signatures/defaults identical to the Session instance methods).



125
126
127
# File 'lib/winhttp.rb', line 125

def get(url, headers: nil, timeout: nil, &chunk)
  default_session.get(url, headers: headers, timeout: timeout, &chunk)
end

.post(url, body: "", headers: nil, timeout: nil, &chunk) ⇒ Object



129
130
131
# File 'lib/winhttp.rb', line 129

def post(url, body: "", headers: nil, timeout: nil, &chunk)
  default_session.post(url, body: body, headers: headers, timeout: timeout, &chunk)
end

.request(method, url, body: nil, headers: nil, timeout: nil, &chunk) ⇒ Object



133
134
135
# File 'lib/winhttp.rb', line 133

def request(method, url, body: nil, headers: nil, timeout: nil, &chunk)
  default_session.request(method, url, body: body, headers: headers, timeout: timeout, &chunk)
end