Skip to content

Commit

Permalink
Add support for builtin libcurl and some other fixes (#4)
Browse files Browse the repository at this point in the history
* Fix crash on easy.do() which does not pass the proper type to curl
* Fix compile on latest zig
* Use standard optimize options in built.zig
* Fix typo in errors.zig
* Allow the use of certificates in std.crypto.Certificate.Bundle instead
of default one
* Include builtin libcurl compiled against mbedTls, the use of
std.crypto.Certificate.Bundle is required, or CURLOPT_CAINFO should be
used to point to cacert bundle.

---------

Co-authored-by: jiacai2050 <[email protected]>
  • Loading branch information
Cloudef and jiacai2050 authored Dec 23, 2023
1 parent cc4c5db commit 659c139
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 28 deletions.
22 changes: 14 additions & 8 deletions README.org
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#+TITLE: zig-curl
#+DATE: 2023-09-16T23:16:15+0800
#+LASTMOD: 2023-09-19T08:30:18+0800
#+LASTMOD: 2023-12-23T11:51:43+0800
#+OPTIONS: toc:nil num:nil
#+STARTUP: content

Expand All @@ -14,16 +14,21 @@ This package is in its early stage, although the core functionality works right
=zig-curl= only support [[https://ziglang.org/download/][Zig master]], and any contributions are welcomed. ⚒️
#+end_quote

The builtin libcurl consists of:
- curl 8.1.1
- mbedtls 3.4.0
- zlib 1.2.13

* Usage
#+begin_src zig
const Easy = @import("curl").Easy;
const curl = @import("curl");

pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() != .ok) @panic("leak");
const allocator = gpa.allocator();

const easy = try Easy.init(allocator);
const easy = try curl.Easy.init(allocator, .{});
defer easy.deinit();

const resp = try easy.get("http://httpbin.org/anything");
Expand Down Expand Up @@ -66,8 +71,9 @@ const curl = b.dependency("curl", .{});

// add curl module to an executable.
exe.addModule("curl", curl.module("curl"));
// Note: since this package doesn't bundle static libcurl,
// so users need to link to system-wide libcurl.
// For builtin libcurl
exe.linkLibrary(curl.artifact("curl"));
// For system-wide libcurl
exe.linkSystemLibrary("curl");
exe.linkLibC();
#+end_src
Expand Down
18 changes: 12 additions & 6 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,24 @@ const MODULE_NAME = "curl";

pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const module = b.addModule(MODULE_NAME, .{
.source_file = .{ .path = "src/main.zig" },
});

try addExample(b, "basic", module, target);
try addExample(b, "advanced", module, target);
const libcurl = b.dependency("libcurl", .{ .target = target, .optimize = optimize });
b.installArtifact(libcurl.artifact("curl"));

try addExample(b, "basic", module, libcurl, target, optimize);
try addExample(b, "advanced", module, libcurl, target, optimize);

const main_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
main_tests.addModule(MODULE_NAME, module);
main_tests.linkSystemLibrary("curl");
main_tests.linkLibC();
main_tests.linkLibrary(libcurl.artifact("curl"));

const run_main_tests = b.addRunArtifact(main_tests);
const test_step = b.step("test", "Run library tests");
Expand All @@ -31,18 +35,20 @@ fn addExample(
b: *std.Build,
comptime name: []const u8,
curl_module: *Module,
libcurl: *Build.Dependency,
target: std.zig.CrossTarget,
optimize: std.builtin.OptimizeMode,
) !void {
const exe = b.addExecutable(.{
.name = name,
.root_source_file = LazyPath.relative("examples/" ++ name ++ ".zig"),
.target = target,
.optimize = optimize,
});

b.installArtifact(exe);
exe.addModule(MODULE_NAME, curl_module);
exe.linkSystemLibrary("curl");
exe.linkLibC();
exe.linkLibrary(libcurl.artifact("curl"));

const run_step = b.step("run-" ++ name, std.fmt.comptimePrint("Run {s} example", .{name}));
run_step.dependOn(&b.addRunArtifact(exe).step);
Expand Down
16 changes: 16 additions & 0 deletions build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.{
.name = "zig-curl",
.version = "0.1.0",
.paths = .{
"src",
"build.zig",
"build.zig.zon",
"LICENSE",
},
.dependencies = .{
.libcurl = .{
.url = "https://github.com/Cloudef/zigcurl/archive/e9e726131d3eb80c02d20b7ee5ca6935324d6697.tar.gz",
.hash = "1220970ad7d9b96b3464fc12c1e67998881bc4d07c03b8b7d75a5367e4c9991ef099",
},
},
}
12 changes: 6 additions & 6 deletions examples/advanced.zig
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn put_with_custom_header(allocator: Allocator, easy: Easy) !void {
var stream = std.io.fixedBufferStream(
\\ {"name": "John", "age": 15}
);
var body = stream.reader();
const body = stream.reader();

const header = blk: {
var h = curl.RequestHeader.init(allocator);
Expand All @@ -34,7 +34,7 @@ fn put_with_custom_header(allocator: Allocator, easy: Easy) !void {
try h.add("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l");
break :blk h;
};
var req = curl.Request(@TypeOf(body)).init("http://httpbin.org/anything/zig-curl", body, .{
var req = curl.Request(@TypeOf(body)).init("https://httpbin.org/anything/zig-curl", body, .{
.method = .PUT,
.header = header,
.verbose = true,
Expand Down Expand Up @@ -66,7 +66,7 @@ fn put_with_custom_header(allocator: Allocator, easy: Easy) !void {
.age = 15,
},
.method = "PUT",
.url = "http://httpbin.org/anything/zig-curl",
.url = "https://httpbin.org/anything/zig-curl",
},
);

Expand All @@ -89,7 +89,7 @@ fn post_mutli_part(easy: Easy) !void {
try multi_part.add_part("build.zig", .{ .file = "build.zig" });
try multi_part.add_part("readme", .{ .file = "README.org" });

var req = curl.Request(void).init("http://httpbin.org/anything/mp", {}, .{
var req = curl.Request(void).init("https://httpbin.org/anything/mp", {}, .{
.method = .PUT,
.multi_part = multi_part,
.verbose = true,
Expand All @@ -105,9 +105,9 @@ fn post_mutli_part(easy: Easy) !void {
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() != .ok) @panic("leak");
var allocator = gpa.allocator();
const allocator = gpa.allocator();

const easy = try Easy.init(allocator);
const easy = try Easy.init(allocator, .{});
defer easy.deinit();

curl.print_libcurl_version();
Expand Down
8 changes: 4 additions & 4 deletions examples/basic.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const curl = @import("curl");
const Easy = curl.Easy;

fn get(easy: Easy) !void {
const resp = try easy.get("http://httpbin.org/anything");
const resp = try easy.get("https://httpbin.org/anything");
defer resp.deinit();

std.debug.print("Status code: {d}\nBody: {s}\n", .{
Expand All @@ -19,7 +19,7 @@ fn post(easy: Easy) !void {
var payload = std.io.fixedBufferStream(
\\{"name": "John", "age": 15}
);
const resp = try easy.post("http://httpbin.org/anything", "application/json", payload.reader());
const resp = try easy.post("https://httpbin.org/anything", "application/json", payload.reader());
defer resp.deinit();

std.debug.print("Status code: {d}\nBody: {s}\n", .{
Expand All @@ -31,9 +31,9 @@ fn post(easy: Easy) !void {
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() != .ok) @panic("leak");
var allocator = gpa.allocator();
const allocator = gpa.allocator();

const easy = try Easy.init(allocator);
const easy = try Easy.init(allocator, .{});
defer easy.deinit();

println("GET demo");
Expand Down
60 changes: 57 additions & 3 deletions src/easy.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const c = @import("c.zig").c;
const std = @import("std");
const errors = @import("errors.zig");
const util = @import("util.zig");

const mem = std.mem;
const fmt = std.fmt;
Expand All @@ -16,9 +17,12 @@ handle: *c.CURL,
/// The maximum time in milliseconds that the entire transfer operation to take.
timeout_ms: usize = 30_000,
default_user_agent: []const u8 = "zig-curl/0.1.0",
ca_bundle: ?[]const u8,

pub const HEADER_CONTENT_TYPE: []const u8 = "Content-Type";
pub const HEADER_USER_AGENT: []const u8 = "User-Agent";
const CERT_MARKER_BEGIN = "-----BEGIN CERTIFICATE-----";
const CERT_MARKER_END = "\n-----END CERTIFICATE-----\n";

pub const Method = enum {
GET,
Expand Down Expand Up @@ -222,17 +226,58 @@ pub const MultiPart = struct {
}
};

pub fn init(allocator: Allocator) !Self {
/// Init options for Easy handle
pub const EasyOptions = struct {
/// Use zig's std.crypto.Certificate.Bundle for TLS instead of libcurl's default.
/// Note that the builtin libcurl is compiled with mbedtls and does not include a CA bundle.
use_std_crypto_ca_bundle: bool = true,
};

pub fn init(allocator: Allocator, options: EasyOptions) !Self {
const ca_bundle = blk: {
if (options.use_std_crypto_ca_bundle) {
var bundle: std.crypto.Certificate.Bundle = .{};
defer bundle.deinit(allocator);

try bundle.rescan(allocator);

var blob = std.ArrayList(u8).init(allocator);
var iter = bundle.map.iterator();
while (iter.next()) |entry| {
const der = try std.crypto.Certificate.der.Element.parse(bundle.bytes.items, entry.value_ptr.*);
const cert = bundle.bytes.items[entry.value_ptr.*..der.slice.end];
const encoded = try util.encode_base64(allocator, cert);
defer allocator.free(encoded);

try blob.ensureUnusedCapacity(CERT_MARKER_BEGIN.len + CERT_MARKER_END.len + encoded.len);
try blob.appendSlice(CERT_MARKER_BEGIN);
for (encoded, 0..) |char, n| {
if (n % 64 == 0) try blob.append('\n');
try blob.append(char);
}
try blob.appendSlice(CERT_MARKER_END);
}
break :blk try blob.toOwnedSlice();
} else {
break :blk null;
}
};

return if (c.curl_easy_init()) |h|
.{
.allocator = allocator,
.handle = h,
.ca_bundle = ca_bundle,
}
else
error.CurlInit;
}

pub fn deinit(self: Self) void {
if (self.ca_bundle) |bundle| {
self.allocator.free(bundle);
}

c.curl_easy_cleanup(self.handle);
}

Expand All @@ -259,6 +304,15 @@ pub fn do(self: Self, req: anytype) !Response {
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_CUSTOMREQUEST, req.args.method.asString().ptr));
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_VERBOSE, req.getVerbose()));

if (self.ca_bundle) |bundle| {
const blob = c.curl_blob{
.data = @constCast(bundle.ptr),
.len = bundle.len,
.flags = c.CURL_BLOB_NOCOPY,
};
try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_CAINFO_BLOB, blob));
}

const body = try req.getBody(self.allocator);
defer if (body) |b| {
self.allocator.free(b);
Expand Down Expand Up @@ -288,11 +342,11 @@ pub fn do(self: Self, req: anytype) !Response {

try checkCode(c.curl_easy_perform(self.handle));

var status_code: i32 = 0;
var status_code: c_long = 0;
try checkCode(c.curl_easy_getinfo(self.handle, c.CURLINFO_RESPONSE_CODE, &status_code));

return .{
.status_code = status_code,
.status_code = @truncate(status_code),
.body = resp_buffer,
.handle = self.handle,
.allocator = self.allocator,
Expand Down
2 changes: 1 addition & 1 deletion src/errors.zig
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ pub fn checkCode(code: c.CURLcode) !void {
// https://curl.se/libcurl/c/libcurl-errors.html
std.log.debug("curl err code:{d}, msg:{s}\n", .{ code, c.curl_easy_strerror(code) });

return error.Unepxected;
return error.Unexpected;
}
11 changes: 11 additions & 0 deletions src/util.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const std = @import("std");
const Allocator = std.mem.Allocator;

const Encoder = std.base64.standard.Encoder;

pub fn encode_base64(allocator: Allocator, input: []const u8) ![]const u8 {
const encoded_len = Encoder.calcSize(input.len);
const dest = try allocator.alloc(u8, encoded_len);

return Encoder.encode(dest, input);
}

0 comments on commit 659c139

Please sign in to comment.