performance improvements + a good amount of changes

This commit is contained in:
Nicholas Orlowsky 2023-10-16 14:36:17 -04:00
parent 89a6d9a528
commit 0649a28a28
No known key found for this signature in database
GPG key ID: BE7DF0188A405E2B
24 changed files with 2040744 additions and 70 deletions

115
src/.clang-format Normal file
View file

@ -0,0 +1,115 @@
---
# BasedOnStyle: WebKit
AccessModifierOffset: -4
AlignAfterOpenBracket: DontAlign
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Right
AlignOperands: false
AlignTrailingComments: false
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: No
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: true
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: false
AfterUnion: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: All
BreakBeforeBraces: WebKit
BreakBeforeInheritanceComma: false
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeComma
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 0
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: false
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: false
ForEachMacros:
- foreach
- Q_FOREACH
- BOOST_FOREACH
IncludeCategories:
- Regex: '^"config\.h"'
Priority: -1
# The main header for a source file automatically gets category 0
- Regex: '^<.*SoftLink.h>'
Priority: 4
- Regex: '^".*SoftLink.h"'
Priority: 3
- Regex: '^<.*>'
Priority: 2
- Regex: '.*'
Priority: 1
IncludeIsMainRegex: '(Test)?$'
IndentCaseLabels: false
IndentWidth: 4
IndentWrappedFunctionNames: false
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: true
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Left
ReflowComments: true
SortIncludes: true
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: true
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Cpp11
TabWidth: 8
UseTab: Never
---
Language: ObjC
PointerAlignment: Right
...

38
src/.clang-tidy Normal file
View file

@ -0,0 +1,38 @@
---
Checks: 'clang-diagnostic-*,clang-analyzer-*,cppcoreguidelines-*,modernize-*,-modernize-use-trailing-return-type'
WarningsAsErrors: true
HeaderFilterRegex: ''
AnalyzeTemporaryDtors: false
FormatStyle: google
CheckOptions:
- key: cert-dcl16-c.NewSuffixes
value: 'L;LL;LU;LLU'
- key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField
value: '0'
- key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors
value: '1'
- key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic
value: '1'
- key: google-readability-braces-around-statements.ShortStatementLines
value: '1'
- key: google-readability-function-size.StatementThreshold
value: '800'
- key: google-readability-namespace-comments.ShortNamespaceLines
value: '10'
- key: google-readability-namespace-comments.SpacesBeforeComments
value: '2'
- key: modernize-loop-convert.MaxCopySize
value: '16'
- key: modernize-loop-convert.MinConfidence
value: reasonable
- key: modernize-loop-convert.NamingStyle
value: CamelCase
- key: modernize-pass-by-value.IncludeStyle
value: llvm
- key: modernize-replace-auto-ptr.IncludeStyle
value: llvm
- key: modernize-use-nullptr.NullMacros
value: 'NULL'
...

25
src/Makefile Normal file
View file

@ -0,0 +1,25 @@
.PHONY: format lint build build-release build-docker run debug
build:
g++ main.cpp -g -o ./anthracite
build-release:
g++ main.cpp -O3 -march=native -o ./anthracite
build-docker:
docker build . -t anthracite
run: build
./anthracite 8080
debug: build
gdb --args ./anthracite 8080
format:
clang-format *.cpp -i
lint:
clang-tidy *.cpp
lint-fix:
clang-tidy *.cpp -fix -fix-errors

7
src/backends/backend.cpp Normal file
View file

@ -0,0 +1,7 @@
#include "../http.cpp"
#include <memory>
class backend {
public:
virtual unique_ptr<http_response> handle_request(http_request& req) = 0;
};

View file

@ -0,0 +1,72 @@
#include "backend.cpp"
#include <filesystem>
class file_backend : public backend {
private:
unordered_map<string, string> file_cache;
bool cache_enabled;
unique_ptr<http_response> handle_request_nocache(http_request& req) {
string filename = req.path() == "/" ? "index.html" : req.path();
filename = "./www/" + filename;
ifstream stream(filename);
int status = 200;
if (!stream.is_open()) {
status = 404;
filename = "./error_pages/404.html";
stream = ifstream(filename);
}
stringstream buffer;
buffer << stream.rdbuf();
return make_unique<http_response>(buffer.str(), status);
}
unique_ptr<http_response> handle_request_cache(http_request& req) {
string filename = req.path() == "/" ? "/index.html" : req.path();
filename = "./www" + filename;
auto file_info = file_cache.find(filename);
int status = 200;
if (file_info == file_cache.end()) {
status = 404;
filename = "./error_pages/404.html";
file_info = file_cache.find(filename);
}
return make_unique<http_response>(file_info->second, status);
}
void populate_cache_dir(string dir) {
filesystem::recursive_directory_iterator cur = begin(filesystem::recursive_directory_iterator(dir));
filesystem::recursive_directory_iterator fin = end(filesystem::recursive_directory_iterator(dir));
while (cur != fin) {
auto p = cur->path();
string filename = p.string();
stringstream buffer;
ifstream stream(filename);
buffer << stream.rdbuf();
file_cache[filename] = buffer.str();
cout << "File at " << filename << " cached (" << file_cache[filename].size() << " bytes)" << endl;
++cur;
}
}
void populate_cache() {
populate_cache_dir("./www/");
populate_cache_dir("./error_pages/");
}
public:
file_backend(bool enable_cache) : cache_enabled(enable_cache) {
if(cache_enabled) {
populate_cache();
}
}
unique_ptr<http_response> handle_request(http_request& req) override {
return cache_enabled ? handle_request_cache(req) : handle_request_nocache(req);
}
};

12
src/error_pages/404.html Normal file
View file

@ -0,0 +1,12 @@
<html>
<head><title>Not Found</title></head>
<body>
<center>
<h1>404 - Not Found</h1>
<hr>
<p>Anthracite/0.0.1</p>
<p><small>Created by Nicholas Orlowsky </small> - <a href="https://github.com/nickorlow/anthracite"><small>This is Open Source Software</small></a></p>
</center>
</body>
</html>

12
src/error_pages/500.html Normal file
View file

@ -0,0 +1,12 @@
<html>
<head><title>Not Found</title></head>
<body>
<center>
<h1>500 - Internal Server Error</h1>
<hr>
<p>Anthracite/0.0.1</p>
<p><small>Created by Nicholas Orlowsky </small> - <a href="https://github.com/nickorlow/anthracite"><small>This is Open Source Software</small></a></p>
</center>
</body>
</html>

418
src/http.cpp Normal file
View file

@ -0,0 +1,418 @@
#include <arpa/inet.h>
#include <malloc.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include "socket.cpp"
#include <exception>
#include <fstream>
#include <iostream>
#include <sstream>
#include <unordered_map>
#include <utility>
#include <vector>
using namespace std;
constexpr int HTTP_HEADER_BYTES = 8190;
enum http_method {
GET,
POST,
DELETE,
PUT,
PATCH,
HEAD,
OPTIONS,
CONNECT,
TRACE,
COPY,
LINK,
UNLINK,
PURGE,
LOCK,
UNLOCK,
PROPFIND,
VIEW,
UNKNOWN
};
static unordered_map<string, http_method> const http_method_map = {
{ "GET", http_method::GET },
{ "POST", http_method::POST },
{ "DELETE", http_method::DELETE },
{ "PUT", http_method::PUT },
{ "PATCH", http_method::PATCH },
{ "HEAD", http_method::HEAD },
{ "OPTIONS", http_method::OPTIONS },
{ "CONNECT", http_method::CONNECT },
{ "TRACE", http_method::TRACE },
{ "COPY", http_method::COPY },
{ "LINK", http_method::LINK },
{ "UNLINK", http_method::UNLINK },
{ "PURGE", http_method::PURGE },
{ "LOCK", http_method::LOCK },
{ "UNLOCK", http_method::UNLOCK },
{ "PROPFIND", http_method::PROPFIND },
{ "VIEW", http_method::VIEW },
{ "UNKNOWN", http_method::UNKNOWN }
};
static unordered_map<http_method, string> const http_reverse_method_map = {
{ http_method::GET, "GET" },
{ http_method::POST, "POST" },
{ http_method::DELETE, "DELETE" },
{ http_method::PUT, "PUT" },
{ http_method::PATCH, "PATCH" },
{ http_method::HEAD, "HEAD" },
{ http_method::OPTIONS, "OPTIONS" },
{ http_method::CONNECT, "CONNECT" },
{ http_method::TRACE, "TRACE" },
{ http_method::COPY, "COPY" },
{ http_method::LINK, "LINK" },
{ http_method::UNLINK, "UNLINK" },
{ http_method::PURGE, "PURGE" },
{ http_method::LOCK, "LOCK" },
{ http_method::UNLOCK, "UNLOCK" },
{ http_method::PROPFIND, "PROPFIND" },
{ http_method::VIEW, "VIEW" },
{ http_method::UNKNOWN, "UNKNOWN" }
};
static unordered_map<int, string> const http_status_map = {
{ 100, "CONTINUE" },
{ 101, "SWITCHING PROTOCOLS" },
{ 200, "OK" },
{ 201, "CREATED" },
{ 202, "ACCEPTED" },
{ 203, "NON-AUTHORITATIVE INFORMATION" },
{ 204, "NO CONTENT" },
{ 205, "RESET CONTENT" },
{ 206, "PARTIAL CONTENT" },
{ 300, "MULTIPLE CHOICES" },
{ 301, "MOVED PERMANENTLY" },
{ 302, "FOUND" },
{ 303, "SEE OTHER" },
{ 304, "NOT MODIFIED" },
{ 305, "USE PROXY" },
{ 307, "TEMPORARY 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, "PAYLOAD TOO LARGE" },
{ 414, "URI TOO LONG" },
{ 415, "UNSUPPORTED MEDIA TYPE" },
{ 416, "RANGE NOT SATISFIABLE" },
{ 417, "EXPECTATION FAILED" },
{ 418, "I'M A TEAPOT" },
{ 421, "MISDIRECTED REQUEST" },
{ 422, "UNPROCESSABLE ENTITY" },
{ 423, "LOCKED" },
{ 424, "FAILED DEPENDENCY" },
{ 426, "UPGRADE REQUIRED" },
{ 428, "PRECONDITION REQUIRED" },
{ 429, "TOO MANY REQUESTS" },
{ 431, "REQUEST HEADER FIELDS TOO LARGE" },
{ 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" },
{ 506, "VARIANT ALSO NEGOTIATES" },
{ 507, "INSUFFICIENT STORAGE" },
{ 508, "LOOP DETECTED" },
{ 510, "NOT EXTENDED" },
{ 511, "NETWORK AUTHENTICATION REQUIRED" },
{ 420, "ENHANCE YOUR CALM" }
};
class name_value {
private:
string _name;
string _value;
protected:
name_value() {}
public:
name_value(string name, string value)
: _name(std::move(name))
, _value(std::move(value))
{
}
virtual ~name_value() = default;
name_value(const name_value&) = default;
name_value& operator=(name_value const&) = default;
name_value(name_value&&) = default;
name_value& operator=(name_value&&) = default;
string name() { return _name; }
string value() { return _value; }
virtual string to_string() { return ""; }
};
class http_header : public name_value {
public:
http_header()
: name_value()
{
}
http_header(string name, string value)
: name_value(name, value)
{
}
string to_string() override { return name() + ": " + value() + "\r\n"; }
};
class query_param : public name_value {
public:
query_param()
: name_value()
{
}
query_param(string name, string value)
: name_value(name, value)
{
}
string to_string() override { return name() + "=" + value(); }
};
enum http_version { HTTP_0_9,
HTTP_1_0,
HTTP_1_1,
HTTP_2_0,
HTTP_3_0 };
static std::unordered_map<std::string, http_version> const http_version_map = {
{ "HTTP/0.9", HTTP_0_9 },
{ "HTTP/1.0", HTTP_1_0 },
{ "HTTP/1.1", HTTP_1_1 },
{ "HTTP/2.0", HTTP_2_0 },
{ "HTTP/3.0", HTTP_3_0 }
};
static std::unordered_map<http_version, std::string> const http_reverse_version_map = {
{ HTTP_0_9, "HTTP/0.9" },
{ HTTP_1_0, "HTTP/1.0" },
{ HTTP_1_1, "HTTP/1.1" },
{ HTTP_2_0, "HTTP/2.0" },
{ HTTP_3_0, "HTTP/3.0" }
};
class http_request {
private:
enum parser_state { METHOD,
PATH,
QUERY_PARAM_NAME,
QUERY_PARAM_VALUE,
VERSION,
HEADER_NAME,
HEADER_VALUE,
BODY_CONTENT };
http_method _method;
http_version _http_version;
string _path;
string _client_ipaddr;
string _body_content;
unordered_map<string, http_header> _headers; // kinda goofy, whatever
unordered_map<string, query_param> _query_params; // kinda goofy, whatever
public:
http_request(anthracite_socket& s)
: _path("")
{
string raw_data = s.recv_message(HTTP_HEADER_BYTES);
_client_ipaddr = s.get_client_ip();
parser_state state = METHOD;
string scratch = "";
string scratch_2 = "";
for (int i = 0; i < raw_data.length(); i++) {
switch (state) {
case METHOD: {
if (raw_data[i] == ' ') {
if (http_method_map.find(scratch) == http_method_map.end()) {
_method = http_method::UNKNOWN;
} else {
_method = http_method_map.find(scratch)->second;
}
scratch = "";
state = PATH;
} else {
scratch += raw_data[i];
}
} break;
case PATH: {
switch (raw_data[i]) {
case ' ':
state = VERSION;
break;
case '?':
state = QUERY_PARAM_NAME;
break;
default:
_path += raw_data[i];
break;
}
} break;
case QUERY_PARAM_NAME: {
if (raw_data[i] == ' ') {
scratch = "";
state = VERSION;
} else if (raw_data[i] == '=') {
state = QUERY_PARAM_VALUE;
} else {
scratch += raw_data[i];
}
} break;
case QUERY_PARAM_VALUE: {
if (raw_data[i] == ' ') {
_query_params[scratch] = query_param(scratch, scratch_2);
scratch = "";
scratch_2 = "";
state = VERSION;
} else if (raw_data[i] == '&') {
_query_params[scratch] = query_param(scratch, scratch_2);
scratch = "";
scratch_2 = "";
state = QUERY_PARAM_NAME;
} else {
scratch_2 += raw_data[i];
}
} break;
case VERSION: {
if (raw_data[i] == '\n') {
_http_version = http_version_map.find(scratch)->second;
scratch = "";
state = HEADER_NAME;
} else if (raw_data[i] != '\r') {
scratch += raw_data[i];
}
} break;
case HEADER_NAME: {
if (raw_data[i] == '\n') {
scratch = "";
scratch_2 = "";
state = BODY_CONTENT;
break;
} else if (raw_data[i] == ' ') {
scratch = "";
cout << "Error: Whitespace found in header name\n";
break;
} else if (raw_data[i] == ':') {
state = HEADER_VALUE;
i++;
} else {
scratch += raw_data[i];
}
} break;
case HEADER_VALUE: {
if (raw_data[i] == '\n') {
_headers[scratch] = http_header(scratch, scratch_2);
scratch = "";
scratch_2 = "";
state = HEADER_NAME;
} else if (raw_data[i] != '\r') {
scratch_2 += raw_data[i];
}
} break;
case BODY_CONTENT: {
_body_content += raw_data[i];
} break;
}
}
}
string path() { return _path; }
http_method method() { return _method; }
string client_ip() { return _client_ipaddr; }
string to_string()
{
string response = "";
response += http_reverse_method_map.find(_method)->second + " " + _path + "?";
for (auto qp : _query_params) {
response += qp.second.to_string() + "&";
}
response += " " + http_reverse_version_map.find(_http_version)->second + "\r\n";
for (auto header : _headers) {
response += header.second.to_string();
}
response += "\r\n";
response += _body_content;
return response;
}
};
class http_response {
private:
int _status_code;
string _content;
unordered_map<string, http_header> _headers; // kinda goofy, whatever
public:
http_response(string content, int status_code = 200)
: _content(std::move(content))
, _status_code(status_code)
{
}
int status_code() { return _status_code; }
void add_header(http_header header, bool override_existing = true)
{
if (override_existing || _headers.find(header.name()) == _headers.end()) {
_headers[header.name()] = header;
}
}
string to_string()
{
string response = "";
response += "HTTP/1.1 " + ::to_string(_status_code) + " " + http_status_map.find(_status_code)->second + "\r\n";
add_header(http_header("Content-Type", "text/html"), false);
add_header(http_header("Content-Length", ::to_string(_content.length())), false);
add_header(http_header("Server", "Anthracite/0.0.1"), false);
for (auto header : _headers) {
response += header.second.to_string();
}
response += "\r\n";
response += _content;
return response;
}
};

69
src/main.cpp Normal file
View file

@ -0,0 +1,69 @@
#include "backends/file_backend.cpp"
#include <condition_variable>
#include <exception>
#include <fstream>
#include <iostream>
#include <mutex>
#include <netinet/in.h>
#include <sstream>
#include <sys/socket.h>
#include <thread>
#include <unistd.h>
#include <unordered_map>
using namespace std;
void log_request_and_response(http_request& req, unique_ptr<http_response>& resp);
constexpr int default_port = 80;
int active_threads = 0;
mutex mtx;
condition_variable cv;
void handle_client(anthracite_socket s, file_backend& fb)
{
http_request req(s);
unique_ptr<http_response> resp = fb.handle_request(req);
log_request_and_response(req, resp);
s.send_message(resp->to_string());
resp.reset();
s.close_conn();
{
std::lock_guard<std::mutex> lock(mtx);
active_threads--;
}
cv.notify_one();
}
int main(int argc, char** argv)
{
int port_number = default_port;
if (argc > 1) {
port_number = atoi(argv[1]);
}
cout << "Initializing Anthracite" << endl;
anthracite_socket s(port_number);
file_backend fb(true);
cout << "Initialization Complete" << endl;
cout << "Listening for HTTP connections on port " << port_number << endl;
for (;;) {
s.wait_for_conn();
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return active_threads < 20; });
active_threads++;
thread(handle_client, s, ref(fb)).detach();
}
exit(0);
}
void log_request_and_response(http_request& req, unique_ptr<http_response>& resp)
{
cout << "[" << resp->status_code() << " " + http_status_map.find(resp->status_code())->second + "] " + req.client_ip() + " " + http_reverse_method_map.find(req.method())->second + " " + req.path() << endl;
}

78
src/socket.cpp Normal file
View file

@ -0,0 +1,78 @@
#include <arpa/inet.h>
#include <exception>
#include <fstream>
#include <iostream>
#include <malloc.h>
#include <netinet/in.h>
#include <sstream>
#include <sys/socket.h>
#include <unistd.h>
#include <unordered_map>
using namespace std;
class anthracite_socket {
private:
int server_socket;
int client_socket {};
string client_ip;
struct sockaddr_in client_addr {};
socklen_t client_addr_len {};
public:
anthracite_socket(int port, int max_queue = 10)
: server_socket(socket(AF_INET, SOCK_STREAM, 0))
, client_ip("")
{
struct sockaddr_in address {};
address.sin_family = AF_INET;
address.sin_port = htons(port);
address.sin_addr.s_addr = INADDR_ANY;
int x = 1;
setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &x, sizeof(x));
bind(server_socket, (struct sockaddr*)&address, sizeof(address));
::listen(server_socket, max_queue);
}
void wait_for_conn()
{
client_ip = "";
client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &client_addr_len);
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, INET_ADDRSTRLEN);
client_ip = string(ip_str);
}
string get_client_ip()
{
return client_ip;
}
void close_conn()
{
close(client_socket);
client_socket = -1;
}
void send_message(string msg)
{
if (client_socket == -1) {
return;
}
send(client_socket, &msg[0], msg.length(), 0);
}
string recv_message(int buffer_size = 1024)
{
if (client_socket == -1) {
return "";
}
char response[buffer_size + 1];
recv(client_socket, response, sizeof(response), 0);
response[buffer_size] = '\0';
return string(response);
}
};

5
src/www/index.html Normal file
View file

@ -0,0 +1,5 @@
<center>
<h1>Anthracite is Running!</h1>
<p>If you are seeing this page, then Anthracite is configured correctly!</p>
<p>Add files to the "www" directory to begin serving your website.</p>
</center>

1020251
src/www/large.html Normal file

File diff suppressed because it is too large Load diff

3
src/www/test.html Normal file
View file

@ -0,0 +1,3 @@
<center>
<h1>Test Page!</h1>
</center>