Added a way for admins to reset passwords.

This commit is contained in:
Dominik Picheta 2015-03-16 20:01:19 +00:00
commit b7584de440

152
forum.nim
View file

@ -50,7 +50,7 @@ type
totalPosts: int totalPosts: int
search: string search: string
noPagenumumNav: bool noPagenumumNav: bool
TStyledButton = tuple[text: string, link: string] TStyledButton = tuple[text: string, link: string]
TForumStats = object TForumStats = object
@ -72,23 +72,23 @@ var
db: TDbConn db: TDbConn
docConfig: StringTableRef docConfig: StringTableRef
isFTSAvailable: bool isFTSAvailable: bool
proc init(c: var TForumData) = proc init(c: var TForumData) =
c.userPass = "" c.userPass = ""
c.userName = "" c.userName = ""
c.threadId = unselectedThread c.threadId = unselectedThread
c.postId = -1 c.postId = -1
c.userid = "" c.userid = ""
c.actionContent = "" c.actionContent = ""
c.errorMsg = "" c.errorMsg = ""
c.loginErrorMsg = "" c.loginErrorMsg = ""
c.invalidField = "" c.invalidField = ""
c.currentPost = (subject: "", content: "") c.currentPost = (subject: "", content: "")
c.search = "" c.search = ""
proc loggedIn(c: TForumData): bool = proc loggedIn(c: TForumData): bool =
result = c.userName.len > 0 result = c.userName.len > 0
# --------------- HTML widgets ------------------------------------------------ # --------------- HTML widgets ------------------------------------------------
@ -98,7 +98,7 @@ proc loggedIn(c: TForumData): bool =
const const
reuseText = "\1" reuseText = "\1"
proc TextWidget(c: TForumData, name, defaultText: string, proc TextWidget(c: TForumData, name, defaultText: string,
maxlength = 30, size = -1): string = maxlength = 30, size = -1): string =
let x = if defaultText != reuseText: defaultText let x = if defaultText != reuseText: defaultText
else: xmlEncode(c.req.params[name]) else: xmlEncode(c.req.params[name])
@ -116,8 +116,8 @@ proc TextAreaWidget(c: TForumData, name, defaultText: string): string =
return """<textarea name="$1">$2</textarea>""" % [ return """<textarea name="$1">$2</textarea>""" % [
name, x] name, x]
proc FieldValid(c: TForumData, name, text: string): string = proc FieldValid(c: TForumData, name, text: string): string =
if name == c.invalidField: if name == c.invalidField:
result = """<span style="color:red">$1</span>""" % text result = """<span style="color:red">$1</span>""" % text
else: else:
result = text result = text
@ -146,14 +146,14 @@ proc UrlButton(c: var TForumData, text, url: string): string =
proc genButtons(c: var TForumData, btns: seq[TStyledButton]): string = proc genButtons(c: var TForumData, btns: seq[TStyledButton]): string =
if btns.len == 1: if btns.len == 1:
var anchor = "" var anchor = ""
result = ("""<a class="active button" href="$1$3">$2</a>""") % [ result = ("""<a class="active button" href="$1$3">$2</a>""") % [
btns[0].link, btns[0].text, anchor] btns[0].link, btns[0].text, anchor]
else: else:
result = "" result = ""
for i, btn in pairs(btns): for i, btn in pairs(btns):
var anchor = "" var anchor = ""
var class = "" var class = ""
if i == 0: class = "left " if i == 0: class = "left "
elif i == btns.len()-1: class = "right " elif i == btns.len()-1: class = "right "
@ -200,7 +200,7 @@ proc getGravatarUrl(email: string, size = 80): string =
"&d=identicon") "&d=identicon")
proc genGravatar(email: string, size: int = 80): string = proc genGravatar(email: string, size: int = 80): string =
result = "<img width=\"$1\" height=\"$2\" src=\"$3\" />" % result = "<img width=\"$1\" height=\"$2\" src=\"$3\" />" %
[$size, $size, getGravatarUrl(email, size)] [$size, $size, getGravatarUrl(email, size)]
proc randomSalt(): string = proc randomSalt(): string =
@ -250,17 +250,17 @@ proc makePassword(password, salt: string, comparingTo = ""): string =
template `||`(x: expr): expr = (if not isNil(x): x else: "") template `||`(x: expr): expr = (if not isNil(x): x else: "")
proc validThreadId(c: TForumData): bool = proc validThreadId(c: TForumData): bool =
result = getValue(db, sql"select id from thread where id = ?", result = getValue(db, sql"select id from thread where id = ?",
$c.threadId).len > 0 $c.threadId).len > 0
proc antibot(c: var TForumData): string = proc antibot(c: var TForumData): string =
let a = math.random(10)+1 let a = math.random(10)+1
let b = math.random(1000)+1 let b = math.random(1000)+1
let answer = $(a+b) let answer = $(a+b)
exec(db, sql"delete from antibot where ip = ?", c.req.ip) exec(db, sql"delete from antibot where ip = ?", c.req.ip)
let captchaId = tryInsertID(db, let captchaId = tryInsertID(db,
sql"insert into antibot(ip, answer) values (?, ?)", c.req.ip, sql"insert into antibot(ip, answer) values (?, ?)", c.req.ip,
answer).int mod 10_000 answer).int mod 10_000
let captchaFile = getCaptchaFilename(captchaId) let captchaFile = getCaptchaFilename(captchaId)
createCaptcha(captchaFile, $a & "+" & $b) createCaptcha(captchaFile, $a & "+" & $b)
@ -274,27 +274,27 @@ proc setError(c: var TForumData, field, msg: string): bool {.inline.} =
c.errorMsg = "Error: " & msg c.errorMsg = "Error: " & msg
return false return false
proc register(c: var TForumData, name, pass, antibot, email: string): bool = proc register(c: var TForumData, name, pass, antibot, email: string): bool =
# Username validation: # Username validation:
if name.len == 0 or not allCharsInSet(name, SecureChars): if name.len == 0 or not allCharsInSet(name, SecureChars):
return setError(c, "name", "Invalid username!") return setError(c, "name", "Invalid username!")
if getValue(db, sql"select name from person where name = ?", name).len > 0: if getValue(db, sql"select name from person where name = ?", name).len > 0:
return setError(c, "name", "Username already exists!") return setError(c, "name", "Username already exists!")
# Password validation: # Password validation:
if pass.len < 4: if pass.len < 4:
return setError(c, "new_password", "Invalid password!") return setError(c, "new_password", "Invalid password!")
# antibot validation: # antibot validation:
let correctRes = getValue(db, let correctRes = getValue(db,
sql"select answer from antibot where ip = ?", c.req.ip) sql"select answer from antibot where ip = ?", c.req.ip)
if antibot != correctRes: if antibot != correctRes:
return setError(c, "antibot", "You seem to be a bot!") return setError(c, "antibot", "You seem to be a bot!")
# email validation # email validation
if not validEmailAddress(email): if not validEmailAddress(email):
return setError(c, "email", "Invalid email address") return setError(c, "email", "Invalid email address")
# perform registration: # perform registration:
var salt = makeSalt() var salt = makeSalt()
exec(db, exec(db,
@ -304,18 +304,18 @@ proc register(c: var TForumData, name, pass, antibot, email: string): bool =
# return setError(c, "", "Could not create your account!") # return setError(c, "", "Could not create your account!")
return true return true
proc checkLoggedIn(c: var TForumData) = proc checkLoggedIn(c: var TForumData) =
let pass = c.req.cookies["sid"] let pass = c.req.cookies["sid"]
if pass.len == 0: return if pass.len == 0: return
if execAffectedRows(db, if execAffectedRows(db,
sql("update session set lastModified = DATETIME('now') " & sql("update session set lastModified = DATETIME('now') " &
"where ip = ? and password = ?"), "where ip = ? and password = ?"),
c.req.ip, pass) > 0: c.req.ip, pass) > 0:
c.userpass = pass c.userpass = pass
c.userid = getValue(db, c.userid = getValue(db,
sql"select userid from session where ip = ? and password = ?", sql"select userid from session where ip = ? and password = ?",
c.req.ip, pass) c.req.ip, pass)
let row = getRow(db, let row = getRow(db,
sql"select name, email, admin from person where id = ?", c.userid) sql"select name, email, admin from person where id = ?", c.userid)
c.username = ||row[0] c.username = ||row[0]
@ -324,7 +324,7 @@ proc checkLoggedIn(c: var TForumData) =
# Update lastOnline # Update lastOnline
db.exec(sql"update person set lastOnline = DATETIME('now') where id = ?", db.exec(sql"update person set lastOnline = DATETIME('now') where id = ?",
c.userid) c.userid)
else: else:
echo("SID not found in sessions. Assuming logged out.") echo("SID not found in sessions. Assuming logged out.")
@ -334,7 +334,7 @@ proc logout(c: var TForumData) =
c.userpass = "" c.userpass = ""
exec(db, query, c.req.ip, c.req.cookies["sid"]) exec(db, query, c.req.ip, c.req.cookies["sid"])
proc incrementViews(c: var TForumData) = proc incrementViews(c: var TForumData) =
const query = sql"update thread set views = views + 1 where id = ?" const query = sql"update thread set views = views + 1 where id = ?"
exec(db, query, $c.threadId) exec(db, query, $c.threadId)
@ -345,7 +345,7 @@ proc isDelete(c: TForumData): bool =
result = c.req.params["delete"].len > 0 result = c.req.params["delete"].len > 0
proc rstToHtml(content: string): string = proc rstToHtml(content: string): string =
result = rstgen.rstToHtml(content, {roSupportSmilies, roSupportMarkdown}, result = rstgen.rstToHtml(content, {roSupportSmilies, roSupportMarkdown},
docConfig) docConfig)
proc validateRst(c: var TForumData, content: string): bool = proc validateRst(c: var TForumData, content: string): bool =
@ -358,10 +358,10 @@ proc validateRst(c: var TForumData, content: string): bool =
proc crud(c: TCrud, table: string, data: varargs[string]): TSqlQuery = proc crud(c: TCrud, table: string, data: varargs[string]): TSqlQuery =
case c case c
of crCreate: of crCreate:
var fields = "insert into " & table & "(" var fields = "insert into " & table & "("
var vals = "" var vals = ""
for i, d in data: for i, d in data:
if i > 0: if i > 0:
fields.add(", ") fields.add(", ")
vals.add(", ") vals.add(", ")
fields.add(d) fields.add(d)
@ -403,7 +403,7 @@ template checkLogin(c: expr) =
template checkOwnership(c, postId: expr) = template checkOwnership(c, postId: expr) =
if not c.isAdmin: if not c.isAdmin:
let x = getValue(db, sql"select author from post where id = ?", let x = getValue(db, sql"select author from post where id = ?",
postId) postId)
if x != c.userId: if x != c.userId:
return setError(c, "", "You are not the owner of this post") return setError(c, "", "You are not the owner of this post")
@ -421,7 +421,7 @@ template writeToDb(c, cr, setPostId: expr) =
c.postId = retID.int c.postId = retID.int
proc edit(c: var TForumData, postId: int): bool = proc edit(c: var TForumData, postId: int): bool =
checkLogin(c) checkLogin(c)
if c.isPreview: if c.isPreview:
retrPost(c) retrPost(c)
setPreviewData(c) setPreviewData(c)
@ -458,19 +458,19 @@ proc edit(c: var TForumData, postId: int): bool =
if rows[0][0] == $postId: if rows[0][0] == $postId:
exec(db, crud(crUpdate, "thread", "name"), subject, $c.threadId) exec(db, crud(crUpdate, "thread", "name"), subject, $c.threadId)
result = true result = true
proc reply(c: var TForumData): bool = proc reply(c: var TForumData): bool =
checkLogin(c) checkLogin(c)
retrPost(c) retrPost(c)
if c.isPreview: if c.isPreview:
setPreviewData(c) setPreviewData(c)
else: else:
writeToDb(c, crCreate, true) writeToDb(c, crCreate, true)
exec(db, sql"update thread set modified = DATETIME('now') where id = ?", exec(db, sql"update thread set modified = DATETIME('now') where id = ?",
$c.threadId) $c.threadId)
result = true result = true
proc newThread(c: var TForumData): bool = proc newThread(c: var TForumData): bool =
const query = sql"insert into thread(name, views, modified) values (?, 0, DATETIME('now'))" const query = sql"insert into thread(name, views, modified) values (?, 0, DATETIME('now'))"
checkLogin(c) checkLogin(c)
@ -488,9 +488,9 @@ proc newThread(c: var TForumData): bool =
discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')") discard tryExec(db, sql"insert into post_fts(thread_fts) values('optimize')")
result = true result = true
proc login(c: var TForumData, name, pass: string): bool = proc login(c: var TForumData, name, pass: string): bool =
# get form data: # get form data:
const query = const query =
sql"select id, name, password, email, salt, admin, ban from person where name = ?" sql"select id, name, password, email, salt, admin, ban from person where name = ?"
if name.len == 0: if name.len == 0:
return c.setError("name", "Username cannot be nil.") return c.setError("name", "Username cannot be nil.")
@ -513,7 +513,7 @@ proc login(c: var TForumData, name, pass: string): bool =
if success: if success:
# create session: # create session:
exec(db, exec(db,
sql"insert into session (ip, password, userid) values (?, ?, ?)", sql"insert into session (ip, password, userid) values (?, ?, ?)",
c.req.ip, c.userpass, c.userid) c.req.ip, c.userpass, c.userid)
return true return true
else: else:
@ -524,6 +524,12 @@ proc setBan(c: var TForumData, nick, reason: string): bool =
sql("update person set ban = ? where name = ?") sql("update person set ban = ? where name = ?")
return tryExec(db, query, reason, nick) return tryExec(db, query, reason, nick)
proc setPassword(c: var TForumData, nick, pass: string): bool =
const query =
sql("update person set password = ?, salt = ? where name = ?")
var salt = makeSalt()
result = tryExec(db, query, makePassword(pass, salt), salt, nick)
proc hasReplyBtn(c: var TForumData): bool = proc hasReplyBtn(c: var TForumData): bool =
result = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply" result = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply"
result = result and c.req.params["action"] != "reply" result = result and c.req.params["action"] != "reply"
@ -543,14 +549,14 @@ proc genActionMenu(c: var TForumData): string =
if c.loggedIn: if c.loggedIn:
let hasReplyBtn = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply" let hasReplyBtn = c.req.pathInfo != "/donewthread" and c.req.pathInfo != "/doreply"
if c.threadId >= 0 and hasReplyBtn: if c.threadId >= 0 and hasReplyBtn:
let replyUrl = c.genThreadUrl(action = "reply", let replyUrl = c.genThreadUrl(action = "reply",
pageNum = $(ceil(c.totalPosts / PostsPerPage).int)) & "#reply" pageNum = $(ceil(c.totalPosts / PostsPerPage).int)) & "#reply"
btns.add(("Reply", replyUrl)) btns.add(("Reply", replyUrl))
btns.add(("New Thread", c.req.makeUri("/newthread", false))) btns.add(("New Thread", c.req.makeUri("/newthread", false)))
result = c.genButtons(btns) result = c.genButtons(btns)
proc getStats(c: var TForumData, simple: bool): TForumStats = proc getStats(c: var TForumData, simple: bool): TForumStats =
const totalUsersQuery = const totalUsersQuery =
sql"select count(*) from person" sql"select count(*) from person"
result.totalUsers = getValue(db, totalUsersQuery).parseInt result.totalUsers = getValue(db, totalUsersQuery).parseInt
const totalPostsQuery = const totalPostsQuery =
@ -576,13 +582,13 @@ proc getStats(c: var TForumData, simple: bool): TForumStats =
proc genPagenumNav(c: var TForumData, stats: TForumStats): string = proc genPagenumNav(c: var TForumData, stats: TForumStats): string =
result = "" result = ""
var var
firstUrl = "" firstUrl = ""
prevUrl = "" prevUrl = ""
totalPages = 0 totalPages = 0
lastUrl = "" lastUrl = ""
nextUrl = "" nextUrl = ""
if c.isThreadsList: if c.isThreadsList:
firstUrl = c.req.makeUri("/") firstUrl = c.req.makeUri("/")
prevUrl = c.req.makeUri(if c.pageNum == 1: "/" else: "/page/" & $(c.pageNum-1)) prevUrl = c.req.makeUri(if c.pageNum == 1: "/" else: "/page/" & $(c.pageNum-1))
@ -593,15 +599,15 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string =
firstUrl = c.req.makeUri("/t/" & $c.threadId) firstUrl = c.req.makeUri("/t/" & $c.threadId)
if c.pageNum == 1: if c.pageNum == 1:
prevUrl = firstUrl prevUrl = firstUrl
else: else:
prevUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum-1)) prevUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum-1))
totalPages = ceil(c.totalPosts / PostsPerPage).int totalPages = ceil(c.totalPosts / PostsPerPage).int
lastUrl = c.req.makeUri(firstUrl & "/" & $(totalPages)) lastUrl = c.req.makeUri(firstUrl & "/" & $(totalPages))
nextUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum+1)) nextUrl = c.req.makeUri(firstUrl & "/" & $(c.pageNum+1))
if totalPages <= 1: if totalPages <= 1:
return "" return ""
var firstTag = "" var firstTag = ""
var prevTag = "" var prevTag = ""
if c.pageNum == 1: if c.pageNum == 1:
@ -613,7 +619,7 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string =
prevTag.add(htmlgen.link(rel="previous", href=prevUrl)) prevTag.add(htmlgen.link(rel="previous", href=prevUrl))
result.add(firstTag) result.add(firstTag)
result.add(prevTag) result.add(prevTag)
# Numbers # Numbers
var pages = "" # Tags var pages = "" # Tags
# cutting numbers to the left and to the right tp MaxPagesFromCurrent # cutting numbers to the left and to the right tp MaxPagesFromCurrent
@ -629,7 +635,7 @@ proc genPagenumNav(c: var TForumData, stats: TForumStats): string =
pageUrl = c.req.makeUri("/page/" & $(i)) pageUrl = c.req.makeUri("/page/" & $(i))
else: else:
pageUrl = c.req.makeUri(firstUrl & "/" & $(i)) pageUrl = c.req.makeUri(firstUrl & "/" & $(i))
pages.add(htmlgen.a(href = pageUrl, $(i))) pages.add(htmlgen.a(href = pageUrl, $(i)))
if lastToShow < totalPages: pages.add(span("...")) if lastToShow < totalPages: pages.add(span("..."))
result.add(pages) result.add(pages)
@ -702,7 +708,7 @@ proc gatherUserInfo(c: var TForumData, nick: string, ui: var TUserInfo): bool =
const totalThreadsQuery = const totalThreadsQuery =
sql("select count(*) from thread where id in (select thread from post where" & sql("select count(*) from thread where id in (select thread from post where" &
" author = ? and post.id in (select min(id) from post group by thread))") " author = ? and post.id in (select min(id) from post group by thread))")
ui.threads = getValue(db, totalThreadsQuery, uid).parseInt ui.threads = getValue(db, totalThreadsQuery, uid).parseInt
const lastOnlineQuery = const lastOnlineQuery =
sql"select strftime('%s', lastOnline) from person where id = ?" sql"select strftime('%s', lastOnline) from person where id = ?"
@ -729,10 +735,10 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string =
) )
) )
result.add(htmlgen.`div`(id = "avatar", genGravatar(ui.email, 250))) result.add(htmlgen.`div`(id = "avatar", genGravatar(ui.email, 250)))
let t2 = if ui.lastOnline != -1: getGMTime(Time(ui.lastOnline)) let t2 = if ui.lastOnline != -1: getGMTime(Time(ui.lastOnline))
else: getGMTime(getTime()) else: getGMTime(getTime())
result.add(htmlgen.`div`(id = "info", result.add(htmlgen.`div`(id = "info",
htmlgen.table( htmlgen.table(
tr( tr(
th("Nickname"), th("Nickname"),
@ -788,7 +794,7 @@ proc genProfile(c: var TForumData, ui: TUserInfo): string =
) )
) )
)) ))
result = htmlgen.`div`(id = "profile", result = htmlgen.`div`(id = "profile",
htmlgen.`div`(id = "left", result)) htmlgen.`div`(id = "left", result))
@ -797,7 +803,7 @@ include "main.tmpl"
proc prependRe(s: string): string = proc prependRe(s: string): string =
result = if s.len == 0: result = if s.len == 0:
"" ""
elif s.startswith("Re:"): s elif s.startswith("Re:"): s
else: "Re: " & s else: "Re: " & s
@ -852,7 +858,7 @@ routes:
case @"action" case @"action"
of "reply": of "reply":
let subject = getValue(db, let subject = getValue(db,
sql"select header from post where id = (select max(id) from post where thread = ?)", sql"select header from post where id = (select max(id) from post where thread = ?)",
$c.threadId).prependRe $c.threadId).prependRe
body = genPostsList(c, $c.threadId, count) body = genPostsList(c, $c.threadId, count)
cond count != 0 cond count != 0
@ -917,16 +923,16 @@ routes:
if (@"postid").len > 0: if (@"postid").len > 0:
parseInt(@"postid", c.postId, -1..1000_000) parseInt(@"postid", c.postId, -1..1000_000)
template finishLogin(): stmt = template finishLogin(): stmt =
setCookie("sid", c.userpass, daysForward(7)) setCookie("sid", c.userpass, daysForward(7))
redirect(uri("/")) redirect(uri("/"))
template handleError(action: string, topText: string, isEdit: bool): stmt = template handleError(action: string, topText: string, isEdit: bool): stmt =
if c.isPreview: if c.isPreview:
body.add genPostPreview(c, @"subject", @"content", body.add genPostPreview(c, @"subject", @"content",
c.userName, $getGMTime(getTime())) c.userName, $getGMTime(getTime()))
body.add genFormPost(c, action, topText, reuseText, reuseText, isEdit) body.add genFormPost(c, action, topText, reuseText, reuseText, isEdit)
resp genMain(c, body(), "Nim Forum - " & resp genMain(c, body(), "Nim Forum - " &
(if c.isPreview: "Preview" else: "Error")) (if c.isPreview: "Preview" else: "Error"))
post "/dologin": post "/dologin":
@ -1042,6 +1048,18 @@ routes:
resp genMain(c, "Failed to change the ban status of user.", resp genMain(c, "Failed to change the ban status of user.",
"Error - Nim Forum") "Error - Nim Forum")
get "/setpassword/?":
createTFD()
cond (@"nick" != "")
cond (@"pass" != "")
if not c.isAdmin:
resp genMain(c, "You cannot change this user's pass.", "Error - Nim Forum")
let res = setPassword(c, @"nick", @"pass")
if res:
resp genMain(c, "Success", "Nim Forum")
else:
resp genMain(c, "Failure", "Nim Forum")
const licenseRst = slurp("static/license.rst") const licenseRst = slurp("static/license.rst")
get "/license": get "/license":
createTFD() createTFD()
@ -1078,7 +1096,7 @@ routes:
if existsFile(path): if existsFile(path):
page = readFile(path) page = readFile(path)
else: else:
let basePath = let basePath =
if path[path.high] == '/': path & "index" if path[path.high] == '/': path & "index"
elif path.endsWith(".html"): path[-5 .. -1] elif path.endsWith(".html"): path[-5 .. -1]
else: path else: path
@ -1095,7 +1113,7 @@ when isMainModule:
docConfig = rstgen.defaultConfig() docConfig = rstgen.defaultConfig()
docConfig["doc.smiley_format"] = "/images/smilieys/$1.png" docConfig["doc.smiley_format"] = "/images/smilieys/$1.png"
math.randomize() math.randomize()
db = open(connection="nimforum.db", user="postgres", password="", db = open(connection="nimforum.db", user="postgres", password="",
database="nimforum") database="nimforum")
isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " & isFTSAvailable = db.getAllRows(sql("SELECT name FROM sqlite_master WHERE " &
"type='table' AND name='post_fts'")).len == 1 "type='table' AND name='post_fts'")).len == 1
@ -1103,9 +1121,9 @@ when isMainModule:
if paramCount() > 0: if paramCount() > 0:
if paramStr(1) == "scgi": if paramStr(1) == "scgi":
http = false http = false
#run("", port = TPort(9000), http = http) #run("", port = TPort(9000), http = http)
runForever() runForever()
db.close() db.close()