main.zig (4500B)
1 const std = @import("std"); 2 3 pub fn main() !void { 4 var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 5 defer _ = gpa.deinit(); 6 const a = gpa.allocator(); 7 8 // Argument parsing 9 var ai = std.process.args(); 10 if (!ai.skip()) return error.Wtf; 11 const dotoken = ai.next() orelse return error.NoDoToken; 12 const domain = ai.next() orelse return error.NoDomain; 13 // then a list of type/name combos to update with our new IP 14 var toupdate = std.ArrayList(Rec).init(a); 15 while (ai.next()) |tpe| { 16 const name = ai.next() orelse return error.InvalidArgs; 17 try toupdate.append(.{ 18 .type = tpe, 19 .name = name, 20 }); 21 } 22 defer toupdate.deinit(); 23 24 // Check my current public IP on the router homepage 25 26 var cli = std.http.Client{ .allocator = a }; 27 defer cli.deinit(); 28 29 const ip = try getWanIp(&cli); 30 defer cli.allocator.free(ip); 31 std.log.info("Found IP address {s}", .{ip}); 32 33 // Now check my digital ocean records and update them if they are out of date 34 const url = try std.fmt.allocPrint(a, "https://api.digitalocean.com/v2/domains/{s}/records", .{domain}); 35 defer a.free(url); 36 const authorization_header_value = try std.fmt.allocPrint(a, "Bearer {s}", .{dotoken}); 37 defer a.free(authorization_header_value); 38 var headers = std.http.Headers.init(a); 39 try headers.append("Authorization", authorization_header_value); 40 defer headers.deinit(); 41 var domains = try cli.fetch(a, .{ 42 .location = .{ .url = url }, 43 .headers = headers, 44 }); 45 defer domains.deinit(); 46 if (domains.status != std.http.Status.ok) { 47 return error.HttpStatusError; 48 } 49 const bod = domains.body orelse return error.HttpNoBodyError; 50 var v = try std.json.parseFromSlice(DomainRecordsResp, a, bod, .{ .ignore_unknown_fields = true }); 51 defer v.deinit(); 52 const drr = v.value; 53 for (drr.domain_records) |dr| { 54 for (toupdate.items) |rec| { 55 if (std.mem.eql(u8, dr.name, rec.name) and std.mem.eql(u8, dr.type, rec.type)) { 56 // Now check the value 57 if (!std.mem.eql(u8, dr.data, ip)) { 58 std.log.info("record needs updaring type [{s}] name [{s}] data [{s}] -> [{s}]", .{ dr.type, dr.name, dr.data, ip }); 59 var dr2: DomainRec = dr; 60 dr2.data = ip; 61 62 // Make a PATCH request... 63 const url2 = try std.fmt.allocPrint(a, "https://api.digitalocean.com/v2/domains/{s}/records/{}", .{ domain, dr2.id }); 64 defer a.free(url2); 65 const payload = try std.json.stringifyAlloc(a, dr2, .{}); 66 defer a.free(payload); 67 var res2 = try cli.fetch(a, .{ .location = .{ .url = url2 }, .method = .PATCH, .payload = .{ .string = payload }, .headers = headers }); 68 defer res2.deinit(); 69 if (res2.status != std.http.Status.ok) { 70 return error.HttpStatusError; 71 } 72 } 73 } 74 } 75 } 76 } 77 78 const Rec = struct { 79 type: []const u8, 80 name: []const u8, 81 }; 82 83 const DomainRec = struct { 84 id: u32, 85 type: []const u8, 86 name: []const u8, 87 data: []const u8, 88 priority: ?u32, 89 port: ?u16, 90 ttl: ?u32, 91 weight: ?u16, 92 flags: ?u8, 93 tag: ?[]const u8, 94 }; 95 96 const DomainRecordsResp = struct { 97 domain_records: []DomainRec, 98 }; 99 100 fn getWanIp(cli: *std.http.Client) ![]const u8 { 101 // Look for this in the HTML of my router's login page 102 // not very cross platform, there are no doubt external services that could do this 103 // in a different way but this doesn't rely on those 104 // <span id="footer-wanip">84.65.54.66</span> 105 var resp = try cli.fetch(cli.allocator, .{ 106 .location = .{ .url = "http://192.168.1.1" }, 107 }); 108 defer resp.deinit(); 109 if (resp.status != std.http.Status.ok) { 110 return error.HttpStatusError; 111 } 112 var bod = resp.body orelse return error.HttpNoBodyError; 113 const wanstart = "<span id=\"footer-wanip\">"; 114 const wanend = "</span>"; 115 const start = (std.mem.indexOf(u8, bod, wanstart) orelse return error.NoWanIpFound) + wanstart.len; 116 const end = start + (std.mem.indexOf(u8, bod[start..], wanend) orelse return error.NoWanIpFound); 117 const ip = bod[start..end]; 118 _ = try std.net.Ip4Address.parse(ip, 0); // Check it's a valid IP... 119 return try cli.allocator.dupe(u8, ip); 120 }