search for standard sites
pub-search.waow.tech
search
zig
blog
atproto
1const std = @import("std");
2const net = std.net;
3const http = std.http;
4const mem = std.mem;
5const activity = @import("activity.zig");
6const search = @import("search.zig");
7const stats = @import("stats.zig");
8const dashboard = @import("dashboard.zig");
9
10const HTTP_BUF_SIZE = 8192;
11const QUERY_PARAM_BUF_SIZE = 64;
12
13pub fn handleConnection(conn: net.Server.Connection) void {
14 defer conn.stream.close();
15
16 var read_buffer: [HTTP_BUF_SIZE]u8 = undefined;
17 var write_buffer: [HTTP_BUF_SIZE]u8 = undefined;
18
19 var reader = conn.stream.reader(&read_buffer);
20 var writer = conn.stream.writer(&write_buffer);
21
22 var server = http.Server.init(reader.interface(), &writer.interface);
23
24 while (true) {
25 var request = server.receiveHead() catch |err| {
26 if (err != error.HttpConnectionClosing and err != error.EndOfStream) {
27 std.debug.print("http receive error: {}\n", .{err});
28 }
29 return;
30 };
31 handleRequest(&server, &request) catch |err| {
32 std.debug.print("request error: {}\n", .{err});
33 return;
34 };
35 if (!request.head.keep_alive) return;
36 }
37}
38
39fn handleRequest(server: *http.Server, request: *http.Server.Request) !void {
40 _ = server;
41 const target = request.head.target;
42
43 // cors preflight
44 if (request.head.method == .OPTIONS) {
45 try sendCorsHeaders(request, "");
46 return;
47 }
48
49 if (mem.startsWith(u8, target, "/search")) {
50 try handleSearch(request, target);
51 } else if (mem.eql(u8, target, "/tags")) {
52 try handleTags(request);
53 } else if (mem.eql(u8, target, "/stats")) {
54 try handleStats(request);
55 } else if (mem.eql(u8, target, "/health")) {
56 try sendJson(request, "{\"status\":\"ok\"}");
57 } else if (mem.eql(u8, target, "/popular")) {
58 try handlePopular(request);
59 } else if (mem.eql(u8, target, "/dashboard")) {
60 try handleDashboard(request);
61 } else if (mem.eql(u8, target, "/api/dashboard")) {
62 try handleDashboardApi(request);
63 } else if (mem.startsWith(u8, target, "/similar")) {
64 try handleSimilar(request, target);
65 } else if (mem.eql(u8, target, "/activity")) {
66 try handleActivity(request);
67 } else {
68 try sendNotFound(request);
69 }
70}
71
72fn handleSearch(request: *http.Server.Request, target: []const u8) !void {
73 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
74 defer arena.deinit();
75 const alloc = arena.allocator();
76
77 // parse query param: /search?q=something&tag=foo
78 const query = parseQueryParam(alloc, target, "q") catch "";
79 const tag_filter = parseQueryParam(alloc, target, "tag") catch null;
80
81 if (query.len == 0 and tag_filter == null) {
82 try sendJson(request, "{\"error\":\"enter a search term\"}");
83 return;
84 }
85
86 // perform FTS search - arena handles cleanup
87 const results = search.search(alloc, query, tag_filter) catch |err| {
88 stats.recordError();
89 return err;
90 };
91 stats.recordSearch(query);
92 try sendJson(request, results);
93}
94
95fn handleTags(request: *http.Server.Request) !void {
96 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
97 defer arena.deinit();
98 const alloc = arena.allocator();
99
100 const tags = try stats.getTags(alloc);
101 try sendJson(request, tags);
102}
103
104fn handlePopular(request: *http.Server.Request) !void {
105 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
106 defer arena.deinit();
107 const alloc = arena.allocator();
108
109 const popular = try stats.getPopular(alloc, 5);
110 try sendJson(request, popular);
111}
112
113fn parseQueryParam(alloc: std.mem.Allocator, target: []const u8, param: []const u8) ![]const u8 {
114 // look for ?param= or ¶m=
115 const patterns = [_][]const u8{ "?", "&" };
116 for (patterns) |prefix| {
117 var search_buf: [QUERY_PARAM_BUF_SIZE]u8 = undefined;
118 const search_str = std.fmt.bufPrint(&search_buf, "{s}{s}=", .{ prefix, param }) catch continue;
119 if (mem.indexOf(u8, target, search_str)) |idx| {
120 const encoded = target[idx + search_str.len ..];
121 const end = mem.indexOf(u8, encoded, "&") orelse encoded.len;
122 const query_encoded = encoded[0..end];
123 const buf = try alloc.dupe(u8, query_encoded);
124 // decode + as space (form-urlencoded), then percent-decode
125 for (buf) |*c| {
126 if (c.* == '+') c.* = ' ';
127 }
128 return std.Uri.percentDecodeInPlace(buf);
129 }
130 }
131 return error.NotFound;
132}
133
134fn handleStats(request: *http.Server.Request) !void {
135 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
136 defer arena.deinit();
137 const alloc = arena.allocator();
138
139 const db_stats = stats.getStats();
140
141 var response: std.ArrayList(u8) = .{};
142 defer response.deinit(alloc);
143
144 try response.print(alloc, "{{\"documents\":{d},\"publications\":{d},\"cache_hits\":{d},\"cache_misses\":{d}}}", .{ db_stats.documents, db_stats.publications, db_stats.cache_hits, db_stats.cache_misses });
145
146 try sendJson(request, response.items);
147}
148
149fn sendJson(request: *http.Server.Request, body: []const u8) !void {
150 try request.respond(body, .{
151 .status = .ok,
152 .extra_headers = &.{
153 .{ .name = "content-type", .value = "application/json" },
154 .{ .name = "access-control-allow-origin", .value = "*" },
155 .{ .name = "access-control-allow-methods", .value = "GET, OPTIONS" },
156 .{ .name = "access-control-allow-headers", .value = "content-type" },
157 },
158 });
159}
160
161fn sendCorsHeaders(request: *http.Server.Request, body: []const u8) !void {
162 try request.respond(body, .{
163 .status = .no_content,
164 .extra_headers = &.{
165 .{ .name = "access-control-allow-origin", .value = "*" },
166 .{ .name = "access-control-allow-methods", .value = "GET, OPTIONS" },
167 .{ .name = "access-control-allow-headers", .value = "content-type" },
168 },
169 });
170}
171
172fn sendNotFound(request: *http.Server.Request) !void {
173 try request.respond("{\"error\":\"not found\"}", .{
174 .status = .not_found,
175 .extra_headers = &.{
176 .{ .name = "content-type", .value = "application/json" },
177 .{ .name = "access-control-allow-origin", .value = "*" },
178 },
179 });
180}
181
182fn handleDashboardApi(request: *http.Server.Request) !void {
183 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
184 defer arena.deinit();
185 const alloc = arena.allocator();
186
187 const data = dashboard.fetch(alloc) catch {
188 try sendJson(request, "{\"error\":\"failed to fetch dashboard data\"}");
189 return;
190 };
191
192 const json_response = dashboard.toJson(alloc, data) catch {
193 try sendJson(request, "{\"error\":\"failed to serialize dashboard data\"}");
194 return;
195 };
196
197 try sendJson(request, json_response);
198}
199
200fn handleDashboard(request: *http.Server.Request) !void {
201 try request.respond("", .{
202 .status = .moved_permanently,
203 .extra_headers = &.{
204 .{ .name = "location", .value = "https://leaflet-search.pages.dev/dashboard.html" },
205 },
206 });
207}
208
209fn handleSimilar(request: *http.Server.Request, target: []const u8) !void {
210 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
211 defer arena.deinit();
212 const alloc = arena.allocator();
213
214 const uri = parseQueryParam(alloc, target, "uri") catch {
215 try sendJson(request, "{\"error\":\"missing uri parameter\"}");
216 return;
217 };
218
219 const results = search.findSimilar(alloc, uri, 5) catch {
220 try sendJson(request, "[]");
221 return;
222 };
223
224 try sendJson(request, results);
225}
226
227fn handleActivity(request: *http.Server.Request) !void {
228 const counts = activity.getCounts();
229
230 // format as JSON array
231 var buf: [512]u8 = undefined;
232 var stream = std.io.fixedBufferStream(&buf);
233 const writer = stream.writer();
234
235 writer.writeByte('[') catch return;
236 for (counts, 0..) |c, i| {
237 if (i > 0) writer.writeByte(',') catch return;
238 writer.print("{d}", .{c}) catch return;
239 }
240 writer.writeByte(']') catch return;
241
242 try sendJson(request, stream.getWritten());
243}