╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ serve.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
188 error.HttpConnectionClosing => break, 189 error.ReadFailed => break, 190 else => |e| return e, 191 }; 192 193 handleHttpGitRequest(repo_kind, any_repo_opts, io, allocator, repo_root_path, &http_server, &request) catch |request_err| { 194 try err.print("request failed: {s}\n", .{@errorName(request_err)}); 195 try err.flush(); 196 if (http_server.reader.state == .received_head) { 197 http_server.reader.state = .ready; 198 } 199 try writeSimpleResponse(&http_server, 500, "Internal Server Error", "text/plain", @errorName(request_err)); 200 }; 201 try http_server.out.flush(); 202 break; 203 } 204 } 205 206 fn handleHttpGitRequest( 207 comptime repo_kind: rp.RepoKind, 208 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 209 io: std.Io, 210 allocator: std.mem.Allocator, 211 repo_root_path: []const u8, 212 http_server: *std.http.Server, 213 request: *std.http.Server.Request, 214 ) !void { 215 const uri = try std.Uri.parseAfterScheme("", request.head.target); 216 const path = uri.path.percent_encoded; 217 if (path.len == 0 or path[0] != '/') { 218 try writeSimpleResponse(http_server, 400, "Bad Request", "text/plain", "bad path"); 219 return; 220 } 221 222 const handler, const suffix = findRoute(path) orelse { 223 if (http_server.reader.state == .received_head) { 224 http_server.reader.state = .ready; 225 } 226 try writeSimpleResponse(http_server, 404, "Not Found", "text/plain", "not found"); 227 return; 228 }; 229 230 const repo_rel_encoded = path[1 .. path.len - suffix.len]; 231 const repo_rel = try decodeAndValidateRepoPath(allocator, repo_rel_encoded); 232 defer allocator.free(repo_rel); 233 234 const repo_path = try std.fs.path.resolve(allocator, &.{ repo_root_path, repo_rel }); 235 defer allocator.free(repo_path); 236 237 if (!isSubPath(repo_root_path, repo_path)) { 238 if (http_server.reader.state == .received_head) { 239 http_server.reader.state = .ready; 240 } 241 try writeSimpleResponse(http_server, 403, "Forbidden", "text/plain", "forbidden"); 242 return; 243 } 244 245 const request_method = normalizeMethod(request.head.method); 246 const content_type = try allocator.dupe(u8, findHeader(request, "content-type") orelse ""); 247 defer allocator.free(content_type); 248 const has_remote_user = findHeader(request, "authorization") != null; 249 const protocol_version = protocolVersionFromHeader(findHeader(request, "git-protocol")); 250 251 const body = if (request.head.method == .POST) blk: { 252 const reader = try request.readerExpectContinue(&.{}); 253 break :blk try reader.allocRemaining(allocator, .unlimited); 254 } else try allocator.dupe(u8, ""); 255 defer allocator.free(body); 256 257 if (http_server.reader.state == .received_head) { 258 http_server.reader.state = .ready; 259 } 260 261 var body_reader = std.Io.Reader.fixed(body); 262 const create_if_missing = isReceivePack(handler, suffix, uri.query); 263 const http_backend_options = xit.net_server_http_backend.Options{ 264 .request_method = request_method, 265 .handler = handler, 266 .suffix = suffix, 267 .query_string = if (uri.query) |q| q.percent_encoded else "", 268 .content_type = content_type, 269 .has_remote_user = has_remote_user, 270 .protocol_version = protocol_version, 271 }; 272 273 try openRepoAndServe(repo_kind, any_repo_opts, io, allocator, repo_path, create_if_missing, HttpGitService{ 274 .body_reader = &body_reader, 275 .writer = http_server.out, 276 .options = http_backend_options, 277 }); 278 } 279 280 fn findRoute(path: []const u8) ?struct { xit.net_server_http_backend.HandlerKind, []const u8 } { 281 for (&xit.net_server_http_backend.routes) |*route| { 282 if (std.mem.endsWith(u8, path, route.suffix)) { 283 return .{ route.handler, route.suffix }; 284 } 285 } 286 return null; 287 } 288 289 fn runSshListener( 290 io: std.Io, 291 allocator: std.mem.Allocator, 292 host_key: *const serve_ssh_protocol.HostKey, 293 session_handler: *const serve_ssh.SessionHandler, 294 net_server: *std.Io.net.Server, 295 tasks: *std.Io.Group, 296 err: *std.Io.Writer, 297 ) void { 298 const Listener = struct { 299 io: std.Io, 300 allocator: std.mem.Allocator, 301 host_key: *const serve_ssh_protocol.HostKey, 302 session_handler: *const serve_ssh.SessionHandler, 303 net_server: *std.Io.net.Server, 304 tasks: *std.Io.Group, 305 err: *std.Io.Writer, 306 307 fn run(ctx: @This()) void { 308 while (true) { 309 const stream = ctx.net_server.accept(ctx.io) catch |accept_err| { 310 if (accept_err == error.Canceled) return; 311 logError(ctx.err, "ssh accept failed: {s}\n", .{@errorName(accept_err)}); 312 continue; 313 }; 314 315 const Connection = struct { 316 io: std.Io, 317 allocator: std.mem.Allocator, 318 host_key: *const serve_ssh_protocol.HostKey, 319 session_handler: *const serve_ssh.SessionHandler, 320 stream: std.Io.net.Stream, 321 err: *std.Io.Writer, 322 323 fn run(conn: @This()) void { 324 defer conn.stream.close(conn.io); 325 var recv_buf: [4096]u8 = undefined; 326 var send_buf: [4096]u8 = undefined; 327 var stream_reader = conn.stream.reader(conn.io, &recv_buf); 328 var stream_writer = conn.stream.writer(conn.io, &send_buf); 329 serve_ssh_protocol.handleConnection( 330 conn.io, 331 conn.allocator, 332 &stream_reader.interface, 333 &stream_writer.interface, 334 conn.host_key, 335 conn.session_handler, 336 ) catch |session_err| { 337 logError(conn.err, "ssh session failed: {s}\n", .{@errorName(session_err)}); 338 }; 339 } 340 }; 341 342 ctx.tasks.async(ctx.io, Connection.run, .{Connection{ 343 .io = ctx.io, 344 .allocator = ctx.allocator, 345 .host_key = ctx.host_key, 346 .session_handler = ctx.session_handler, 347 .stream = stream, 348 .err = ctx.err, 349 }}); 350 } 351 } 352 }; 353 354 tasks.async(io, Listener.run, .{Listener{ 355 .io = io, 356 .allocator = allocator, 357 .host_key = host_key, 358 .session_handler = session_handler, 359 .net_server = net_server, 360 .tasks = tasks, 361 .err = err, 362 }}); 363 } 364 365 fn openRepoAndServe( 366 comptime repo_kind: rp.RepoKind, 367 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 368 io: std.Io, 369 allocator: std.mem.Allocator, 370 repo_path: []const u8, 371 create_if_missing: bool, 372 service: anytype, 373 ) !void { 374 if (any_repo_opts.hash) |hash_kind| { 375 var repo = try openRepo(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), io, allocator, repo_path, create_if_missing); 376 defer repo.deinit(io, allocator); 377 try service.serve(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), &repo, io, allocator); 378 } else { 379 var any_repo = rp.AnyRepo(repo_kind, any_repo_opts).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) { 380 error.RepoNotFound => { 381 if (!create_if_missing) return open_err; 382 383 var repo = try openRepo(repo_kind, any_repo_opts.toRepoOpts(), io, allocator, repo_path, true); 384 defer repo.deinit(io, allocator); 385 try service.serve(repo_kind, any_repo_opts.toRepoOpts(), &repo, io, allocator); 386 return; 387 }, 388 else => |e| return e, 389 }; 390 defer any_repo.deinit(io, allocator); 391 392 switch (any_repo) { 393 inline else => |*repo| { 394 try service.serve(repo.self_repo_kind, repo.self_repo_opts, repo, io, allocator); 395 }, 396 } 397 } 398 } 399 400 fn openRepo( 401 comptime repo_kind: rp.RepoKind, 402 comptime repo_opts: rp.RepoOpts(repo_kind), 403 io: std.Io, 404 allocator: std.mem.Allocator, 405 repo_path: []const u8, 406 create_if_missing: bool, 407 ) !rp.Repo(repo_kind, repo_opts) { 408 return rp.Repo(repo_kind, repo_opts).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) { 409 error.RepoNotFound => { 410 if (!create_if_missing) return open_err; 411 412 var repo = try rp.Repo(repo_kind, repo_opts).init(io, allocator, .{ .path = repo_path }); 413 errdefer repo.deinit(io, allocator); 414 try repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); 415 try repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); 416 return repo; 417 }, 418 else => |e| return e, 419 }; 420 } 421 422 const HttpGitService = struct { 423 body_reader: *std.Io.Reader, 424 writer: *std.Io.Writer, 425 options: xit.net_server_http_backend.Options, 426 427 fn serve( 428 self: @This(), 429 comptime repo_kind: rp.RepoKind, 430 comptime repo_opts: rp.RepoOpts(repo_kind), 431 repo: *rp.Repo(repo_kind, repo_opts), 432 io: std.Io, 433 allocator: std.mem.Allocator, 434 ) !void { 435 try repo.httpBackend(io, allocator, self.body_reader, self.writer, .http, self.options); 436 } 437 }; 438 439 fn isReceivePack( 440 handler: xit.net_server_http_backend.HandlerKind, 441 suffix: []const u8, 442 query: ?std.Uri.Component, 443 ) bool { 444 return switch (handler) { 445 .run_service => std.mem.eql(u8, suffix, "/git-receive-pack"), 446 .get_info_refs => if (query) |q| 447 std.mem.startsWith(u8, q.percent_encoded, "service=git-receive-pack") 448 else 449 false, 450 }; 451 } 452 453 fn parseListenAddress(value: []const u8) !ListenAddress { 454 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidListenAddress; 455 if (colon == 0 or colon + 1 >= value.len) return error.InvalidListenAddress; 456 const port = try std.fmt.parseInt(u16, value[colon + 1 ..], 10); 457 return .{ .host = value[0..colon], .port = port }; 458 } 459 460 fn decodeAndValidateRepoPath(allocator: std.mem.Allocator, encoded: []const u8) ![]const u8 { 461 if (encoded.len == 0) return error.InvalidRepoPath; 462 463 const mutable = try allocator.dupe(u8, encoded); 464 errdefer allocator.free(mutable); 465 const decoded = std.Uri.percentDecodeInPlace(mutable); 466 467 var iter = std.mem.splitScalar(u8, decoded, '/'); 468 while (iter.next()) |segment| { 469 if (segment.len == 0 or std.mem.eql(u8, segment, ".") or std.mem.eql(u8, segment, "..")) { 470 return error.InvalidRepoPath; 471 } 472 } 473 474 return try allocator.realloc(mutable, decoded.len); 475 } 476 477 fn isSubPath(parent: []const u8, child: []const u8) bool { 478 if (std.mem.eql(u8, parent, std.fs.path.sep_str)) return std.fs.path.isAbsolute(child); 479 if (!std.mem.startsWith(u8, child, parent)) return false; 480 return child.len == parent.len or child[parent.len] == std.fs.path.sep; 481 } 482 483 fn normalizeMethod(method: std.http.Method) std.http.Method { 484 return if (method == .HEAD) .GET else method; 485 } 486 487 fn protocolVersionFromHeader(header: ?[]const u8) xit.net_server_common.ProtocolVersion { 488 const git_protocol = header orelse return .v0; 489 var version: xit.net_server_common.ProtocolVersion = .v0; 490 var iter = std.mem.splitScalar(u8, git_protocol, ':'); 491 while (iter.next()) |entry| { 492 const value = std.mem.trimStart(u8, entry, " "); 493 if (std.mem.startsWith(u8, value, "version=")) { 494 const v = value["version=".len..]; 495 if (std.mem.eql(u8, v, "2")) { 496 version = .v2; 497 } else if (std.mem.eql(u8, v, "1") and version != .v2) { 498 version = .v1; 499 } 500 } 501 } 502 return version; 503 } 504 505 fn findHeader(request: *std.http.Server.Request, name: []const u8) ?[]const u8 { 506 var it = request.iterateHeaders(); 507 while (it.next()) |header| { 508 if (std.ascii.eqlIgnoreCase(header.name, name)) return header.value; 509 } 510 return null; 511 } 512 513 fn writeSimpleResponse( 514 http_server: *std.http.Server, 515 code: u16, 516 message: []const u8, 517 content_type: []const u8, 518 body: []const u8, 519 ) !void { 520 try http_server.out.print( 521 "HTTP/1.1 {d} {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\n\r\n{s}", 522 .{ code, message, content_type, body.len, body }, 523 ); 524 } 525 526 fn logError(err: *std.Io.Writer, comptime fmt: []const u8, args: anytype) void { 527 err.print(fmt, args) catch return; 528 err.flush() catch {}; 529 } 530