Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for builtin libcurl and some other fixes #4

Merged
merged 6 commits into from
Dec 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(.{});
jiacai2050 marked this conversation as resolved.
Show resolved Hide resolved
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",
Cloudef marked this conversation as resolved.
Show resolved Hide resolved
.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);
}
Loading