@velox0/cerver 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -0
- package/bin/cerver.js +44 -0
- package/lib/assets/discover.js +50 -0
- package/lib/assets/embed.js +124 -0
- package/lib/assets/minify.js +83 -0
- package/lib/codegen/emit.js +249 -0
- package/lib/codegen/generator.js +111 -0
- package/lib/codegen/route_table.js +44 -0
- package/lib/commands/build.js +122 -0
- package/lib/commands/new.js +96 -0
- package/lib/commands/run.js +47 -0
- package/lib/compiler/compile.js +91 -0
- package/lib/config.js +52 -0
- package/lib/ir/transform.js +335 -0
- package/lib/ir/types.js +204 -0
- package/lib/parser/discover.js +74 -0
- package/lib/parser/parse.js +50 -0
- package/lib/validator/validate.js +179 -0
- package/package.json +28 -0
- package/runtime/cerver.h +185 -0
- package/runtime/http_parser.c +195 -0
- package/runtime/http_writer.c +137 -0
- package/runtime/mime.c +84 -0
- package/runtime/router.c +150 -0
- package/runtime/server.c +344 -0
- package/runtime/static.c +145 -0
- package/templates/cerver.config.js +6 -0
- package/templates/index.route.js +3 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const walk = require("acorn-walk");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Allowed AST node types in cerver route files.
|
|
7
|
+
* Anything not in this set will be rejected.
|
|
8
|
+
*/
|
|
9
|
+
const ALLOWED_NODES = new Set([
|
|
10
|
+
"Program",
|
|
11
|
+
"ExportNamedDeclaration",
|
|
12
|
+
"ExportDefaultDeclaration",
|
|
13
|
+
"FunctionDeclaration",
|
|
14
|
+
"VariableDeclaration",
|
|
15
|
+
"VariableDeclarator",
|
|
16
|
+
"BlockStatement",
|
|
17
|
+
"ExpressionStatement",
|
|
18
|
+
"ReturnStatement",
|
|
19
|
+
"IfStatement",
|
|
20
|
+
"BinaryExpression",
|
|
21
|
+
"LogicalExpression",
|
|
22
|
+
"UnaryExpression",
|
|
23
|
+
"CallExpression",
|
|
24
|
+
"MemberExpression",
|
|
25
|
+
"Identifier",
|
|
26
|
+
"Literal",
|
|
27
|
+
"TemplateLiteral",
|
|
28
|
+
"TemplateElement",
|
|
29
|
+
"ObjectExpression",
|
|
30
|
+
"Property",
|
|
31
|
+
"ArrayExpression",
|
|
32
|
+
"ConditionalExpression",
|
|
33
|
+
"AssignmentExpression",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Explicitly banned features with human-readable error messages.
|
|
38
|
+
*/
|
|
39
|
+
const BANNED_NODES = {
|
|
40
|
+
AwaitExpression: "async/await is not supported",
|
|
41
|
+
YieldExpression: "generators are not supported",
|
|
42
|
+
ForStatement: "for loops are not supported",
|
|
43
|
+
ForInStatement: "for...in loops are not supported",
|
|
44
|
+
ForOfStatement: "for...of loops are not supported",
|
|
45
|
+
WhileStatement: "while loops are not supported",
|
|
46
|
+
DoWhileStatement: "do...while loops are not supported",
|
|
47
|
+
ClassDeclaration: "classes are not supported",
|
|
48
|
+
ClassExpression: "class expressions are not supported",
|
|
49
|
+
NewExpression: "the 'new' operator is not supported",
|
|
50
|
+
ImportDeclaration: "runtime imports are not supported (use export functions)",
|
|
51
|
+
ImportExpression: "dynamic imports are not supported",
|
|
52
|
+
TryStatement: "try/catch is not supported (errors are compile-time)",
|
|
53
|
+
ThrowStatement: "throw is not supported",
|
|
54
|
+
WithStatement: "'with' is not supported",
|
|
55
|
+
ThisExpression: "'this' is not supported",
|
|
56
|
+
SwitchStatement: "switch statements are not supported (use if/else)",
|
|
57
|
+
LabeledStatement: "labeled statements are not supported",
|
|
58
|
+
BreakStatement: "break is not supported",
|
|
59
|
+
ContinueStatement: "continue is not supported",
|
|
60
|
+
ArrowFunctionExpression: "arrow functions are not supported (use named function exports)",
|
|
61
|
+
FunctionExpression: "function expressions are not supported (use named function declarations)",
|
|
62
|
+
SpreadElement: "spread syntax is not supported",
|
|
63
|
+
RestElement: "rest parameters are not supported",
|
|
64
|
+
TaggedTemplateExpression: "tagged templates are not supported",
|
|
65
|
+
MetaProperty: "meta properties are not supported",
|
|
66
|
+
SequenceExpression: "comma expressions are not supported",
|
|
67
|
+
UpdateExpression: "increment/decrement (++/--) is not supported",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Banned function calls.
|
|
72
|
+
*/
|
|
73
|
+
const BANNED_CALLS = new Set([
|
|
74
|
+
"eval",
|
|
75
|
+
"Function",
|
|
76
|
+
"setTimeout",
|
|
77
|
+
"setInterval",
|
|
78
|
+
"require",
|
|
79
|
+
"import",
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validate a route file AST.
|
|
84
|
+
* Throws an error with file location on first violation.
|
|
85
|
+
*
|
|
86
|
+
* @param {object} ast - ESTree AST from Acorn
|
|
87
|
+
* @param {string} filePath - For error messages
|
|
88
|
+
* @param {string} source - Original source for context
|
|
89
|
+
*/
|
|
90
|
+
function validate(ast, filePath, source) {
|
|
91
|
+
const errors = [];
|
|
92
|
+
|
|
93
|
+
function addError(node, message) {
|
|
94
|
+
const loc = node.loc
|
|
95
|
+
? `${filePath}:${node.loc.start.line}:${node.loc.start.column}`
|
|
96
|
+
: filePath;
|
|
97
|
+
errors.push(`cerver: error: ${loc} — ${message}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Check top-level structure: only exports allowed */
|
|
101
|
+
for (const node of ast.body) {
|
|
102
|
+
if (
|
|
103
|
+
node.type !== "ExportNamedDeclaration" &&
|
|
104
|
+
node.type !== "ExportDefaultDeclaration" &&
|
|
105
|
+
node.type !== "VariableDeclaration" &&
|
|
106
|
+
node.type !== "ExpressionStatement"
|
|
107
|
+
) {
|
|
108
|
+
addError(
|
|
109
|
+
node,
|
|
110
|
+
`top-level ${node.type} is not allowed (only exports and variable declarations)`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Validate exported functions are named GET or POST */
|
|
115
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration) {
|
|
116
|
+
const decl = node.declaration;
|
|
117
|
+
if (decl.type === "FunctionDeclaration") {
|
|
118
|
+
const name = decl.id.name;
|
|
119
|
+
if (!["GET", "POST"].includes(name)) {
|
|
120
|
+
addError(
|
|
121
|
+
decl,
|
|
122
|
+
`exported function "${name}" is not a valid HTTP method (use GET or POST)`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Check async */
|
|
127
|
+
if (decl.async) {
|
|
128
|
+
addError(decl, "async functions are not supported");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Validate parameters: must be (req, res) */
|
|
132
|
+
if (decl.params.length !== 2) {
|
|
133
|
+
addError(
|
|
134
|
+
decl,
|
|
135
|
+
`handler function "${name}" must have exactly 2 parameters (req, res)`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Walk the entire tree checking for banned features */
|
|
143
|
+
walk.full(ast, (node) => {
|
|
144
|
+
/* Check banned node types */
|
|
145
|
+
if (BANNED_NODES[node.type]) {
|
|
146
|
+
addError(node, BANNED_NODES[node.type]);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Check for unknown node types */
|
|
151
|
+
if (!ALLOWED_NODES.has(node.type)) {
|
|
152
|
+
addError(node, `unsupported syntax: ${node.type}`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Check for banned function calls */
|
|
157
|
+
if (node.type === "CallExpression") {
|
|
158
|
+
if (
|
|
159
|
+
node.callee.type === "Identifier" &&
|
|
160
|
+
BANNED_CALLS.has(node.callee.name)
|
|
161
|
+
) {
|
|
162
|
+
addError(node, `'${node.callee.name}()' is not allowed`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Validate variable declarations: only const and let */
|
|
167
|
+
if (node.type === "VariableDeclaration") {
|
|
168
|
+
if (node.kind === "var") {
|
|
169
|
+
addError(node, "'var' is not supported (use 'const' or 'let')");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (errors.length > 0) {
|
|
175
|
+
throw new Error(errors.join("\n"));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = { validate };
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@velox0/cerver",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Compile restricted JavaScript server logic into optimized native C binaries",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cerver": "bin/cerver.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node test/run.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"c",
|
|
14
|
+
"compiler",
|
|
15
|
+
"http",
|
|
16
|
+
"server",
|
|
17
|
+
"native",
|
|
18
|
+
"binary",
|
|
19
|
+
"framework"
|
|
20
|
+
],
|
|
21
|
+
"author": "Velox0",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"acorn": "^8.14.0",
|
|
25
|
+
"acorn-walk": "^8.3.4",
|
|
26
|
+
"commander": "^13.1.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/runtime/cerver.h
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* cerver.h — Core header for the cerver HTTP runtime.
|
|
3
|
+
*
|
|
4
|
+
* This is the only header needed by generated server code.
|
|
5
|
+
* It defines request/response types, server lifecycle, and
|
|
6
|
+
* the route dispatch interface.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#ifndef CERVER_H
|
|
10
|
+
#define CERVER_H
|
|
11
|
+
|
|
12
|
+
#include <stddef.h>
|
|
13
|
+
#include <stdint.h>
|
|
14
|
+
|
|
15
|
+
/* ------------------------------------------------------------------ */
|
|
16
|
+
/* Limits */
|
|
17
|
+
/* ------------------------------------------------------------------ */
|
|
18
|
+
|
|
19
|
+
#define CERVER_MAX_HEADERS 64
|
|
20
|
+
#define CERVER_MAX_PARAMS 16
|
|
21
|
+
#define CERVER_MAX_QUERY 32
|
|
22
|
+
#define CERVER_MAX_PATH 2048
|
|
23
|
+
#define CERVER_MAX_HEADER_VAL 4096
|
|
24
|
+
#define CERVER_READ_BUF 8192
|
|
25
|
+
#define CERVER_MAX_ROUTES 256
|
|
26
|
+
|
|
27
|
+
/* ------------------------------------------------------------------ */
|
|
28
|
+
/* Key-value pair (used for headers, query params, route params) */
|
|
29
|
+
/* ------------------------------------------------------------------ */
|
|
30
|
+
|
|
31
|
+
typedef struct {
|
|
32
|
+
const char *key;
|
|
33
|
+
const char *value;
|
|
34
|
+
} cerver_kv_t;
|
|
35
|
+
|
|
36
|
+
/* ------------------------------------------------------------------ */
|
|
37
|
+
/* Request */
|
|
38
|
+
/* ------------------------------------------------------------------ */
|
|
39
|
+
|
|
40
|
+
typedef struct {
|
|
41
|
+
/* HTTP method: "GET", "POST", etc. */
|
|
42
|
+
char method[16];
|
|
43
|
+
|
|
44
|
+
/* Decoded path (no query string) */
|
|
45
|
+
char path[CERVER_MAX_PATH];
|
|
46
|
+
|
|
47
|
+
/* Raw query string (after '?') */
|
|
48
|
+
char query_string[CERVER_MAX_PATH];
|
|
49
|
+
|
|
50
|
+
/* Parsed query parameters */
|
|
51
|
+
cerver_kv_t query[CERVER_MAX_QUERY];
|
|
52
|
+
int query_count;
|
|
53
|
+
|
|
54
|
+
/* Route parameters (from dynamic segments like :key) */
|
|
55
|
+
cerver_kv_t params[CERVER_MAX_PARAMS];
|
|
56
|
+
int params_count;
|
|
57
|
+
|
|
58
|
+
/* Request headers */
|
|
59
|
+
cerver_kv_t headers[CERVER_MAX_HEADERS];
|
|
60
|
+
int header_count;
|
|
61
|
+
|
|
62
|
+
/* Request body (for POST) */
|
|
63
|
+
const char *body;
|
|
64
|
+
size_t body_len;
|
|
65
|
+
|
|
66
|
+
/* Internal: raw buffer ownership */
|
|
67
|
+
char *_raw_buf;
|
|
68
|
+
size_t _raw_len;
|
|
69
|
+
} cerver_request_t;
|
|
70
|
+
|
|
71
|
+
/* ------------------------------------------------------------------ */
|
|
72
|
+
/* Response */
|
|
73
|
+
/* ------------------------------------------------------------------ */
|
|
74
|
+
|
|
75
|
+
typedef struct {
|
|
76
|
+
int status;
|
|
77
|
+
const char *content_type;
|
|
78
|
+
|
|
79
|
+
/* Response body — can be heap-allocated or static */
|
|
80
|
+
const char *body;
|
|
81
|
+
size_t body_len;
|
|
82
|
+
|
|
83
|
+
/* Extra headers */
|
|
84
|
+
cerver_kv_t headers[CERVER_MAX_HEADERS];
|
|
85
|
+
int header_count;
|
|
86
|
+
|
|
87
|
+
/* Internal flag: was body malloc'd? */
|
|
88
|
+
int _body_owned;
|
|
89
|
+
} cerver_response_t;
|
|
90
|
+
|
|
91
|
+
/* Response helpers — called by generated handler code */
|
|
92
|
+
void cerver_res_text(cerver_response_t *res, int status, const char *text);
|
|
93
|
+
void cerver_res_json(cerver_response_t *res, int status, const char *json);
|
|
94
|
+
void cerver_res_html(cerver_response_t *res, int status, const char *html);
|
|
95
|
+
void cerver_res_file(cerver_response_t *res, int status, const char *mime,
|
|
96
|
+
const unsigned char *data, size_t len);
|
|
97
|
+
void cerver_res_header(cerver_response_t *res, const char *key, const char *val);
|
|
98
|
+
|
|
99
|
+
/* ------------------------------------------------------------------ */
|
|
100
|
+
/* Request helpers */
|
|
101
|
+
/* ------------------------------------------------------------------ */
|
|
102
|
+
|
|
103
|
+
const char *cerver_req_param(const cerver_request_t *req, const char *key);
|
|
104
|
+
const char *cerver_req_query(const cerver_request_t *req, const char *key);
|
|
105
|
+
const char *cerver_req_header(const cerver_request_t *req, const char *key);
|
|
106
|
+
|
|
107
|
+
/* ------------------------------------------------------------------ */
|
|
108
|
+
/* Route definition */
|
|
109
|
+
/* ------------------------------------------------------------------ */
|
|
110
|
+
|
|
111
|
+
typedef void (*cerver_handler_fn)(cerver_request_t *req, cerver_response_t *res);
|
|
112
|
+
|
|
113
|
+
typedef struct {
|
|
114
|
+
const char *method; /* "GET", "POST" */
|
|
115
|
+
const char *pattern; /* "/", "/art/:key", "/api/projects" */
|
|
116
|
+
cerver_handler_fn handler;
|
|
117
|
+
} cerver_route_t;
|
|
118
|
+
|
|
119
|
+
/* ------------------------------------------------------------------ */
|
|
120
|
+
/* Embedded asset (for --embed builds) */
|
|
121
|
+
/* ------------------------------------------------------------------ */
|
|
122
|
+
|
|
123
|
+
typedef struct {
|
|
124
|
+
const char *path; /* e.g. "/index.html" */
|
|
125
|
+
const char *mime_type; /* e.g. "text/html" */
|
|
126
|
+
const unsigned char *data;
|
|
127
|
+
size_t data_len;
|
|
128
|
+
} cerver_asset_t;
|
|
129
|
+
|
|
130
|
+
/* ------------------------------------------------------------------ */
|
|
131
|
+
/* Server */
|
|
132
|
+
/* ------------------------------------------------------------------ */
|
|
133
|
+
|
|
134
|
+
typedef struct {
|
|
135
|
+
int port;
|
|
136
|
+
int sock_fd;
|
|
137
|
+
cerver_route_t *routes;
|
|
138
|
+
int route_count;
|
|
139
|
+
cerver_asset_t *assets;
|
|
140
|
+
int asset_count;
|
|
141
|
+
const char *public_dir; /* NULL if embedded mode */
|
|
142
|
+
volatile int running;
|
|
143
|
+
} cerver_server_t;
|
|
144
|
+
|
|
145
|
+
/* Server lifecycle */
|
|
146
|
+
int cerver_init(cerver_server_t *srv, int port);
|
|
147
|
+
int cerver_add_routes(cerver_server_t *srv, cerver_route_t *routes, int count);
|
|
148
|
+
int cerver_set_assets(cerver_server_t *srv, cerver_asset_t *assets, int count);
|
|
149
|
+
void cerver_set_public_dir(cerver_server_t *srv, const char *dir);
|
|
150
|
+
int cerver_listen(cerver_server_t *srv);
|
|
151
|
+
void cerver_shutdown(cerver_server_t *srv);
|
|
152
|
+
|
|
153
|
+
/* ------------------------------------------------------------------ */
|
|
154
|
+
/* HTTP parser (internal) */
|
|
155
|
+
/* ------------------------------------------------------------------ */
|
|
156
|
+
|
|
157
|
+
int cerver_parse_request(const char *raw, size_t len, cerver_request_t *req);
|
|
158
|
+
|
|
159
|
+
/* ------------------------------------------------------------------ */
|
|
160
|
+
/* HTTP writer (internal) */
|
|
161
|
+
/* ------------------------------------------------------------------ */
|
|
162
|
+
|
|
163
|
+
int cerver_write_response(int fd, const cerver_response_t *res);
|
|
164
|
+
|
|
165
|
+
/* ------------------------------------------------------------------ */
|
|
166
|
+
/* Router (internal) */
|
|
167
|
+
/* ------------------------------------------------------------------ */
|
|
168
|
+
|
|
169
|
+
int cerver_route_match(const cerver_route_t *route, cerver_request_t *req);
|
|
170
|
+
cerver_handler_fn cerver_dispatch(cerver_server_t *srv, cerver_request_t *req);
|
|
171
|
+
|
|
172
|
+
/* ------------------------------------------------------------------ */
|
|
173
|
+
/* MIME (internal) */
|
|
174
|
+
/* ------------------------------------------------------------------ */
|
|
175
|
+
|
|
176
|
+
const char *cerver_mime_from_path(const char *path);
|
|
177
|
+
|
|
178
|
+
/* ------------------------------------------------------------------ */
|
|
179
|
+
/* Static file serving (internal) */
|
|
180
|
+
/* ------------------------------------------------------------------ */
|
|
181
|
+
|
|
182
|
+
int cerver_serve_static(cerver_server_t *srv, cerver_request_t *req,
|
|
183
|
+
cerver_response_t *res);
|
|
184
|
+
|
|
185
|
+
#endif /* CERVER_H */
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* http_parser.c — Minimal HTTP/1.1 request parser.
|
|
3
|
+
*
|
|
4
|
+
* Parses method, path, query string, and headers from a raw HTTP request.
|
|
5
|
+
* No external dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
#include "cerver.h"
|
|
9
|
+
|
|
10
|
+
#include <stdio.h>
|
|
11
|
+
#include <stdlib.h>
|
|
12
|
+
#include <string.h>
|
|
13
|
+
#include <ctype.h>
|
|
14
|
+
|
|
15
|
+
/* ------------------------------------------------------------------ */
|
|
16
|
+
/* URL-decode a string in-place */
|
|
17
|
+
/* ------------------------------------------------------------------ */
|
|
18
|
+
|
|
19
|
+
static int hex_val(char c) {
|
|
20
|
+
if (c >= '0' && c <= '9') return c - '0';
|
|
21
|
+
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
22
|
+
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
23
|
+
return -1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static void url_decode(char *str) {
|
|
27
|
+
char *src = str;
|
|
28
|
+
char *dst = str;
|
|
29
|
+
|
|
30
|
+
while (*src) {
|
|
31
|
+
if (*src == '%' && src[1] && src[2]) {
|
|
32
|
+
int hi = hex_val(src[1]);
|
|
33
|
+
int lo = hex_val(src[2]);
|
|
34
|
+
if (hi >= 0 && lo >= 0) {
|
|
35
|
+
*dst++ = (char)((hi << 4) | lo);
|
|
36
|
+
src += 3;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (*src == '+') {
|
|
41
|
+
*dst++ = ' ';
|
|
42
|
+
src++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
*dst++ = *src++;
|
|
46
|
+
}
|
|
47
|
+
*dst = '\0';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ------------------------------------------------------------------ */
|
|
51
|
+
/* Parse query string: "a=1&b=2" → key-value pairs */
|
|
52
|
+
/* ------------------------------------------------------------------ */
|
|
53
|
+
|
|
54
|
+
static void parse_query_string(char *qs, cerver_request_t *req) {
|
|
55
|
+
if (!qs || !*qs) return;
|
|
56
|
+
|
|
57
|
+
char *saveptr = NULL;
|
|
58
|
+
char *pair = strtok_r(qs, "&", &saveptr);
|
|
59
|
+
|
|
60
|
+
while (pair && req->query_count < CERVER_MAX_QUERY) {
|
|
61
|
+
char *eq = strchr(pair, '=');
|
|
62
|
+
if (eq) {
|
|
63
|
+
*eq = '\0';
|
|
64
|
+
req->query[req->query_count].key = pair;
|
|
65
|
+
req->query[req->query_count].value = eq + 1;
|
|
66
|
+
url_decode((char *)req->query[req->query_count].key);
|
|
67
|
+
url_decode((char *)req->query[req->query_count].value);
|
|
68
|
+
} else {
|
|
69
|
+
req->query[req->query_count].key = pair;
|
|
70
|
+
req->query[req->query_count].value = "";
|
|
71
|
+
}
|
|
72
|
+
req->query_count++;
|
|
73
|
+
pair = strtok_r(NULL, "&", &saveptr);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ------------------------------------------------------------------ */
|
|
78
|
+
/* Parse the HTTP request */
|
|
79
|
+
/* ------------------------------------------------------------------ */
|
|
80
|
+
|
|
81
|
+
int cerver_parse_request(const char *raw, size_t len, cerver_request_t *req) {
|
|
82
|
+
if (!raw || len == 0) return -1;
|
|
83
|
+
|
|
84
|
+
/* We need a mutable copy because we'll be inserting NUL terminators */
|
|
85
|
+
char *buf = malloc(len + 1);
|
|
86
|
+
if (!buf) return -1;
|
|
87
|
+
memcpy(buf, raw, len);
|
|
88
|
+
buf[len] = '\0';
|
|
89
|
+
|
|
90
|
+
req->_raw_buf = buf;
|
|
91
|
+
req->_raw_len = len;
|
|
92
|
+
|
|
93
|
+
/* ---- Request line: METHOD PATH HTTP/1.x ---- */
|
|
94
|
+
char *line_end = strstr(buf, "\r\n");
|
|
95
|
+
if (!line_end) {
|
|
96
|
+
free(buf);
|
|
97
|
+
return -1;
|
|
98
|
+
}
|
|
99
|
+
*line_end = '\0';
|
|
100
|
+
|
|
101
|
+
/* Method */
|
|
102
|
+
char *sp1 = strchr(buf, ' ');
|
|
103
|
+
if (!sp1) { free(buf); return -1; }
|
|
104
|
+
*sp1 = '\0';
|
|
105
|
+
|
|
106
|
+
size_t method_len = (size_t)(sp1 - buf);
|
|
107
|
+
if (method_len >= sizeof(req->method)) method_len = sizeof(req->method) - 1;
|
|
108
|
+
memcpy(req->method, buf, method_len);
|
|
109
|
+
req->method[method_len] = '\0';
|
|
110
|
+
|
|
111
|
+
/* Path (and maybe query string) */
|
|
112
|
+
char *path_start = sp1 + 1;
|
|
113
|
+
char *sp2 = strchr(path_start, ' ');
|
|
114
|
+
if (sp2) *sp2 = '\0';
|
|
115
|
+
|
|
116
|
+
/* Split path and query string */
|
|
117
|
+
char *qmark = strchr(path_start, '?');
|
|
118
|
+
if (qmark) {
|
|
119
|
+
*qmark = '\0';
|
|
120
|
+
strncpy(req->query_string, qmark + 1, sizeof(req->query_string) - 1);
|
|
121
|
+
req->query_string[sizeof(req->query_string) - 1] = '\0';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Decode and store path */
|
|
125
|
+
url_decode(path_start);
|
|
126
|
+
strncpy(req->path, path_start, sizeof(req->path) - 1);
|
|
127
|
+
req->path[sizeof(req->path) - 1] = '\0';
|
|
128
|
+
|
|
129
|
+
/* Normalize trailing slash: "/foo/" → "/foo" (but keep "/" as is) */
|
|
130
|
+
size_t plen = strlen(req->path);
|
|
131
|
+
if (plen > 1 && req->path[plen - 1] == '/') {
|
|
132
|
+
req->path[plen - 1] = '\0';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Parse query string */
|
|
136
|
+
if (req->query_string[0]) {
|
|
137
|
+
/* We need a mutable copy for strtok */
|
|
138
|
+
char *qs_copy = strdup(req->query_string);
|
|
139
|
+
if (qs_copy) {
|
|
140
|
+
parse_query_string(qs_copy, req);
|
|
141
|
+
/* Note: keys/values point into qs_copy which we leak intentionally
|
|
142
|
+
since the request's lifetime is short (one connection). */
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ---- Headers ---- */
|
|
147
|
+
char *hdr_start = line_end + 2; /* skip \r\n */
|
|
148
|
+
size_t content_length = 0;
|
|
149
|
+
|
|
150
|
+
while (hdr_start < buf + len) {
|
|
151
|
+
char *hdr_end = strstr(hdr_start, "\r\n");
|
|
152
|
+
if (!hdr_end) break;
|
|
153
|
+
|
|
154
|
+
/* Empty line = end of headers */
|
|
155
|
+
if (hdr_end == hdr_start) {
|
|
156
|
+
hdr_start = hdr_end + 2;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
*hdr_end = '\0';
|
|
161
|
+
|
|
162
|
+
if (req->header_count < CERVER_MAX_HEADERS) {
|
|
163
|
+
char *colon = strchr(hdr_start, ':');
|
|
164
|
+
if (colon) {
|
|
165
|
+
*colon = '\0';
|
|
166
|
+
char *val = colon + 1;
|
|
167
|
+
while (*val == ' ') val++;
|
|
168
|
+
|
|
169
|
+
req->headers[req->header_count].key = hdr_start;
|
|
170
|
+
req->headers[req->header_count].value = val;
|
|
171
|
+
req->header_count++;
|
|
172
|
+
|
|
173
|
+
/* Track content-length */
|
|
174
|
+
if (strcasecmp(hdr_start, "Content-Length") == 0) {
|
|
175
|
+
content_length = (size_t)atol(val);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
hdr_start = hdr_end + 2;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ---- Body (for POST etc.) ---- */
|
|
184
|
+
if (content_length > 0 && hdr_start < buf + len) {
|
|
185
|
+
req->body = hdr_start;
|
|
186
|
+
req->body_len = content_length;
|
|
187
|
+
/* Ensure we don't read past the buffer */
|
|
188
|
+
size_t remaining = (size_t)(buf + len - hdr_start);
|
|
189
|
+
if (req->body_len > remaining) {
|
|
190
|
+
req->body_len = remaining;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return 0;
|
|
195
|
+
}
|