defmodule ExCurl.Request do
@moduledoc false
use Zig,
otp_app: :ex_curl,
c: [link_lib: {:system, "curl"}],
nifs: [request: [], request_dirty_cpu: [:dirty_cpu]]
~Z"""
const beam = @import("beam");
const std = @import("std");
const cURL = @cImport({
@cInclude("curl/curl.h");
});
pub const Header = struct { key: []u8, value: []u8 };
pub const RequestFlags = struct {
follow_location: bool,
ssl_verifyhost: bool,
ssl_verifypeer: bool,
return_metrics: bool,
verbose: bool,
http_auth_negotiate: bool,
};
pub const RequestConfiguration = struct {
headers: []Header,
url: []u8,
method: []u8,
body: []u8,
flags: RequestFlags,
};
pub const ResponseMetrics = struct {
namelookup_time: f64,
connect_time: f64,
appconnect_time: f64,
pretransfer_time: f64,
starttransfer_time: f64,
total_time: f64,
};
pub const Response = struct {
body: []u8,
status_code: u64,
headers: []u8,
metrics: ?ResponseMetrics,
};
pub fn request_dirty_cpu(config: RequestConfiguration) !beam.term {
return request(config);
}
pub fn request(config: RequestConfiguration) !beam.term {
// initialize curl and vars
var arena_state = std.heap.ArenaAllocator.init(std.heap.c_allocator);
defer arena_state.deinit();
const allocator = arena_state.allocator();
const handle = cURL.curl_easy_init() orelse return beam.make_error_pair("init_failed", .{});
defer cURL.curl_easy_cleanup(handle);
var response_buffer = std.ArrayList(u8).init(allocator);
var headers_buffer = std.ArrayList(u8).init(allocator);
// superfluous when using an arena allocator, but
// important if the allocator implementation changes
defer response_buffer.deinit();
defer headers_buffer.deinit();
// set curl opts & callbacks
try setCurlOpts(allocator, handle, config);
// set headers
var header_slist: [*c]cURL.curl_slist = null;
defer cURL.curl_slist_free_all(header_slist);
for (config.headers) |header| {
const buf = try allocator.alloc(u8, header.key.len + 3 + header.value.len);
_ = try std.fmt.bufPrint(buf, "{s}: {s}\x00", .{ header.key, header.value });
header_slist = cURL.curl_slist_append(header_slist, buf.ptr);
allocator.free(buf);
}
// Response body callback
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_HTTPHEADER, header_slist) != cURL.CURLE_OK)
unreachable;
// Response body callback
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_WRITEFUNCTION, writeToArrayListCallback) != cURL.CURLE_OK)
unreachable;
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_WRITEDATA, &response_buffer) != cURL.CURLE_OK)
unreachable;
// Headers callback
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_HEADERFUNCTION, writeToArrayListCallback) != cURL.CURLE_OK)
unreachable;
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_HEADERDATA, &headers_buffer) != cURL.CURLE_OK)
unreachable;
// Request body
if (!std.mem.eql(u8, config.body, "")) {
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_READFUNCTION, readFn) != cURL.CURLE_OK)
unreachable;
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_READDATA, &config) != cURL.CURLE_OK)
unreachable;
}
// 3. perform request
const result = cURL.curl_easy_perform(handle);
if (result != cURL.CURLE_OK)
return beam.make_error_pair(result, .{});
// 4. getinfo and create response
const response = try makeResponse(handle, response_buffer, headers_buffer, config);
return beam.make(.{ .ok, response }, .{});
}
fn makeResponse(handle: *cURL.CURL, response_buffer: std.ArrayList(u8), headers_buffer: std.ArrayList(u8), config: RequestConfiguration) !Response {
var status_code: u64 = 0;
if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_RESPONSE_CODE, &status_code) != cURL.CURLE_OK)
return error.CURLGETINFO_FAILED;
return .{
.body = response_buffer.items,
.status_code = status_code,
.headers = headers_buffer.items,
.metrics = if (config.flags.return_metrics) try makeResponseMetrics(handle) else null,
};
}
fn makeResponseMetrics(handle: *cURL.CURL) !ResponseMetrics {
var total_time: f64 = 0;
if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_TOTAL_TIME_T, &total_time) != cURL.CURLE_OK)
return error.CURLGETINFO_FAILED;
var namelookup_time: f64 = 0;
if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_NAMELOOKUP_TIME_T, &namelookup_time) != cURL.CURLE_OK)
return error.CURLGETINFO_FAILED;
var connect_time: f64 = 0;
if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_CONNECT_TIME_T, &connect_time) != cURL.CURLE_OK)
return error.CURLGETINFO_FAILED;
var appconnect_time: f64 = 0;
if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_APPCONNECT_TIME_T, &appconnect_time) != cURL.CURLE_OK)
return error.CURLGETINFO_FAILED;
var pretransfer_time: f64 = 0;
if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_PRETRANSFER_TIME_T, &pretransfer_time) != cURL.CURLE_OK)
return error.CURLGETINFO_FAILED;
var starttransfer_time: f64 = 0;
if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_STARTTRANSFER_TIME_T, &starttransfer_time) != cURL.CURLE_OK)
return error.CURLGETINFO_FAILED;
return .{
.namelookup_time = namelookup_time,
.connect_time = connect_time,
.appconnect_time = appconnect_time,
.pretransfer_time = pretransfer_time,
.starttransfer_time = starttransfer_time,
.total_time = total_time,
};
}
fn setCurlOpts(allocator: std.mem.Allocator, handle: *cURL.CURL, config: RequestConfiguration) !void {
if (config.flags.verbose) {
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_VERBOSE, @as(c_long, 1)) != cURL.CURLE_OK)
unreachable;
}
if (config.flags.follow_location) {
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_FOLLOWLOCATION, @as(c_long, 1)) != cURL.CURLE_OK)
unreachable;
}
if (config.flags.ssl_verifypeer) {
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_SSL_VERIFYPEER, @as(c_long, 1)) != cURL.CURLE_OK)
unreachable;
} else {
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_SSL_VERIFYPEER, @as(c_long, 0)) != cURL.CURLE_OK)
unreachable;
}
if (config.flags.ssl_verifyhost) {
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_SSL_VERIFYHOST, @as(c_long, 1)) != cURL.CURLE_OK)
unreachable;
} else {
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_SSL_VERIFYHOST, @as(c_long, 0)) != cURL.CURLE_OK)
unreachable;
}
// Set options to support RFC 4559 for SPNEGO-based Kerberos authentication
// when this flag is enabled
if (config.flags.http_auth_negotiate) {
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_HTTPAUTH, cURL.CURLAUTH_NEGOTIATE) != cURL.CURLE_OK)
unreachable;
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_USERPWD, ":") != cURL.CURLE_OK)
unreachable;
}
// HTTP Method
if (std.mem.eql(u8, config.method, "POST")) {
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_POST, @as(c_long, 1)) != cURL.CURLE_OK)
unreachable;
} else if (!std.mem.eql(u8, config.method, "GET")) {
const method_as_c_string = allocator.dupeZ(u8, config.method) catch unreachable;
defer allocator.free(method_as_c_string);
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_CUSTOMREQUEST, method_as_c_string.ptr) != cURL.CURLE_OK)
unreachable;
}
// URL
const url_as_c_string = allocator.dupeZ(u8, config.url) catch unreachable;
defer allocator.free(url_as_c_string);
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_URL, url_as_c_string.ptr) != cURL.CURLE_OK)
unreachable;
}
fn readFn(dest: [*c]u8, size: usize, nmemb: usize, config: *RequestConfiguration) callconv(.C) usize {
const bufferSize = size * nmemb;
if (config.body.len == 0) {
return 0; // nothing to read
}
const n = @min(config.body.len, bufferSize);
std.mem.copyForwards(u8, dest[0..n], config.body[0..n]);
config.body = config.body[n..];
return n;
}
fn writeToArrayListCallback(data: *anyopaque, size: c_uint, nmemb: c_uint, user_data: *anyopaque) callconv(.C) c_uint {
var buffer: *std.ArrayList(u8) = @alignCast(@ptrCast(user_data));
var typed_data: [*]u8 = @ptrCast(data);
buffer.appendSlice(typed_data[0 .. nmemb * size]) catch return 0;
return nmemb * size;
}
"""
end