utility functions for implementing noja functions in c; built-ins for socket management; http server draft in noja
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
cat = string.cat;
|
||||
|
||||
fun Node(T) {
|
||||
K = { item: T, next: none };
|
||||
K.next = ?K;
|
||||
return K;
|
||||
}
|
||||
|
||||
fun makeNode(item)
|
||||
return { item: item, next: none };
|
||||
|
||||
fun Stack(T)
|
||||
return { size: int, head: ?Node(T) };
|
||||
|
||||
fun StackIterator(T)
|
||||
return { stack: Stack(T), index: ?int, cursor: ?Node(T), next: Callable };
|
||||
|
||||
fun makeStackIterator(stack: Stack(any))
|
||||
return {
|
||||
stack: stack,
|
||||
index: none,
|
||||
cursor: stack.head,
|
||||
|
||||
fun toString(iter: StackIterator(stack.type))
|
||||
return cat("StackIterator(", toString(stack), ")");
|
||||
|
||||
fun next(iter: StackIterator(stack.type)) {
|
||||
if iter.index == none: {
|
||||
iter.index = 0;
|
||||
iter.cursor = iter.stack.head;
|
||||
} else {
|
||||
if iter.cursor != none: {
|
||||
iter.cursor = iter.cursor.next;
|
||||
iter.index = iter.index+1;
|
||||
}
|
||||
}
|
||||
if iter.cursor == none:
|
||||
return none;
|
||||
return iter.cursor.item, iter.index;
|
||||
}
|
||||
|
||||
fun haveNext(iter: StackIterator(stack.type)) {
|
||||
return none != iter.cursor
|
||||
and none != iter.cursor.next;
|
||||
}
|
||||
};
|
||||
|
||||
fun makeStack(T)
|
||||
return {
|
||||
type: T,
|
||||
size: 0,
|
||||
head: none,
|
||||
|
||||
fun push(stack: Stack(T), item: T) {
|
||||
|
||||
if item == none:
|
||||
error("Can't insert \"none\" into a stack");
|
||||
|
||||
node = makeNode(item);
|
||||
node.next = stack.head;
|
||||
stack.head = node;
|
||||
stack.size = stack.size+1;
|
||||
}
|
||||
|
||||
fun pop(stack: Stack(T)) {
|
||||
node = stack.head;
|
||||
if node != none: {
|
||||
stack.head = node.next;
|
||||
stack.size = stack.size-1;
|
||||
}
|
||||
return node.item;
|
||||
}
|
||||
|
||||
fun isEmpty(stack: Stack(T))
|
||||
return stack.head == none;
|
||||
|
||||
fun toString(stack: Stack(T)) {
|
||||
iter = stack->iter();
|
||||
s = "[";
|
||||
while (item = iter->next()) != none: {
|
||||
s = cat(s, toString(item));
|
||||
if iter->haveNext():
|
||||
s = cat(s, ", ");
|
||||
}
|
||||
s = cat(s, "]");
|
||||
return s;
|
||||
}
|
||||
|
||||
fun print(stack: Stack(T))
|
||||
print(stack->toString());
|
||||
|
||||
fun iter(stack: Stack(T))
|
||||
return makeStackIterator(stack);
|
||||
};
|
||||
|
||||
return {
|
||||
Stack: Stack,
|
||||
makeStack: makeStack,
|
||||
StackIterator: StackIterator,
|
||||
makeStackIterator: makeStackIterator
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
stack, error = import("stack.noja");
|
||||
print(error, "\n");
|
||||
Stack = stack.Stack;
|
||||
makeStack = stack.makeStack;
|
||||
StackIterator = stack.StackIterator;
|
||||
makeStackIterator = stack.makeStackIterator;
|
||||
|
||||
s1 = makeStack(int);
|
||||
s1->push(1);
|
||||
s1->push(2);
|
||||
s1->push(3);
|
||||
print(toString(s1), "\n");
|
||||
|
||||
iter = makeIterator(s1);
|
||||
print(toString(iter), "\n");
|
||||
while (val, key = iter->next()) != none:
|
||||
print(key, " => ", val, "\n");
|
||||
+12
-3
@@ -260,7 +260,7 @@ fun parseVersion(scan: Scanner) {
|
||||
char = scan->consume(5);
|
||||
major = ord(char) - ord("0");
|
||||
|
||||
if scan->hint(1) != "." and isDigit(scan->hint(2)): {
|
||||
if scan->hint(1) == "." and isDigit(scan->hint(2)): {
|
||||
char = scan->consume(2);
|
||||
minor = ord(char) - ord("0");
|
||||
} else
|
||||
@@ -316,9 +316,10 @@ fun parse(src: String) {
|
||||
if error != none:
|
||||
return none, error;
|
||||
|
||||
if scan->hint(0) != "\r" or scan->hint(1) != "\n":
|
||||
if scan->hint(0) != "\r" or scan->hint(1) != "\n": {
|
||||
# Request with no headers or body
|
||||
return newRequest(method, url, version);
|
||||
}
|
||||
scan->consume(2); # Consume the "\r\n"
|
||||
|
||||
headers = {};
|
||||
@@ -344,10 +345,18 @@ fun parse(src: String) {
|
||||
} while char != "\r" and char != none;
|
||||
|
||||
headers[name] = body;
|
||||
|
||||
if char == "\r": {
|
||||
if scan->hint(1) != "\n":
|
||||
return none, "Missing \\n after \\r in header body";
|
||||
scan->consume(2);
|
||||
}
|
||||
}
|
||||
|
||||
if char != none:
|
||||
if char != none: {
|
||||
assert(char == "\r" and scan->hint(1) == "\n");
|
||||
scan->consume(2); # Consume the final "\r\n"
|
||||
}
|
||||
|
||||
return newRequest(method, url, version, headers);
|
||||
}
|
||||
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
server = import("server.noja");
|
||||
request = import("request.noja");
|
||||
response = import("response.noja");
|
||||
Request = request.Request;
|
||||
|
||||
fun loadFile(path: String) {
|
||||
|
||||
stream, error = files.openFile(path, files.READ);
|
||||
if error != none:
|
||||
return none, error;
|
||||
|
||||
text = "";
|
||||
size = 512;
|
||||
do {
|
||||
size = 2 * size;
|
||||
data = buffer.new(size);
|
||||
|
||||
num_bytes, error = files.read(stream, data);
|
||||
if error != none:
|
||||
return none, error;
|
||||
|
||||
text = string.cat(text, buffer.toString(data));
|
||||
|
||||
} while num_bytes == size;
|
||||
|
||||
files.close(stream);
|
||||
return text;
|
||||
}
|
||||
|
||||
fun generateHome() {
|
||||
title = "Homepage";
|
||||
content = "Hello, world!";
|
||||
return string.cat(
|
||||
"<html>",
|
||||
"<head>",
|
||||
"<title>", title, "</title>",
|
||||
"</head>",
|
||||
"<body>",
|
||||
"<a>", content, "</a>",
|
||||
"</body>",
|
||||
"</html>"
|
||||
);
|
||||
}
|
||||
|
||||
fun generateLogout() {
|
||||
}
|
||||
|
||||
table = {
|
||||
'/': 'login.html',
|
||||
'/home': generateHome,
|
||||
'/login': 'login.html',
|
||||
'/signup': 'signup.html',
|
||||
'/logout': generateLogout
|
||||
};
|
||||
|
||||
default = 'notfound.html';
|
||||
debug = true;
|
||||
|
||||
fun callback(req: Request) {
|
||||
|
||||
path = req.url.path;
|
||||
|
||||
if path == "/" or path == "/login": {
|
||||
|
||||
if req.method == "POST": {
|
||||
|
||||
|
||||
|
||||
} else if req.method == "GET"; {
|
||||
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
# Generate response body
|
||||
{
|
||||
entry = table[req.url.path];
|
||||
if entry == none: {
|
||||
entry = default;
|
||||
not_found = true;
|
||||
} else
|
||||
not_found = false;
|
||||
|
||||
if isCallable(entry):
|
||||
data, error = entry(req);
|
||||
else
|
||||
data, error = loadFile(entry);
|
||||
}
|
||||
|
||||
# Generate response object
|
||||
{
|
||||
if error == none:
|
||||
res = response.new(200, data);
|
||||
else if not_found:
|
||||
res = response.new(404, data);
|
||||
else if debug:
|
||||
res = response.new(500, error);
|
||||
else
|
||||
res = response.new(500);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
s, err = server.new(callback, "127.0.0.1", 8080);
|
||||
if s == none: error(err);
|
||||
s->loop(); # This doesn't return
|
||||
s->close();
|
||||
@@ -0,0 +1,12 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Login page</title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="/login" method="POST">
|
||||
<input type="text" name="user" placeholder="[Username]" />
|
||||
<input type="password" name="pass" placeholder="[Password]" />
|
||||
<input type="submit" value="Enter" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,359 @@
|
||||
scanner = import("scanner.noja");
|
||||
|
||||
newScanner = scanner.newScanner;
|
||||
Scanner = scanner.Scanner;
|
||||
isDigit = scanner.isDigit;
|
||||
ord = string.ord;
|
||||
cat = string.cat;
|
||||
|
||||
fun isAlpha(char: String)
|
||||
return (ord(char) >= ord('a') and ord(char) <= ord('z'))
|
||||
or (ord(char) >= ord('A') and ord(char) <= ord('Z'));
|
||||
|
||||
fun isUpper(char: String)
|
||||
return ord(char) >= ord('A')
|
||||
and ord(char) <= ord('Z');
|
||||
|
||||
fun isUnreserved(char: String)
|
||||
return isAlpha(char)
|
||||
or isDigit(char)
|
||||
or char == "-"
|
||||
or char == "."
|
||||
or char == "_"
|
||||
or char == "~";
|
||||
|
||||
fun isSubdelim(char: String)
|
||||
return char == "!" or char == "$"
|
||||
or char == "&" or char == "'"
|
||||
or char == "(" or char == ")"
|
||||
or char == "*" or char == "+"
|
||||
or char == "," or char == ";"
|
||||
or char == "=";
|
||||
|
||||
fun isPChar(char: String)
|
||||
return isUnreserved(char)
|
||||
or isSubdelim(char)
|
||||
or char == ":"
|
||||
or char == "@";
|
||||
|
||||
fun parseMethod(scan: Scanner) {
|
||||
|
||||
char = scan->current();
|
||||
if char == none or not isUpper(char):
|
||||
return none, "Missing method";
|
||||
|
||||
method = "";
|
||||
do {
|
||||
method = cat(method, char);
|
||||
char = scan->consume();
|
||||
} while char != none and isUpper(char);
|
||||
|
||||
return method, none;
|
||||
}
|
||||
|
||||
fun parsePort(scan: Scanner) {
|
||||
|
||||
port = none;
|
||||
char = scan->current();
|
||||
if char == ":" and isDigit(scan->hint(1)): {
|
||||
|
||||
char = scan->consume(); # Skip the ":"
|
||||
|
||||
port = 0;
|
||||
do {
|
||||
digit = ord(char) - ord("0");
|
||||
port = port * 10 + digit;
|
||||
char = scan->consume();
|
||||
} while char != none and isDigit(char);
|
||||
}
|
||||
return port;
|
||||
}
|
||||
|
||||
fun isHostname(char: String)
|
||||
return isUnreserved(char)
|
||||
or isSubdelim(char);
|
||||
|
||||
fun parseHost(scan: Scanner) {
|
||||
|
||||
char = scan->current();
|
||||
if char == none:
|
||||
return none, "Missing host";
|
||||
|
||||
if not isHostname(char):
|
||||
return none, "Invalid character in place of host";
|
||||
|
||||
name = "";
|
||||
do {
|
||||
name = cat(name, char);
|
||||
char = scan->consume();
|
||||
} while char != none and isHostname(char);
|
||||
|
||||
port = parsePort(scan);
|
||||
return {name: name, port: port};
|
||||
}
|
||||
|
||||
fun parsePath(scan: Scanner) {
|
||||
|
||||
path = "";
|
||||
char = scan->current();
|
||||
if char != none and char == "/": {
|
||||
path = cat(path, char);
|
||||
char = scan->consume();
|
||||
} else {
|
||||
if char == none or not isPChar(char):
|
||||
return none, "Invalid character in place of path";
|
||||
}
|
||||
|
||||
while char != none and isPChar(char): {
|
||||
do {
|
||||
path = cat(path, char);
|
||||
char = scan->consume();
|
||||
} while char != none and isPChar(char);
|
||||
if char == none or char != "/":
|
||||
break;
|
||||
path = cat(path, char);
|
||||
char = scan->consume();
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
fun isQuery(char: ?String) {
|
||||
if char == none:
|
||||
return false;
|
||||
return isPChar(char)
|
||||
or char == "/"
|
||||
or char == "?";
|
||||
}
|
||||
|
||||
fun parseQuery(scan: Scanner) {
|
||||
|
||||
char = scan->current();
|
||||
if char != none and char == "?": {
|
||||
query = "";
|
||||
while isQuery(char = scan->consume()):
|
||||
query = cat(query, char);
|
||||
} else
|
||||
query = none;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
fun isFragment(char: ?String) {
|
||||
if char == none:
|
||||
return false;
|
||||
return isPChar(char) or char == "/";
|
||||
}
|
||||
|
||||
fun parseFragment(scan: Scanner) {
|
||||
|
||||
char = scan->current();
|
||||
if char != none and char == "#": {
|
||||
fragment = "";
|
||||
while isFragment(char = scan->consume()):
|
||||
fragment = cat(fragment, char);
|
||||
} else
|
||||
fragment = none;
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
fun isSchemaFirst(char: ?String) {
|
||||
return char != none and isAlpha(char);
|
||||
}
|
||||
|
||||
fun isSchema(char: ?String)
|
||||
return char != none
|
||||
and (isAlpha(char)
|
||||
or isDigit(char)
|
||||
or char == "+"
|
||||
or char == "-"
|
||||
or char == ".");
|
||||
|
||||
fun parseSchema(scan: Scanner) {
|
||||
|
||||
char = scan->current();
|
||||
schema = none;
|
||||
if isSchemaFirst(char): {
|
||||
start = scan.i;
|
||||
schema = "";
|
||||
do {
|
||||
schema = cat(schema, char);
|
||||
char = scan->consume();
|
||||
} while isSchema(char);
|
||||
|
||||
if char == ":":
|
||||
scan->consume();
|
||||
else {
|
||||
schema = none;
|
||||
scan.i = start;
|
||||
}
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
fun followsAuthority(scan: Scanner)
|
||||
return scan->current() == "/" and scan->hint(1) == "/";
|
||||
|
||||
fun parseURL(scan: Scanner) {
|
||||
|
||||
schema = parseSchema(scan);
|
||||
|
||||
if followsAuthority(scan): {
|
||||
scan->consume(2);
|
||||
|
||||
host, error = parseHost(scan);
|
||||
if error != none:
|
||||
return none, error;
|
||||
|
||||
path = none;
|
||||
if scan->current() == "/":
|
||||
path = parsePath(scan);
|
||||
|
||||
} else {
|
||||
|
||||
host = none;
|
||||
|
||||
char = scan->current();
|
||||
if char == none or char == "?" or char == "#":
|
||||
return none, "Missing path";
|
||||
|
||||
path, error = parsePath(scan);
|
||||
if error != none:
|
||||
return none, error;
|
||||
}
|
||||
|
||||
query = parseQuery(scan);
|
||||
fragment = parseFragment(scan);
|
||||
|
||||
return {
|
||||
host : host,
|
||||
path : path,
|
||||
query : query,
|
||||
schema : schema,
|
||||
fragment: fragment
|
||||
};
|
||||
}
|
||||
|
||||
fun parseVersion(scan: Scanner) {
|
||||
|
||||
if scan->hint(0) != "H" or scan->hint(1) != "T"
|
||||
or scan->hint(2) != "T" or scan->hint(3) != "P"
|
||||
or scan->hint(4) != "/" or not isDigit(scan->hint(5)):
|
||||
return none, "Invalid version token";
|
||||
|
||||
char = scan->consume(5);
|
||||
major = ord(char) - ord("0");
|
||||
|
||||
if scan->hint(1) == "." and isDigit(scan->hint(2)): {
|
||||
char = scan->consume(2);
|
||||
minor = ord(char) - ord("0");
|
||||
} else
|
||||
minor = 0;
|
||||
scan->consume();
|
||||
|
||||
return {major: major, minor: minor};
|
||||
}
|
||||
|
||||
fun parseHead(src: String) {
|
||||
|
||||
scan = newScanner(src);
|
||||
|
||||
method, error = parseMethod(scan);
|
||||
if error != none:
|
||||
return none, error;
|
||||
|
||||
char = scan->current();
|
||||
if char == none or char != " ":
|
||||
return none, "Missing space after method";
|
||||
scan->consume();
|
||||
|
||||
url, error = parseURL(scan);
|
||||
if error != none:
|
||||
return none, error;
|
||||
|
||||
char = scan->current();
|
||||
if char != " ":
|
||||
return none, "Missing space after URL";
|
||||
scan->consume();
|
||||
|
||||
version, error = parseVersion(scan);
|
||||
if error != none:
|
||||
return none, error;
|
||||
|
||||
if scan->hint(0) != "\r"
|
||||
or scan->hint(1) != "\n": {
|
||||
|
||||
# Request with no headers or body
|
||||
return {
|
||||
method: method,
|
||||
url: url,
|
||||
version:
|
||||
version,
|
||||
headers: {},
|
||||
body: none
|
||||
}, none, scan.i;
|
||||
}
|
||||
|
||||
scan->consume(2); # Consume the "\r\n"
|
||||
|
||||
headers = {};
|
||||
while (char = scan->hint(0)) != none and (char != "\r" or scan->hint(1) != "\n"): {
|
||||
|
||||
# Header name until the next ":"
|
||||
name = "";
|
||||
do {
|
||||
name = cat(name, char);
|
||||
char = scan->consume();
|
||||
} while char != ":" and char != none;
|
||||
|
||||
if char == none:
|
||||
return none, "Invalid header (missing \":\" after name)";
|
||||
scan->consume();
|
||||
char = scan->consumeSpaces();
|
||||
|
||||
# Header body until \r
|
||||
body = "";
|
||||
do {
|
||||
body = cat(body, char);
|
||||
char = scan->consume();
|
||||
} while char != "\r" and char != none;
|
||||
|
||||
headers[name] = body;
|
||||
|
||||
if char == "\r": {
|
||||
if scan->hint(1) != "\n":
|
||||
return none, "Missing \\n after \\r in header body";
|
||||
scan->consume(2);
|
||||
}
|
||||
}
|
||||
|
||||
if char != none: {
|
||||
assert(char == "\r" and scan->hint(1) == "\n");
|
||||
scan->consume(2); # Consume the final "\r\n"
|
||||
}
|
||||
|
||||
return {
|
||||
method: method,
|
||||
url: url,
|
||||
version: version,
|
||||
headers: headers,
|
||||
body: none
|
||||
}, none, scan.i;
|
||||
}
|
||||
|
||||
fun test() {
|
||||
sample = cat(
|
||||
"GET /search?client=firefox-b-d&q=http+request+dataset HTTP/2\r\n",
|
||||
"Host: www.google.com\r\n",
|
||||
"user-agent: curl/7.84.0\r\n",
|
||||
"accept: */*\r\n",
|
||||
"\r\n");
|
||||
|
||||
request, error = parseHead(sample);
|
||||
print("request=", request, "\n");
|
||||
print("error=", error, "\n");
|
||||
}
|
||||
|
||||
return {parseHead: parseHead, test: test};
|
||||
@@ -0,0 +1,87 @@
|
||||
parseHead = import("parse.noja").parseHead;
|
||||
|
||||
Version = {
|
||||
major: int,
|
||||
minor: int
|
||||
};
|
||||
|
||||
Host = {
|
||||
name: String,
|
||||
port: ?int
|
||||
};
|
||||
|
||||
URL = {
|
||||
schema: ?String,
|
||||
host : ?Host,
|
||||
path : String,
|
||||
query : ?String,
|
||||
fragment: ?String
|
||||
};
|
||||
|
||||
Request = {
|
||||
method : String,
|
||||
url : URL,
|
||||
version: Version,
|
||||
body : None | Buffer | [Buffer, Buffer]
|
||||
};
|
||||
|
||||
fun fromSocket(fd: int, max_head: int = 1024) {
|
||||
|
||||
{
|
||||
# Create a buffer to hold the received bytes
|
||||
head_buffer = buffer.new(max_head);
|
||||
|
||||
# Receive bytes up to the buffer's size
|
||||
received_bytes, error = net.recv(fd, head_buffer);
|
||||
if error != none:
|
||||
return none, error, true;
|
||||
|
||||
# Downsize the buffer to the received amount
|
||||
head_buffer = buffer.sliceUp(head_buffer, 0, received_bytes);
|
||||
assert(count(head_buffer) == received_bytes);
|
||||
|
||||
# Convert the received data to a string
|
||||
text, error = buffer.toString(head_buffer);
|
||||
if error != none:
|
||||
return none, error, false; # Request isn't UTF-8
|
||||
}
|
||||
|
||||
{
|
||||
# Parse the request head's information
|
||||
request, error, parsed_bytes = parseHead(text);
|
||||
if error != none:
|
||||
return none, error, false;
|
||||
|
||||
# Split the received data into head and body.
|
||||
head_bytes = buffer.sliceUp(head_buffer, 0, parsed_bytes);
|
||||
body_bytes = buffer.sliceUp(head_buffer, parsed_bytes, count(head_buffer) - parsed_bytes);
|
||||
}
|
||||
|
||||
# Receive the remaining portion of the
|
||||
# request's body (if it wasn't already)
|
||||
{
|
||||
content_length = request.headers['Content-Length'];
|
||||
if content_length == none:
|
||||
content_length = 0;
|
||||
|
||||
# TODO: Limit the content length
|
||||
|
||||
waiting_bytes = content_length - count(body_bytes);
|
||||
assert(waiting_bytes >= 0);
|
||||
|
||||
if waiting_bytes > 0: {
|
||||
|
||||
second_half = buffer.new(waiting_bytes);
|
||||
|
||||
received_bytes, error = net.recv(fd, second_half);
|
||||
if error != none:
|
||||
return none, error, true; # Failed to receive the second half of the body
|
||||
assert(received_bytes == waiting_bytes);
|
||||
|
||||
body_bytes = [body_bytes, second_half];
|
||||
}
|
||||
}
|
||||
|
||||
request.body = body_bytes;
|
||||
return request;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
Response = {
|
||||
status: int,
|
||||
body: Buffer,
|
||||
headers: Map
|
||||
};
|
||||
|
||||
status_table = {
|
||||
100: "Continue",
|
||||
101: "Switching Protocols",
|
||||
102: "Processing",
|
||||
|
||||
200: "OK",
|
||||
201: "Created",
|
||||
202: "Accepted",
|
||||
203: "Non-Authoritative Information",
|
||||
204: "No Content",
|
||||
205: "Reset Content",
|
||||
206: "Partial Content",
|
||||
207: "Multi-Status",
|
||||
208: "Already Reported",
|
||||
|
||||
300: "Multiple Choices",
|
||||
301: "Moved Permanently",
|
||||
302: "Found",
|
||||
303: "See Other",
|
||||
304: "Not Modified",
|
||||
305: "Use Proxy",
|
||||
306: "Switch Proxy",
|
||||
307: "Temporary Redirect",
|
||||
308: "Permanent Redirect",
|
||||
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
402: "Payment Required",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
405: "Method Not Allowed",
|
||||
406: "Not Acceptable",
|
||||
407: "Proxy Authentication Required",
|
||||
408: "Request Timeout",
|
||||
409: "Conflict",
|
||||
410: "Gone",
|
||||
411: "Length Required",
|
||||
412: "Precondition Failed",
|
||||
413: "Request Entity Too Large",
|
||||
414: "Request-URI Too Long",
|
||||
415: "Unsupported Media Type",
|
||||
416: "Requested Range Not Satisfiable",
|
||||
417: "Expectation Failed",
|
||||
418: "I'm a teapot",
|
||||
420: "Enhance your calm",
|
||||
422: "Unprocessable Entity",
|
||||
426: "Upgrade Required",
|
||||
429: "Too many requests",
|
||||
431: "Request Header Fields Too Large",
|
||||
449: "Retry With",
|
||||
451: "Unavailable For Legal Reasons",
|
||||
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
504: "Gateway Timeout",
|
||||
505: "HTTP Version Not Supported",
|
||||
509: "Bandwidth Limit Exceeded"
|
||||
};
|
||||
|
||||
fun getStatusText(code: int) {
|
||||
text = status_table[code];
|
||||
if text == none:
|
||||
return "???";
|
||||
return text;
|
||||
}
|
||||
|
||||
fun new(status: int = 200,
|
||||
body: Buffer | String = "",
|
||||
headers: Map = {}) {
|
||||
|
||||
if type(body) == String:
|
||||
body = buffer.fromString(body);
|
||||
|
||||
# It's important to cast the body from a
|
||||
# string to a buffer before calculating
|
||||
# the Content-Length because the count of
|
||||
# a string doesn't necessarily match the
|
||||
# number of bytes!
|
||||
headers['Content-Length'] = count(body);
|
||||
|
||||
return {status: status, body: body, headers: headers};
|
||||
}
|
||||
|
||||
fun toBuffer(res: Response) {
|
||||
|
||||
cat = string.cat;
|
||||
text = cat("HTTP/1.0 ", toString(res.status), " ", getStatusText(res.status), "\r\n");
|
||||
|
||||
i = 0;
|
||||
header_names = keysof(res.headers);
|
||||
while i < count(header_names): {
|
||||
name = header_names[i];
|
||||
body = toString(res.headers[name]);
|
||||
text = cat(text, name, ": ", body, "\r\n");
|
||||
i = i+1;
|
||||
}
|
||||
text = cat(text, "\r\n", buffer.toString(res.body));
|
||||
return buffer.fromString(text);
|
||||
}
|
||||
|
||||
fun toSocket(fd: int, res: Response) {
|
||||
buf = toBuffer(res);
|
||||
_, error = net.send(fd, buf);
|
||||
return error;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
|
||||
Scanner = {
|
||||
src: String,
|
||||
i: int,
|
||||
hint: Callable,
|
||||
current: Callable,
|
||||
consume: Callable,
|
||||
consumeSpaces: Callable
|
||||
};
|
||||
|
||||
fun newScanner(src: String) {
|
||||
|
||||
fun isSpace(c: String)
|
||||
return c == ' '
|
||||
or c == '\t'
|
||||
or c == '\n';
|
||||
|
||||
scan = {
|
||||
src: src,
|
||||
i: 0,
|
||||
|
||||
fun hint(scan: Scanner, n: int = 1) {
|
||||
k = scan.i + n;
|
||||
if k < count(scan.src):
|
||||
return scan.src[k];
|
||||
return none;
|
||||
}
|
||||
|
||||
fun current(scan: Scanner)
|
||||
return scan->hint(0);
|
||||
|
||||
fun consume(scan: Scanner, n: int = 1) {
|
||||
assert(n > 0);
|
||||
limit = count(scan.src);
|
||||
scan.i = min(scan.i + n, limit);
|
||||
return scan->current();
|
||||
}
|
||||
|
||||
fun consumeSpaces(scan: Scanner) {
|
||||
char = scan->current();
|
||||
while isSpace(char):
|
||||
char = scan->consume();
|
||||
return char;
|
||||
}
|
||||
};
|
||||
assert(istypeof(Scanner, scan));
|
||||
return scan;
|
||||
}
|
||||
|
||||
fun isDigit(char: ?String) {
|
||||
if char == none:
|
||||
return false;
|
||||
ord = string.ord;
|
||||
return ord(char) >= ord('0')
|
||||
and ord(char) <= ord('9');
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
request = import("request.noja");
|
||||
response = import("response.noja");
|
||||
|
||||
Server = {
|
||||
fd: int,
|
||||
callback: Callable,
|
||||
loop : Callable,
|
||||
close: Callable
|
||||
};
|
||||
|
||||
fun handleClient(fd: int, server: Server) {
|
||||
|
||||
req, error, error_is_internal = request.fromSocket(fd);
|
||||
if error != none: {
|
||||
if error_is_internal: {
|
||||
if debug:
|
||||
res = response.new(500, error);
|
||||
else
|
||||
res = response.new(500);
|
||||
} else
|
||||
res = response.new(400, error);
|
||||
} else {
|
||||
if req.version.major != 1:
|
||||
res = response.new(505, "HTTP version not supported");
|
||||
else
|
||||
res = server.callback(req);
|
||||
}
|
||||
return response.toSocket(fd, res);
|
||||
}
|
||||
|
||||
fun fallback_callback(request: Request)
|
||||
return response.new(500, "No callback provided");
|
||||
|
||||
return {
|
||||
|
||||
Server: Server,
|
||||
|
||||
fun new(callback: Callable = fallback_callback,
|
||||
addr : String = "127.0.0.1",
|
||||
port : int = 8080,
|
||||
backlog : int = 32) {
|
||||
|
||||
fd, err = net.socket(net.AF_INET, net.SOCK_STREAM, 0);
|
||||
if err != none:
|
||||
return none, err;
|
||||
|
||||
if (err = net.bind(fd, net.AF_INET, port, addr)) != none: {
|
||||
net.close(fd);
|
||||
return none, err;
|
||||
}
|
||||
|
||||
if (err = net.listen(fd, backlog)) != none: {
|
||||
net.close(fd);
|
||||
return none, err;
|
||||
}
|
||||
|
||||
return {
|
||||
fd: fd,
|
||||
callback: callback,
|
||||
|
||||
fun loop(server: Server) {
|
||||
|
||||
while true: {
|
||||
fd, addr_or_err, port = net.accept(server.fd);
|
||||
if fd == none:
|
||||
error = addr_or_err;
|
||||
else {
|
||||
error = handleClient(fd, server);
|
||||
net.close(fd);
|
||||
}
|
||||
if error != none:
|
||||
print(error);
|
||||
}
|
||||
}
|
||||
|
||||
fun close(server: Server)
|
||||
net.close(server.fd);
|
||||
};
|
||||
}
|
||||
};
|
||||
+4
-4
@@ -9,11 +9,11 @@ Scanner = {
|
||||
};
|
||||
|
||||
fun newScanner(src: String) {
|
||||
|
||||
|
||||
fun isSpace(c: String)
|
||||
return c == ' '
|
||||
or c == '\t'
|
||||
or c == '\n';
|
||||
return c == ' '
|
||||
or c == '\t'
|
||||
or c == '\n';
|
||||
|
||||
scan = {
|
||||
src: src,
|
||||
|
||||
Reference in New Issue
Block a user