| 1632 | } |
| 1633 | |
| 1634 | static void dispatch_request(cbm_http_server_t *srv, cbm_http_conn_t *c, |
| 1635 | const cbm_http_req_t *req) { |
| 1636 | /* Build per-request CORS headers (only reflects localhost origins) */ |
| 1637 | update_cors(req); |
| 1638 | |
| 1639 | /* DNS-rebinding / cross-site guard: the server binds to loopback only, so a |
| 1640 | * request carrying any non-loopback Host was routed here under a foreign |
| 1641 | * name (a rebinding DNS record, a proxy) and must be refused before it can |
| 1642 | * reach a state-changing endpoint. A bare request with no Host header |
| 1643 | * (HTTP/1.0 local tooling) is still allowed. */ |
| 1644 | if (req->host[0] != '\0' && !host_is_loopback(req->host)) { |
| 1645 | cbm_http_replyf(c, 403, g_cors, "%s", "{\"error\":\"forbidden host\"}"); |
| 1646 | return; |
| 1647 | } |
| 1648 | |
| 1649 | bool is_get = strcmp(req->method, "GET") == 0; |
| 1650 | bool is_post = strcmp(req->method, "POST") == 0; |
| 1651 | bool is_delete = strcmp(req->method, "DELETE") == 0; |
| 1652 | |
| 1653 | /* OPTIONS preflight for CORS */ |
| 1654 | if (strcmp(req->method, "OPTIONS") == 0) { |
| 1655 | cbm_http_replyf(c, 204, g_cors, "%s", ""); |
| 1656 | return; |
| 1657 | } |
| 1658 | |
| 1659 | /* POST /rpc → JSON-RPC dispatch (reuses existing MCP tools) */ |
| 1660 | if (is_post && cbm_http_path_match(req->path, "/rpc")) { |
| 1661 | handle_rpc(c, req, srv->mcp); |
| 1662 | return; |
| 1663 | } |
| 1664 | |
| 1665 | /* GET /api/layout → 3D graph layout */ |
| 1666 | if (is_get && cbm_http_path_match(req->path, "/api/layout*")) { |
| 1667 | handle_layout(c, req); |
| 1668 | return; |
| 1669 | } |
| 1670 | |
| 1671 | /* GET /api/repo-info → git remote / branch for GitHub deep-links */ |
| 1672 | if (is_get && cbm_http_path_match(req->path, "/api/repo-info*")) { |
| 1673 | handle_repo_info(c, req); |
| 1674 | return; |
| 1675 | } |
| 1676 | |
| 1677 | /* POST /api/index → start background indexing */ |
| 1678 | if (is_post && cbm_http_path_match(req->path, "/api/index")) { |
| 1679 | handle_index_start(c, req); |
| 1680 | return; |
| 1681 | } |
| 1682 | |
| 1683 | /* GET /api/index-status → check indexing progress */ |
| 1684 | if (is_get && cbm_http_path_match(req->path, "/api/index-status")) { |
| 1685 | handle_index_status(c); |
| 1686 | return; |
| 1687 | } |
| 1688 | |
| 1689 | /* GET /api/ui-config → language and local UI preferences */ |
| 1690 | if (is_get && cbm_http_path_match(req->path, "/api/ui-config")) { |
| 1691 | handle_ui_config(c, req); |
no test coverage detected