defmodule ExCurl.Request do
@moduledoc false
use ExCurl.Zig.MultiPlatformCurl
~Z"""
const cURL = @cImport({
@cInclude("curl.h");
});
const Header = struct {
key: []u8,
value: []u8
};
const RequestFlags = struct {
follow_location: bool,
ssl_verifyhost: bool,
ssl_verifypeer: bool,
return_metrics: bool,
verbose: bool,
};
const RequestConfiguration = struct {
headers: []Header,
url: []u8,
method: []u8,
body: []u8,
flags: RequestFlags,
};
/// nif: request_dirty_cpu/1 dirty_cpu
fn request_dirty_cpu(env: beam.env, json: []u8) !beam.term {
return request(env, json);
}
/// nif: request/1
fn request(env: beam.env, json: []u8) !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_atom(env, "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
var gpa = std.heap.GeneralPurposeAllocator(.{}){.backing_allocator = allocator};
var config: RequestConfiguration = try parseRequestConfiguration(gpa.allocator(), json);
defer std.json.parseFree(RequestConfiguration, config, .{
.allocator = gpa.allocator(),
});
try setCurlOpts(allocator, handle, response_buffer, headers_buffer, config);
// set headers
var header_slist: [*c]cURL.curl_slist = null;
defer cURL.curl_slist_free_all(header_slist);
for (config.headers) |header| {
var 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);
}
if (cURL.curl_easy_setopt(handle, cURL.CURLOPT_HTTPHEADER, header_slist) != cURL.CURLE_OK)
unreachable;
// 3. perform request
var result = cURL.curl_easy_perform(handle);
if (result != cURL.CURLE_OK)
return beam.make_error_term(env, beam.make_u64(env, result));
// 4. getinfo and create response
var response_list = try makeKeywordListResponse(env, handle, response_buffer, headers_buffer, config);
return beam.make_ok_term(env, response_list);
}
fn parseRequestConfiguration(allocator: std.mem.Allocator, json: []u8) !RequestConfiguration {
var stream = std.json.TokenStream.init(json);
var parsedData = try std.json.parse(RequestConfiguration, &stream, .{
.allocator = allocator,
.ignore_unknown_fields = true,
});
return parsedData;
}
fn makeKeywordListResponse(env: beam.env, handle: *cURL.CURL, response_buffer: std.ArrayList(u8), headers_buffer: std.ArrayList(u8), config: RequestConfiguration) !beam.term {
// metrics
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 total_time_term = beam.make_f64(env, total_time);
var total_time_tuple = try makeKeywordListTuple(env, "total_time", total_time_term);
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 namelookup_time_term = beam.make_f64(env, namelookup_time);
var name_lookup_tuple = try makeKeywordListTuple(env, "namelookup_time", namelookup_time_term);
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 connect_time_term = beam.make_f64(env, connect_time);
var connect_time_tuple = try makeKeywordListTuple(env, "connect_time", connect_time_term);
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 appconnect_time_term = beam.make_f64(env, appconnect_time);
var appconnect_time_tuple = try makeKeywordListTuple(env, "appconnect_time", appconnect_time_term);
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 pretransfer_time_term = beam.make_f64(env, pretransfer_time);
var pretransfer_time_tuple = try makeKeywordListTuple(env, "pretransfer_time", pretransfer_time_term);
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;
var starttransfer_time_term = beam.make_f64(env, starttransfer_time);
var starttransfer_time_tuple = try makeKeywordListTuple(env, "starttransfer_time", starttransfer_time_term);
var status_code: u64 = 0;
if (cURL.curl_easy_getinfo(handle, cURL.CURLINFO_RESPONSE_CODE, &status_code) != cURL.CURLE_OK)
return error.CURLGETINFO_FAILED;
var status_code_term = beam.make_u64(env, status_code);
var status_code_tuple = try makeKeywordListTuple(env, "status_code", status_code_term);
var response_body_term = beam.make_slice(env, response_buffer.items);
var response_body_tuple = try makeKeywordListTuple(env, "body", response_body_term);
var headers_term = beam.make_slice(env, headers_buffer.items);
var headers_tuple = try makeKeywordListTuple(env, "headers", headers_term);
// response list
var response_tuple_slice: []beam.term = undefined;
if (config.flags.return_metrics) {
response_tuple_slice = try beam.allocator.alloc(beam.term, 10);
response_tuple_slice[0] = response_body_tuple;
response_tuple_slice[1] = total_time_tuple;
response_tuple_slice[2] = name_lookup_tuple;
response_tuple_slice[3] = connect_time_tuple;
response_tuple_slice[4] = appconnect_time_tuple;
response_tuple_slice[5] = pretransfer_time_tuple;
response_tuple_slice[6] = starttransfer_time_tuple;
response_tuple_slice[7] = status_code_tuple;
response_tuple_slice[8] = try makeKeywordListTuple(env, "metrics_returned", beam.make_bool(env, true));
response_tuple_slice[9] = headers_tuple;
} else {
response_tuple_slice = try beam.allocator.alloc(beam.term, 3);
response_tuple_slice[0] = response_body_tuple;
response_tuple_slice[1] = status_code_tuple;
response_tuple_slice[2] = headers_tuple;
}
defer beam.allocator.free(response_tuple_slice);
return beam.make_term_list(env, response_tuple_slice);
}
fn makeKeywordListTuple(env: beam.env, key: []const u8, value: beam.term) !beam.term {
var key_atom = beam.make_atom(env, key);
var tuple_slice: []beam.term = try beam.allocator.alloc(beam.term, 2);
defer beam.allocator.free(tuple_slice);
tuple_slice[0] = key_atom;
tuple_slice[1] = value;
return beam.make_tuple(env, tuple_slice);
}
fn setCurlOpts(allocator: std.mem.Allocator, handle: *cURL.CURL, response_buffer: std.ArrayList(u8), headers_buffer: std.ArrayList(u8), config: RequestConfiguration) !void {
if (config.flags.verbose) {
if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_VERBOSE), @as(c_long, 1)) != cURL.CURLE_OK)
unreachable;
}
if (config.flags.follow_location) {
if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_FOLLOWLOCATION), @as(c_long, 1)) != cURL.CURLE_OK)
unreachable;
}
if (config.flags.ssl_verifypeer) {
if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_SSL_VERIFYPEER), @as(c_long, 1)) != cURL.CURLE_OK)
unreachable;
} else {
if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_SSL_VERIFYPEER), @as(c_long, 0)) != cURL.CURLE_OK)
unreachable;
}
if (config.flags.ssl_verifyhost) {
if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_SSL_VERIFYHOST), @as(c_long, 1)) != cURL.CURLE_OK)
unreachable;
} else {
if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_SSL_VERIFYHOST), @as(c_long, 0)) != cURL.CURLE_OK)
unreachable;
}
// HTTP Method
if (std.mem.eql(u8, config.method, "POST")) {
if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_POST), @as(c_long, 1)) != cURL.CURLE_OK)
unreachable;
} else if (!std.mem.eql(u8, config.method, "GET")) {
var method_as_c_string = std.cstr.addNullByte(allocator, config.method) catch unreachable;
defer allocator.free(method_as_c_string);
if (cURL.curl_easy_setopt(handle, @bitCast(c_uint, cURL.CURLOPT_CUSTOMREQUEST), method_as_c_string.ptr) != cURL.CURLE_OK)
unreachable;
}
// URL
var url_as_c_string = std.cstr.addNullByte(allocator, 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;
// 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;
}
// 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;
}
fn readFn(dest: [*]u8, size: usize, nmemb: usize, config: *RequestConfiguration) usize {
const bufferSize = size * nmemb;
if (config.body.len > 0) {
const n = std.math.min(config.body.len, bufferSize);
std.mem.copy(u8, dest[0..n], config.body[0..n]);
config.body = config.body[n..];
return n;
}
return 0;
}
fn writeToArrayListCallback(data: *anyopaque, size: c_uint, nmemb: c_uint, user_data: *anyopaque) callconv(.C) c_uint {
var buffer = @intToPtr(*std.ArrayList(u8), @ptrToInt(user_data));
var typed_data = @intToPtr([*]u8, @ptrToInt(data));
buffer.appendSlice(typed_data[0 .. nmemb * size]) catch return 0;
return nmemb * size;
}
"""
end