Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simple web server support for static files #21

Merged
merged 13 commits into from
Dec 15, 2018
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ sedona-*.zip
*.log
*.swo
.DS_Store
statics/*
259 changes: 259 additions & 0 deletions src/kits/communityWebServer/StaticFileWeblet.sedona
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
//
// Copyright (c) 2018
// Licensed under the Academic Free License version 3.0
//
// History:
// 30 Jun 18 Vincent Wang Static File Weblet
//

**
** StaticFileWeblet provides the ability to serve static files(html, css, js,
** images etc) through http, so that user can use sedona as a minimal web
** server and host a static web app just in sedona, without any outside
** dependency.
** to save space on sedona, gzipped file is supported and I recommend to use it
** since that will be more efficient(less bytes to be loaded and transferred)
**
class StaticFileWeblet extends Weblet
{
////////////////////////////////////////////////////////////////
// Weblet Registration
////////////////////////////////////////////////////////////////

static void _sInit()
{
instance.register()
divisuals marked this conversation as resolved.
Show resolved Hide resolved
}
static internal inline StaticFileWeblet instance

////////////////////////////////////////////////////////////////
// Weblet
////////////////////////////////////////////////////////////////

**
** all static files to be served must be put under 'statics' folder
** and the url will start with 'statics', too
**
override Str prefix() { return "statics" }
divisuals marked this conversation as resolved.
Show resolved Hide resolved

override Str description() { return "Sedona Static File Weblet" }

**
** since we only serve static file here, so only 'GET' method is supported
**
override void service(WebReq req, WebRes res)
{
Str method = req.method;
if (method.equals("GET"))
get(req, res)
else {
res.writeStatus(HttpCode.methodNotAllowed)
res.writeHeader("Allow", "GET")
}
}

**
** handle 'GET' request, try to find the static file and return it
**
override void get(WebReq req, WebRes res)
{
reset()

prepareFile(req)

if (isDir(filePath)) {
tryAppendIndex(filePath)
}

serveFile(res)
}

internal void reset()
{
filePath.set(0, 0)
}

internal void serveFile(WebRes res)
{
if (file.name==null) {
res.writeStatus(HttpCode.notFound).finishHeaders()
return
}

if (!file.exists()) {
//try gzipped content
appendStr(file.name, ".gz", pathStrLen)

if (!file.exists()) {
res.writeStatus(HttpCode.notFound).finishHeaders()
return
}
}

if (!file.open("r")) {
res.writeStatus(HttpCode.internalError).finishHeaders()
return
}

App.log.message("Serving File: " + file.name);
linsong marked this conversation as resolved.
Show resolved Hide resolved
res.writeStatusOk()

//write headers
if (endsWith(file.name, ".gz", 0))
res.writeHeader("Content-Encoding", "gzip");

res.writeContentType(getContentType(file.name))
.writeHeader("Cache-Control", "max-age=604800")
.writeHeader("Server", "Sedona Web Server")
.writeHeader("Content-Length", Sys.intStr(file.size()))
.finishHeaders()

file.seek(0)
while(true) {
int readed = file.in.readBytes(readBuf, 0, bufLen)
if (readed <= 0)
break

res.writeBytes(readBuf, 0, readed)
}

file.close()
}

bool isDir(Str fileName) {
if (endsWith(fileName, "/", 0))
return true

int len = fileName.length()
//check if there is a file extention
for(int i=len-3; i>0 && i>len-6; --i) {
if (fileName.get(i) != '.')
continue

return false
}

return true
}

**
** if the URL ends with '/', then append 'index.html' and try serve it
**
bool tryAppendIndex(Str fileName) {
int len = fileName.length()
if (len <= 0 || len+1>=pathStrLen)
return false

//try to append index.html to path end
if (!endsWith(fileName, "/", 0))
appendStr(fileName, "/", pathStrLen)

return appendStr(fileName, "index.html", pathStrLen)
}

**
** check if str ends with suffix.
** if endOffset is not 0, then check the suffix after
** that offset. for example, to check if 'index.html.gz'
** ends with '.html':
** // buf equals "index.html.gz", offset is 3(len of ".gz")
** endsWith(buf, ".html", 3)
**
static public bool endsWith(Str str, Str suffix, int endOffset) {
return suffix.equalsRegion(str, str.length()-suffix.length()-endOffset, str.length()-endOffset)
}

**
** append suffix to the end of dst.
** return:
** true if managed to append suffix to dst
** false if dst's buffer is not big enough
**
static public bool appendStr(Str dst, Str suffix, int bufMaxLen)
{
int dstLen = dst.length()
int suffixLen = suffix.length()
//return early if buffer is not big enough
if (dstLen + suffixLen + 1 > bufMaxLen)
return false

byte[] dstBuf = dst.toBytes()
byte[] suffixBuf = suffix.toBytes()
for(int i=0; i<suffixLen; ++i)
dstBuf[dstLen+i] = suffixBuf[i]
dstBuf[dstLen+suffixLen] = 0
return true
}

internal Str getContentType(Str fileName)
{
int endOffset = 0
if (endsWith(fileName, ".gz", 0))
endOffset = 3

if (endsWith(fileName, ".html", endOffset) || endsWith(fileName, ".htm", endOffset))
return "text/html"
else if (endsWith(fileName, ".css", endOffset))
return "text/css"
else if (endsWith(fileName, ".txt", endOffset))
return "text/plain"
else if (endsWith(fileName, ".js", endOffset))
return "application/javascript"
else if (endsWith(fileName, ".json", endOffset))
return "application/json"
else if (endsWith(fileName, ".xml", endOffset))
return "application/xml"
else if (endsWith(fileName, ".pdf", endOffset))
return "application/pdf"
else if (endsWith(fileName, ".jpg", endOffset))
return "image/jpeg"
else if (endsWith(fileName, ".png", endOffset))
return "image/png"
else
return "text/plain"
}

internal void prepareFile(WebReq req)
{
if (req.path.size < 1)
return
else {
//TODO: process path to avoid security issues
filePath.copyFromStr(".", pathStrLen);
int index = filePath.length()
byte[] fileBuf = filePath.toBytes()
for(int i=0; i<req.path.size && index<pathStrLen-2; ++i) {
byte[] nameBuf = req.path.names[i].toBytes()
fileBuf[index++] = '/'
for(int j=0; j<req.path.names[i].length() && index<pathStrLen-2; ++j) {
fileBuf[index++] = nameBuf[j]
}
}
fileBuf[index] = 0
}

file.name = filePath
}

////////////////////////////////////////////////////////////////
// Functions
////////////////////////////////////////////////////////////////

public void index(WebReq req, WebRes res)
{
res.html();
res.w("<h1>Hello World!</h1>\n");
res.htmlEnd();
}

////////////////////////////////////////////////////////////////
// Fields
////////////////////////////////////////////////////////////////
define int bufLen = 1024
define int pathStrLen = 256

private inline byte[bufLen] readBuf
internal inline Str(pathStrLen) filePath // buffer for file's path
internal inline File file
}

26 changes: 26 additions & 0 deletions src/kits/communityWebServer/kit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!--
- Sedona Web Server Kit
- Copyright (c) 2018 Sedona Community
- Licensed under the Academic Free License version 3.0
-->

<sedonaKit
name = "communityWebServer"
vendor = "community"
description = "Simple Web Server mainly to serve static files"
includeSource = "true"
doc = "true"
>

<!-- Dependencies -->
<depend on="sys 1.2+" />
<depend on="inet 1.2+" />
<depend on="sox 1.2+" />
<depend on="web 1.2+" />

<!-- Source Directories -->
<source dir="." />
<source dir="test" />

</sedonaKit>

35 changes: 35 additions & 0 deletions src/kits/communityWebServer/test/StrUtilTest.sedona
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@palette=false
public class StrUtilTest extends Test
{
static void testEndsWith()
{
assert(!StaticFileWeblet.endsWith("/foo/bar", "/", 0))
assert(StaticFileWeblet.endsWith("/foo/bar/", "/", 0))
assert(StaticFileWeblet.endsWith("/foo/bar/index.html", ".html", 0))

assert(StaticFileWeblet.endsWith("/foo/bar/vue.min.js.gz", ".js", 3))
}

static void testAppendStr()
{
assert(StaticFileWeblet.appendStr(buf, "/foo/bar", bufLen))
assert(buf.equals("/foo/bar"))

assert(StaticFileWeblet.appendStr(buf, "/", bufLen))
assert(buf.equals("/foo/bar/"))

assert(StaticFileWeblet.appendStr(buf, "index.html", bufLen))
assert(buf.equals("/foo/bar/index.html"))

assert(StaticFileWeblet.appendStr(buf, ".gz", bufLen))
assert(buf.equals("/foo/bar/index.html.gz"))

//test buffer overflow
assert(!StaticFileWeblet.appendStr(buf, "1234567890", bufLen))
//under this case, buf should not be modified
assert(buf.equals("/foo/bar/index.html.gz"))
}

define int bufLen = 32
inline static Str(bufLen) buf
}