do-dyn

Unnamed repository; edit this file 'description' to name the repository.
git clone git://code.mfashby.net:/do-dyn
Log | Files | Refs | README

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 }