/ src / main.zig
main.zig
  1  const std = @import("std");
  2  const zap = @import("zap");
  3  const oss = @import("os-stats");
  4  const utils = @import("utils");
  5  
  6  const os = std.os;
  7  const print = std.debug.print;
  8  const mem = std.mem;
  9  const fmt = std.fmt;
 10  const json = std.json;
 11  
 12  const CPUInfo = oss.CPUInfo;
 13  const RAMStat = oss.RAMStat;
 14  const OSInfo = oss.OSInfo;
 15  
 16  const str = []const u8;
 17  const banner = @embedFile("banner");
 18  
 19  const DEFAULT_PORT = 3040;
 20  const DEFAULT_PASSWORD = "admin";
 21  const DEFAULT_SERVERNAME = "ZDSM";
 22  
 23  var gpa = std.heap.GeneralPurposeAllocator(.{}){};
 24  var alloc = gpa.allocator();
 25  var ctx: *const Context = undefined;
 26  
 27  // TODO: Parse build.zig.zon at compile time to get version
 28  const SERVER_VERSION = "v0.1.0";
 29  
 30  const SoftwareInfo = struct { version: str = SERVER_VERSION };
 31  
 32  const ServerInfo = struct {
 33      id: str,
 34      uptime: i64,
 35      hostname: str,
 36      cpu: ?CPUInfo,
 37      ram: ?RAMStat,
 38      os: ?OSInfo,
 39  };
 40  
 41  const Infos = struct { software: SoftwareInfo, server: ServerInfo };
 42  
 43  const Context = struct {
 44      server_name: str,
 45      password: str,
 46  };
 47  
 48  fn getLoadAverage() str {
 49      // https://fr.wikipedia.org/wiki/Load_average
 50      return "TODO";
 51  }
 52  
 53  fn trimZerosRight(value: *[64:0]u8) []u8 {
 54      return value[0..mem.indexOfScalar(u8, value, 0).?];
 55  }
 56  
 57  fn processRequest(request: zap.Request) void {
 58      if (!(request.method == null) and !mem.eql(u8, request.method.?, "GET") or (!(request.path == null) and !mem.eql(u8, request.path.?, "/api"))) {
 59          request.setStatus(.not_found);
 60          request.sendJson("{\"Error\":\"BAD REQUEST\"}") catch return;
 61          utils.warn("Got malformed request: got {?s} /{?s}", .{ request.method, request.path }) catch return;
 62          return;
 63      }
 64  
 65      const authenticator = zap.Auth.BearerSingle;
 66      var auth = authenticator.init(alloc, ctx.password, null) catch return;
 67      defer auth.deinit();
 68  
 69      const rq = request;
 70      const ar = auth.authenticateRequest(&rq);
 71  
 72      if (ar != zap.Auth.AuthResult.AuthOK) {
 73          request.setStatus(.unauthorized);
 74          request.sendJson("{\"Error\":\"UNAUTHORIZED\"}") catch return;
 75          utils.warn("Login attempt failed", .{}) catch return;
 76          return;
 77      }
 78  
 79      var request_body: []const u8 = undefined;
 80      var cpu_info_buff: [256]u8 = undefined;
 81      var uname = os.uname();
 82  
 83      const system_info = Infos{
 84          .software = SoftwareInfo{},
 85          .server = ServerInfo{
 86              .id = ctx.server_name,
 87              .uptime = oss.getUptime(),
 88              .hostname = trimZerosRight(&uname.nodename),
 89              .cpu = CPUInfo{
 90                  .usage = oss.getCPUPercent(null) orelse 0,
 91                  .arch = trimZerosRight(&uname.machine),
 92                  .model = utils.parseKVPairOpenFile("/proc/cpuinfo", "model name", &cpu_info_buff, ':') catch return orelse "Data Unavailable",
 93              },
 94              .ram = oss.getRAMStats(),
 95              .os = OSInfo{
 96                  .type = trimZerosRight(&uname.sysname),
 97                  .platform = trimZerosRight(&uname.sysname), // basically the same as the OS type
 98                  .version = trimZerosRight(&uname.version),
 99                  .release = trimZerosRight(&uname.release),
100              },
101          },
102      };
103  
104      request_body = json.stringifyAlloc(alloc, system_info, .{}) catch "{\"Error\":\"Unable to generate JSON\"}";
105      request.sendJson(request_body) catch return;
106  }
107  
108  pub fn main() !void {
109      var password_buff: [50]u8 = undefined;
110      var server_name_buff: [50]u8 = undefined;
111      var port_buffer: [5]u8 = undefined;
112  
113      const password = utils.getenv("PASSWORD", &password_buff) orelse DEFAULT_PASSWORD;
114      const server_name = utils.getenv("SERVER_NAME", &server_name_buff) orelse DEFAULT_SERVERNAME;
115      const port = p: {
116          const env = utils.getenv("PORT", &port_buffer);
117          if (env == null) break :p DEFAULT_PORT;
118          break :p fmt.parseUnsigned(usize, env.?, 10) catch DEFAULT_PORT;
119      };
120  
121      ctx = &Context{ .server_name = server_name, .password = password };
122  
123      var server = zap.HttpListener.init(.{
124          .port = port,
125          .on_request = processRequest,
126          .log = false,
127      });
128  
129      print(banner, .{ server_name, SERVER_VERSION, port, password });
130  
131      try server.listen();
132      try utils.info("Server running on port {any}", .{@as(u16, @truncate(port))});
133  
134      zap.start(.{
135          .threads = 1,
136          .workers = 1,
137      });
138  }