Compare commits

..

No commits in common. "1930ade3f7652c5c350af9e2eff1d70a5beac1e8" and "9d2c95bb33b67f7bed34110888ff50f28bdb8f31" have entirely different histories.

65 changed files with 575 additions and 2398 deletions

3
.gitignore vendored
View File

@ -5,6 +5,3 @@ smr.so
assets.h
cert
kore_chroot/*
conf/smr.conf
src/lua/config.lua
src/pages/error.etlua

View File

@ -6,9 +6,7 @@ COPY=cp
RM=rm -f
SPP=spp
CD=cd
AWK=awk
GREP=grep
SORT=sort
# Config
chroot_dir=kore_chroot/
@ -21,8 +19,7 @@ user=robin
port=8888
domain=test.monster:$(port)
SPPFLAGS=-D port=$(port) -D kore_chroot=$(chroot_dir) -D chuser=$(user) -D domain=$(domain)
# squelch prints, flip to print verbose information
#squelch prints
Q=@
#Q=
@ -41,34 +38,28 @@ part_files=$(in_part_files:%.in=%) $(shell find src/pages/parts/*.etlua -type f)
built_pages=$(page_files:src/pages/%.etlua=$(chroot_dir)pages/%.etlua)
built_sql=$(sql_files:src/sql/%.sql=$(chroot_dir)sql/%.sql)
built=$(built_files) $(built_sql) $(built_pages) $(built_tests)
asset_in_files=$(wildcard assets/*.in -type f)
asset_files=$(asset_in_files:%.in=%)
help: ## Print this help
$(Q)$(GREP) -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | $(SORT) | $(AWK) 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}'
all: $(chroot_dir) smr.so $(built_files) $(built_pages) $(built_sql) ## Build and run smr in a chroot
all: $(chroot_dir) smr.so $(built_files) $(built_pages) $(built_sql)
$(Q)$(ECHO) "[running] $@"
$(Q)$(KODEV) run
conf/smr.conf : conf/smr.conf.in Makefile
$(Q)$(ECHO) "[preprocess] $@"
$(Q)$(SPP) -o $@ $(SPPFLAGS) $<
$(Q)$(SPP) -o $@ -D port=$(port) -D kore_chroot=$(chroot_dir) -D chuser=$(user) $<
apk-tools-static-$(version).apk:
# wget -q $(mirror)latest-stable/main/$(arch)/apk-tools-static-$(version).apk
clean: ## clean up all the files generated by this makefile
clean:
$(Q)$(ECHO) "[clean] $@"
$(Q)$(KODEV) clean
$(Q)$(RM) $(page_files)
$(Q)$(RM) conf/smr.conf
$(Q)$(RM) src/pages/parts/story_breif.etlua
$(Q)$(RM) src/lua/config.lua
$(Q)$(RM) $(asset_files)
cloc: ## calculate source lines of code in smr
cloc --force-lang="HTML",etlua.in src assets
cloc:
cloc src
$(chroot_dir): apk-tools-static-$(version).apk
$(Q)$(MKDIR) $(chroot_dir)
@ -77,6 +68,31 @@ $(chroot_dir): apk-tools-static-$(version).apk
$(Q)$(MKDIR) $(chroot_dir)/data
$(Q)$(MKDIR) $(chroot_dir)/data/archive
$(Q)$(MKDIR) $(chroot_dir)/endpoints
#cd $(chroot_dir) && tar -xvzf ../apk-tools-static-*.apk
#cd $(chroot_dir) && sudo ./sbin/apk.static -X $(mirror)latest-stable/main -U --allow-untrusted --root $(chroot_dir) --no-cache --initdb add alpine-base
#ln -s /dev/urandom $(chroot_dir)/dev/random #Prevent an attacker with access to the chroot from exhausting our entropy pool and causing a dos
#ln -s /dev/urandom $(chroot_dir)/dev/urandom
#mount /dev/ $(chroot_dir)/dev --bind
#mount -o remount,ro,bind $(chroot_dir)/dev
#mount -t proc none $(chroot_dir)/proc
#mount -o bind /sys $(chroot_dir)/sys
#cp /etc/resolv.conf $(chroot_dir)/etc/resolv.conf
#echo "$(mirror)/$(branch)/main" > $(chroot)/etc/apk/repositories
#echo "$(mirror)/$(branch)/community" >> $(chroot)/etc/apk/repositories
#cp /etc/apk/repositories $(chroot_dir)/etc/apk/repositories
#mkdir $(chroot_dir)/var/sm
## Things to build lua libraries
#chroot $(chroot_dir) apk add luarocks5.1 sqlite sqlite-dev lua5.1-dev build-base zlib zlib-dev
#chroot $(chroot_dir) luarocks-5.1 install etlua
#chroot $(chroot_dir) luarocks-5.1 install lsqlite3
#chroot $(chroot_dir) luarocks-5.1 install lpeg
#chroot $(chroot_dir) luarocks-5.1 install lua-zlib ZLIB_LIBDIR=/lib #for some reason lzlib looks in /usr/lib for libz, when it needs to look at /lib
## Once we've built + installed everything, delete extra stuff from the chroot
#chroot $(chroot_dir) apk del sqlite-dev lua5.1-dev build-base zlib-dev
## SSL certificates, if you don't trust EFF (they have an antifa black block member as their favicon at time of writing) you may want to replace this.
#chroot $(chroot_dir) apk add certbot
## After chroot, apk add luarocks5.1 sqlite sqlite-dev lua5.1-dev build-base
## After chroot, luarocks install etlua; luarocks install lsqlite3
code : $(built_files)
@ -90,15 +106,15 @@ $(built_pages): $(chroot_dir)pages/%.etlua : src/pages/%.etlua
src/lua/config.lua : src/lua/config.lua.in Makefile
$(Q)$(ECHO) "[preprocess] $@"
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
$(Q)$(SPP) -o $@ -D domain=$(domain) $<
$(page_files) : % : %.in $(part_files)
$(Q)$(ECHO) "[preprocess] $@"
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
$(Q)$(SPP) -o $@ $<
src/pages/parts/story_breif.etlua : src/pages/parts/story_breif.etlua.in
$(Q)$(ECHO) "[preprocess] $@"
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
$(Q)$(SPP) -o $@ $<
$(built_sql): $(chroot_dir)sql/%.sql : src/sql/%.sql
$(Q)$(ECHO) "[copy] $@"
@ -108,19 +124,9 @@ $(built_tests) : $(chroot_dir)% : %
$(Q)$(ECHO) "[copy] $@"
$(Q)$(COPY) $^ $@
$(asset_files) : % : %.in
$(Q)$(ECHO) "[preprocess] $@"
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
smr.so : $(src_files) conf/smr.conf conf/build.conf $(asset_files)
smr.so : $(src_files) conf/smr.conf conf/build.conf
$(Q)$(ECHO) "[build] $@"
$(Q)$(KODEV) build
test : $(built) ## run the unit tests
$(Q)$(CD) kore_chroot && busted -v --no-keep-going #--exclude-tags slow
cov : $(built) ## code coverage (based on unit tests)
$(Q)$(RM) kore_chroot/luacov.stats.out
$(Q)$(CD) kore_chroot && busted -v -c --no-keep-going #--exclude-tags slow
$(Q)$(CD) kore_chroot && luacov endpoints/
$(Q)$(ECHO) "open kore_chroot/luacov.report.out to view coverage results."
test : $(built)
$(Q)$(CD) kore_chroot && busted

View File

@ -6,26 +6,8 @@ This repository contains the source code to a pastebin clone. It was made after
concerns with pastebin.com taking down certain kinds of content. SMR aims to
be small, fast, and secure. It is built on top of [Kore](https://kore.io), using
[luajit](https://luajit.org) to expose a Lua programming environment. It uses
[sqlite3](https://sqlite.org) as it's database. SMR is implemented in about
4k SLOC and is expected to never exceed 5k SLOC. Contributions welcome.
```
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Lua 36 190 306 1993
C 4 61 116 709
HTML 18 21 0 561
SQL 36 6 35 266
JavaScript 3 19 21 203
CSS 3 4 8 73
C/C++ Header 4 3 0 46
-------------------------------------------------------------------------------
SUM: 104 304 486 3851
-------------------------------------------------------------------------------
```
[sqlite3](https://sqlite.org) as it's database. SMR is implemented in just over
2k SLOC and is expected to never exceed 5k SLOC. Contributions welcome.
## Roadmap
@ -33,21 +15,12 @@ SUM: 104 304 486 3851
* Comments (complete)
* Tags (complete)
* Search (complete)
* Archive (complete)
* Author biographies (complete)
* Kore 4.2.0 (complete)
* addon api
* Author biographies
* Archive
TODO's:
* Currently, people can post comments to unlisted stories even if they don't have
Currently, people can post comments to unlisted stories even if they don't have
the correct link.
* Find a replacement preprocessor
* The archive is currently generated weekly from a cron job, and served
syncronously. We can generate a zip file on-the-fly instead, and if the client
disconnects, it's fine to drop the whole thing.
* We can simplify a lot of error handling logic by setting sql prepared statements to reset during error unwinding.
* We can simplify a lot of business logic by having requests parse their parameters eagerly.
## Hacking
@ -64,9 +37,9 @@ If you want to contribute to this repository:
but everything should still work with later versions.
6. Install [spp](https://github.com/radare/spp)
7. Clone this repository into the smr folder, cd into the root, and run `make`!
* You may need to modify the configuration in the Makefile, add `test.monster 127.0.0.1` to your `/etc/hosts`, modify command invocation, ect.
* You may need to modify the configuration in the Makefile, add test.monster 127.0.0.1 to your `/etc/hosts`, modify command invocation, ect.
## Misc. notes
## Misc notes.
SMR requires a slightly modified version of Kore to run. See [my kore patches](https://git.fuwafuwa.moe/rmalley/kore_patches)
for the changes I needed to make to get the JIT compiler playing nice with

View File

@ -1,12 +0,0 @@
/*
Stores the scroll location on a story to local storage, and re-scroll to the
position next time the page is loaded.
*/
window.onbeforeunload = function(e) {
localStorage.setItem(window.location.pathname,window.scrollY)
}
document.addEventListener("DOMContentLoaded", function(e) {
var scrollpos = localStorage.getItem(window.location.pathname)
if(scrollpos)
window.scrollTo(0,scrollpos)
})

View File

@ -1,36 +0,0 @@
/*
There's a delete buttotn to delete a post. If javascript is enabled, replace
the button with one that will ask for confirmation before deleting.
*/
function delete_intervine(){
var forms = document.getElementsByTagName("form");
if(forms.length == 0){return;}//Don't load if the story is missing.
var delete_form;
for(var i = 0; i < forms.length; i++){
if(forms[i].action.endsWith("_delete")){
delete_form = forms[i];
break;
}
}
if(delete_form == null){return;}//Don't load if we're not logged in
var delete_parent = delete_form.parentNode;
delete_parent.removeChild(delete_form);
var delete_wrapper = document.createElement("div");
var delete_button = document.createElement("button");
delete_button.classList.add("button");
delete_button.classList.add("column");
delete_button.classList.add("column-0");
delete_button.textContent = "Delete";
delete_button.addEventListener("click",function(){
if(confirm("Are you sure you want to delete this story?")){
document.documentElement.appendChild(delete_form);
delete_form.submit();
}
});
delete_parent.appendChild(delete_wrapper);
delete_wrapper.appendChild(delete_button);
}
document.addEventListener("DOMContentLoaded",delete_intervine,false);

View File

@ -9,7 +9,6 @@ body{
}
h1,h2,h3{line-height:1.2}
p,.tag-list{margin-bottom:0px}
.spacer{margin-bottom:1em}
.spoiler,.spoiler2{background:#444}
.spoiler:hover,.spoiler2:hover{color:#FFF}
.greentext{color:#282}
@ -32,7 +31,6 @@ p,.tag-list{margin-bottom:0px}
}
.column-0{margin-right:5px}
.label-inline{margin:0.5rem}
.biography{border:1px solid #9b4dca}
@media (prefers-color-scheme: dark){
body, input, select, textarea, pre, code{

View File

@ -1,32 +0,0 @@
.tag-suggestion, .tag-suggestion>input {
height: 1rem !important;
margin:0px;
}
.tag-suggestion{
font-size:0.8rem !important;
display:block !important;
}
.tag-suggestion>input{
line-height:1rem !important;
width:100% !important;
text-align:left;
background-color:transparent;
color:black;
border:none;
padding:0px;
}
.tag-suggestion-list{
list-style: none;
margin-top:3.8rem;
background: white;
border: 1px solid black;
border-top: 0px;
}
@media (prefers-color-scheme: dark){
body, input, select, textarea, pre, code, .tag-suggestion-list{
background: #1c1428;
color: #d0d4d8 !important;
}
.spoiler, .spoiler2{color:#444;}
}

View File

@ -1,41 +1,11 @@
console.log("Hello, world!");
/*Singleton object*/
var tag_suggestion_list = {
input_el: null,
list_element: document.createElement('ol'),
tag_suggestion_list = {
list_element: null,
suggestion_elements: [],
hover_last: -1,
}
tag_suggestion_list.list_element.setAttribute("class","tag-suggestion-list");
tag_suggestion_list.list_element.setAttribute("style","position:absolute;");
function appendTag(name){
return function(event){
var ie = tag_suggestion_list.input_el;
var prev = ie.value.split(";");
prev.pop();
prev.push(name);
ie.value = prev.join(";");
ie.value += ";";
ie.focus();
tag_suggestion_list.list_element.style = "display:none;";
}
}
function hoverTag(name, root){
return function(event){
var ie = tag_suggestion_list.input_el;
if(ie.value.slice(-1) == ";"){//comming from another tab completion
var prev = ie.value.slice(hover_last);
}else{
var prev = ie.value.split(";");
prev.pop()
prev.push(name)
ie.value = prev.join(";");
}
}
}
var lel = document.createElement('div');
/**
* Stolen from medium.com/@jh3y
@ -49,23 +19,37 @@ function getCursorXY(input, selectionPoint){
offsetLeft: inputX,
offsetTop: inputY,
} = input
// create a dummy element that will be a clone of our input
const div = document.createElement('div')
// get the computed style of the input and clone it onto the dummy element
const copyStyle = getComputedStyle(input)
for (const prop of copyStyle) {
div.style[prop] = copyStyle[prop]
}
// we need a character that will replace whitespace when filling our dummy element if it's a single line <input/>
const swap = '.'
const inputValue = input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value
// set the div content to that of the textarea up until selection
const textContent = inputValue.substr(0, selectionPoint)
// set the text content of the dummy element div
div.textContent = textContent
if (input.tagName === 'TEXTAREA') div.style.height = 'auto'
// if a single line input then the div needs to be single line and not break out like a text area
if (input.tagName === 'INPUT') div.style.width = 'auto'
// create a marker element to obtain caret position
const span = document.createElement('span')
// give the span the textContent of remaining content so that the recreated dummy element is as close as possible
span.textContent = inputValue.substr(selectionPoint) || '.'
// append the span marker to the div
div.appendChild(span)
// append the dummy element to the body
document.body.appendChild(div)
// get the marker position, this is the caret position top and left relative to the input
const { offsetLeft: spanX, offsetTop: spanY } = span
// lastly, remove that dummy element
// NOTE:: can comment this out for debugging purposes if you want to see where that span is rendered
document.body.removeChild(div)
// return an object with the x and y of the caret. account for input positioning so that you don't need to wrap the input
return {
x: inputX + spanX,
y: inputY + spanY,
@ -73,86 +57,33 @@ function getCursorXY(input, selectionPoint){
}
function display_suggestions(elem, sugg, event){
console.log("sugg:",sugg);
//Check that the value hasn't change since we fired
//off the request
var tags_so_far = elem.value.split(";");
recent = elem.value.split(";").pop().trim();
if(recent == sugg[0]){
var v = getCursorXY(elem,elem.value.length);
var sugx = v.x;
var sugy = v.y;
var sty = `position:absolute; margin-left:${sugx}px;`;
tag_suggestion_list.list_element.style = sty;
for(var i in tag_suggestion_list.suggestion_elements){
tag_suggestion_list.list_element.removeChild(tag_suggestion_list.suggestion_elements[i]);
}
tag_suggestion_list.suggestion_elements = [];
var hover_last = 0;
for(var i in tags_so_far){
hover_last += tags_so_far[i].length + 1;
}
tag_suggestion_list.hover_last = hover_last;
var sugx, sugy = getCursorXY(elem,elem.value.length);
console.log("Looking at position to display suggestions:",sugx, sugy);
for(var i in sugg){
if(i == 0){
continue;
}
var suggestion_el = document.createElement("li");
var suggestion_but = document.createElement("input")
suggestion_el.appendChild(suggestion_but);
suggestion_but.setAttribute("type","button");
suggestion_but.setAttribute("value",sugg[i]);
suggestion_el.setAttribute("class"," button-clear tag-suggestion");
tag_suggestion_list.list_element.appendChild(suggestion_el);
tag_suggestion_list.suggestion_elements.push(suggestion_el);
suggestion_but.onkeyup = function(event){
if(event.key == "Tab"){
hoverTag(event.target.value)(event);
}else if(event.key == ";"){
appendTag(event.target.value)(event);
}
}
suggestion_but.onclick = function(event){
appendTag(event.target.value)(event);
}
suggestion_but.onblur = function(event){
var other_input = false;
for(var i in tag_suggestion_list.suggestion_elements){
if(tag_suggestion_list.suggestion_elements[i].firstChild == event.relatedTarget){
other_input = true;
break;
}
}
if(!other_input){
tag_suggestion_list.list_element.style = "display:none;";
}
}
}
if(tag_suggestion_list.suggestion_elements.length > 0){
var last_element = tag_suggestion_list.suggestion_elements[tag_suggestion_list.suggestion_elements.length - 1];
//last_element.firstChild.last_element = true;
last_element.firstChild.onblur = function(event){
tag_suggestion_list.suggestion_elements[0].firstChild.focus();
}
console.log("Displaying suggestion:",sugg[i]);
lel.setAttribute('style',`left: $(sugx)px; top: $(sugy)px;`);
}
}
}
function hint_tags(elem, event){
//Get the most recent tag
var recent = elem.value.split(";").pop().trim();
recent = elem.value.split(";").pop().trim();
if(recent.length > 0){
console.log("Most recent tag:",recent);
//Ask the server for tags that look like this
var xhr = new XMLHttpRequest();
xhr = new XMLHttpRequest();
xhr.open("GET", "/_api?call=suggest&data=" + recent);
xhr.onreadystatechange = function(e){
if(xhr.readyState === 4){
console.log("Event:",e);
suggestions = xhr.response.split(";");
console.log("suggestions:",suggestions);
display_suggestions(elem,suggestions, event);
}
@ -162,34 +93,13 @@ function hint_tags(elem, event){
}
function init(){
var head_el = document.head;
var extra_css_el = document.createElement("link");
document.head.appendChild(extra_css_el);
extra_css_el.setAttribute("rel","stylesheet");
extra_css_el.setAttribute("href","/_css/suggest_tags.css");
var tag_el_list = document.getElementsByName("tags");
tag_el_list = document.getElementsByName("tags");
console.assert(tag_el_list.length == 1);
var tag_el = tag_el_list[0];
tag_suggestion_list.input_el = tag_el;
tag_el = tag_el_list[0];
tag_el.onkeyup = function(event){
console.log("Looking at tag:", event);
console.log("And element:",tag_el);
hint_tags(tag_el, event);
}
tag_el.onblur = function(event){
var not_suggestion = true;
var ies = tag_suggestion_list.suggestion_elements;
for(var i in ies){
if(event.relatedTarget == ies[i].firstChild){
not_suggestion = false;
break;
}
}
if(not_suggestion){
tag_suggestion_list.list_element.style = "display:none;";
}
}
var fieldset = tag_el.parentNode;
fieldset.appendChild(tag_suggestion_list.list_element);
var paste_el = document.getElementsByName("tags");
}
document.addEventListener("DOMContentLoaded",init,false);

View File

@ -26,7 +26,7 @@ dev {
# These flags are added to the shared ones when
# you build the "dev" flavor.
ldflags=-llua5.1
cflags=-g -Wall -Wextra -Werror
cflags=-g -Wextra
cflags=-I/usr/include/lua5.1
cxxflags=-g -Wextra
}

View File

@ -6,21 +6,14 @@ server tls {
}
seccomp_tracing yes
privsep worker {
runas <{get chuser }>
root <{get kore_chroot }>
}
privsep keymgr {
runas <{get chuser }>
root .
}
load ./smr.so
root <{get kore_chroot}>
runas <{get chuser }>
keymgr_runas <{get chuser }>
keymgr_root .
workers 1
http_body_max 8388608
@ -45,169 +38,83 @@ domain * {
#I run kore behind a lighttpd reverse proxy, so this is a bit useless to me
accesslog /dev/null
route / {
handler home
methods get
}
route /_css/style.css {
handler asset_serve_style_css
methods get
}
route /_css/suggest_tags.css {
handler asset_serve_style_css
methods get
}
route /_css/milligram.css {
handler asset_serve_milligram_css
methods get
}
route /_css/milligram.min.css.map {
handler asset_serve_milligram_min_css_map
methods get
}
route /_faq {
handler asset_serve_faq_html
methods get
}
route /_js/suggest_tags.js {
handler asset_serve_suggest_tags_js
methods get
}
route /_js/bookmark.js {
handler asset_serve_bookmark_js
methods get
}
route /_js/intervine_deletion.js {
handler asset_serve_intervine_deletion_js
methods get
}
route /favicon.ico {
handler asset_serve_favicon_ico
methods get
}
route /_paste {
handler post_story
methods get post
validate post title v_any
validate post text v_any
validate post pasteas v_subdomain
validate post markup v_markup
validate post tags v_any
validate post unlisted v_checkbox
}
route /_edit {
handler edit_story
methods get post
validate qs:get story v_storyid
validate post title v_any
validate post story v_storyid
validate post text v_any
validate post pasteas v_subdomain
validate post markup v_markup
validate post tags v_any
validate post unlisted v_checkbox
}
route /_bio {
handler edit_bio
methods get post
validate post text v_any
validate post author v_subdomain
}
route /_login {
handler login
methods get post
validate post user v_subdomain
validate post pass v_any
}
route /_logout {
handler logout
methods get
}
route ^/_claim {
handler claim
methods get post
validate post user v_subdomain
}
route /_download {
handler download
methods get
validate qs:get story v_storyid
validate qs:get pwd v_hex_128
}
route /_preview {
handler preview
methods post
validate post title v_any
validate post text v_any
validate post pasteas v_subdomain
validate post markup v_markup
validate post tags v_any
validate post unlisted v_checkbox
}
route /_search {
handler search
methods get
validate qs:get q v_any
}
route /_archive {
handler archive
methods get
validate qs:get t v_time
}
route /_api {
handler api
methods get
validate qs:get call v_any
validate qs:get data v_any
}
route /_delete {
handler delete
methods post
validate post story v_storyid
}
route / home
route /_css/style.css asset_serve_style_css
route /_css/milligram.css asset_serve_milligram_css
route /_css/milligram.min.css.map asset_serve_milligram_min_css_map
route /_faq asset_serve_faq_html
route /_js/suggest_tags.js asset_serve_suggest_tags_js
route /favicon.ico asset_serve_favicon_ico
route /_paste post_story
route /_edit edit_story
route /_bio edit_bio
route /_login login
route /_logout logout
route ^/_claim claim
route /_download download
route /_preview preview
route /_search search
route /_archive archive
route /_api api
# Leading ^ is needed for dynamic routes, kore says the route is dynamic if it does not start with '/'
route ^/[^_].* {
handler read_story
methods get post
route ^/[^_].* read_story
validate qs:get comments v_bool
validate qs:get pwd v_hex_128
validate post text v_any
validate post postas v_subdomain
validate post pwd v_hex_128
params get /_edit {
validate story v_storyid
}
params get /_download {
validate story v_storyid
validate pwd v_hex_128
}
params post /_edit {
validate title v_any
validate story v_storyid
validate text v_any
validate pasteas v_subdomain
validate markup v_markup
validate tags v_any
validate unlisted v_checkbox
}
params post /_paste {
validate title v_any
validate text v_any
validate pasteas v_subdomain
validate markup v_markup
validate tags v_any
validate unlisted v_checkbox
}
params post /_preview {
validate title v_any
validate text v_any
validate pasteas v_subdomain
validate markup v_markup
validate tags v_any
validate unlisted v_checkbox
}
params get /_search {
validate q v_any
}
params get /_archive {
validate t v_time
}
params get ^/[^_].* {
validate comments v_bool
validate pwd v_hex_128
}
params post ^/[^_].* {
validate text v_any
validate postas v_subdomain
validate pwd v_hex_128
}
params post /_login {
validate user v_subdomain
validate pass v_any
}
params post ^/_claim {
validate user v_subdomain
}
params get /_api {
validate call v_any
validate data v_any
}
}

View File

@ -1,118 +0,0 @@
_G.spy = spy
local mock_env = require("spec.env_mock")
local rng = require("spec.fuzzgen")
describe("smr biography",function()
setup(mock_env.setup)
teardown(mock_env.teardown)
it("should allow users to set their biography",function()
local claim_post = require("endpoints.claim_post")
local login_post = require("endpoints.login_post")
local index_get = require("endpoints.index_get")
local bio_get = require("endpoints.bio_get")
local bio_post = require("endpoints.bio_post")
local db = require("db")
local config = require("config")
config.domain = "test.host"
configure()
local username = rng.subdomain()
local claim_req = {
method = "POST",
host = "test.host",
path = "/_claim",
args = {
user = username
}
}
claim_post(claim_req)
local login_req = {
method = "POST",
host = "test.host",
path = "/_login",
args = {
user = username
},
file = {
pass = claim_req.response
}
}
login_post(login_req)
local cookie = login_req.response_headers["set-cookie"]
local sessionid = cookie:match("session=([^;]+)")
local home_req_get = {
method = "GET",
host = username .. ".test.host",
path = "/",
cookies = {
session = sessionid
}
}
index_get(home_req_get)
local edit_bio_button = '<a href="/_bio"'
assert(
home_req_get.response:find(edit_bio_button),
"After logging in the user should have a button to" ..
" edit their biography. Looking for " .. edit_bio_button
.. " but didn't find it in " .. home_req_get.response
)
local edit_bio_req_get = {
method = "GET",
host = username .. ".test.host",
path = "/_bio",
cookies = {
session = sessionid
},
args = {}
}
bio_get(edit_bio_req_get)
assert(edit_bio_req_get.responsecode == 200)
--[=[
local paste_req_post = {
method = "POST",
host = username .. ".test.host",
path = "/_paste",
cookies = {
session = sessionid
},
args = {
title = "post title",
text = "post text",
markup = "plain",
tags = "",
}
}
paste_post(paste_req_post)
for row in db.conn:rows("SELECT COUNT(*) FROM posts") do
assert(row[1] == 1, "Expected exactly 1 post in sample db")
end
local code = paste_req_post.responsecode
assert(code >= 300 and code <= 400, "Should receive a redirect after posting, got:" .. tostring(code))
assert(paste_req_post.response_headers, "Should have received some response headers")
assert(paste_req_post.response_headers.Location, "Should have received a location in response headers")
local redirect = paste_req_post.response_headers.Location:match("(/[^/]*)$")
local read_req_get = {
method = "GET",
host = username .. ".test.host",
path = redirect,
cookies = {
session = sessionid
},
args = {}
}
read_get(read_req_get)
local response = read_req_get.response
assert(
response:find([[post title]]),
"Failed to find post title in response."
)
assert(
response:find('By <a href="https://' .. username .. '.test.host">' .. username .. '</a>'),
"Failed to find the author name after a paste."
)
assert(
response:find([[post text]]),
"Failed to find post text in response."
)
]=]
end)
end)

View File

@ -1,120 +0,0 @@
_G.spy = spy
local mock_env = require("spec.env_mock")
describe("smr cacheing",function()
setup(mock_env.setup)
teardown(mock_env.teardown)
it("caches a page if the page is requested twice #working",function()
local read_get = require("endpoints.read_get")
local cache = require("cache")
renderspy = spy.on(cache,"render")
configure()
local req = {
method = "GET",
path = "/a",
args = {},
host = "test.host"
}
assert.spy(renderspy).called(0)
read_get(req)
assert.spy(renderspy).called(1)
read_get(req)
assert.spy(renderspy).called(2)
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
assert(row[1] == 1, string.format(
"Exepected only one cache entry after" ..
"calling test.host/a 2 times " ..
", but got %d rows.", row[1]
))
end
end)
it("does not cache the page if the user is logged in", function()
local read_get = require("endpoints.read_get")
local cache = require("cache")
renderspy = spy.on(cache,"render")
configure()
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
assert(row[1] == 0, string.format(
"Cache should not have any rows before " ..
"request have been made."
))
end
local req = mock_env.session()
req.method = "GET"
req.path = "/a"
req.args = {}
read_get(req)
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
assert(row[1] == 0, string.format(
"Cache should not cache requests made by " ..
"logged in users."
))
end
end)
it("caches one page for domain/id and author.domain/id",function()
local read_get = require("endpoints.read_get")
local cache = require("cache")
configure()
local req_m = {__index = {
method = "GET",
path = "/a",
args = {}
}}
local base_host = {host="test.host"}
local base_req = setmetatable({host="test.host"},req_m)
read_get(base_req)
local user_req = setmetatable({host="admin.test.host"},req_m)
read_get(user_req)
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
assert(row[1] == 1, string.format(
"Exepected only one cache entry for" ..
"'test.host/a' and 'admin.test.host/a'" ..
", but got %d rows.", row[1]
))
end
end)
it("detours configure",function()
local s = {}
local c = false
local oldconfigure = configure
--local index_get = require("endpoints.index_get")
--configure(s)
--assert(c)
end)
describe("author home page",function()
it("lists all stories by that author",function()
local read_get = require("endpoints.index_get")
local cache = require("cache")
configure()
local req_m = {__index = {
method = "GET",
path = "/a",
args = {}
}}
local base_host = {host="user.test.host"}
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
assert(row[1] == 0, string.format(
"Before requesting user homepage " ..
"there should not be any pages in the " ..
"cache."
))
end
local base_req = setmetatable({host="user.test.host"},req_m)
read_get(base_req)
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
assert(row[1] == 1, string.format(
"After reading the autor home page, " ..
" only that page should be cached."
))
end
read_get(base_req)
for row in cache.cache:rows("SELECT COUNT(*) FROM cache") do
assert(row[1] == 1, string.format(
"After reading the autor home page " ..
" twice only that page should be cached."
))
end
end)
end)
end)

View File

@ -1,249 +0,0 @@
local config = require("config")
config.db = "data/unittest.db"
local mock = {}
local env = {}
mock.env = env
--Mirror print prior to lua 5.4
--local oldprint = print
local ntostring
-- Modules that get required lazily
local login_post
local fuzzy
local claim_post
print_table= function(...)
print("Print called")
local args = {...}
local mapped_args = {}
for k,v in ipairs(args) do
print("mapping",v)
mapped_args[k] = ntostring(v)
end
print(table.concat(mapped_args,"\t"))
end
local tables_called = {}
function ntostring(arg)
io.stdout:write("Calling tostring with:",tostring(arg),"\n")
if type(arg) ~= "table" then
return tostring(arg)
end
local function tbl_to_string(tbl,indent)
if tables_called[tbl] then
return tostring(tbl)
end
tables_called[tbl] = true
if type(tbl) ~= "table" then
error("tbl_to_string must be called with a table, got a " .. type(tbl))
end
local lines = {string.rep("\t",indent) .. "{"}
for k, v in pairs(tbl) do
local kv = {}
for i,n in pairs{k,v} do
if type(n) == "table" then
kv[i] = string.format("%q",tbl_to_string(n,indent+1))
else
kv[i] = string.format("%q",tostring(n))
end
end
table.insert(
lines,
string.rep("\t",indent+1) .. kv[1] .. ":" .. kv[2]
)
end
table.insert(lines,string.rep("\t",indent) .. "}")
return table.concat(lines,"\n")
end
--It's a table
local ret = tbl_to_string(arg,0)
tables_called = {}
return ret
end
local smr_mock_env = {
--An empty function that gets called to set up databases and do other
--startup-time stuff, runs once for each worker process.
configure = spy.new(function(...) end),
http_request_get_host = spy.new(function(req) return req.host or "test.host" end),
http_request_get_path = spy.new(function(req) return req.path or "/" end),
http_request_populate_qs = spy.new(function(req) req.qs_populated = true end),
http_request_populate_post = spy.new(function(req) req.post_populated = true end),
http_populate_multipart_form = spy.new(function(req) req.post_populated = true end),
http_argument_get_string = spy.new(function(req,str)
assert(req.args,"requests should have a .args table")
assert(
req.method == "GET" and req.qs_populated or
req.method == "POST" and req.post_populated,[[
http_argument_get_string() can only be called after
the appropriate populate method has been called, either
http_request_populate_qs(req) or
http_request_populate_post(req)]]
)
return req.args[str]
end),
http_file_get = spy.new(function(req,filename)
assert(req.multipart_forum_populated,[[
http_file_get() can only be called after the approriate
populate method has been called. (http_populate_multipart_form())
]])
return req.file["pass"]
end),
http_response = spy.new(function(req,errcode,html)
req.responsecode = errcode
req.response = html
end),
http_response_header = spy.new(function(req,name,value)
req.response_headers = req.response_headers or {}
req.response_headers[name] = value
end),
http_method_text = spy.new(function(req) return req.method end),
http_populate_cookies = spy.new(function(req)
req.cookies_populated = true
req.cookies = req.cookies or {}
end),
http_request_cookie = spy.new(function(req,cookie_name)
assert(req.cookies_populated,[[
http_request_cookie() can only be called after
http_populate_cookies() has been called.
]])
return req.cookies[cookie_name]
end),
http_response_cookie = spy.new(function(req,name,value) req.cookies = {[name] = value} end),
log = spy.new(function(priority, message) --[[print(string.format("[LOG %q]: %s",priority,message))]] end),
--Logging:
LOG_DEBUG = "debug",
LOG_INFO = "info",
LOG_NOTICE = "notice",
LOG_WARNING = "warning",
LOG_ERR = "error",
LOG_CRIT = "critical",
LOG_ALERT = "alert",
LOG_EMERG = "emergency",
sha3 = spy.new(function(message) return "digest" end),
}
local smr_mock_env_m = {
__index = smr_mock_env,
__newindex = function(self,key,value)
local setter = debug.getinfo(2)
if setter.source ~= "=[C]" and key ~= "configure" then
error(string.format(
"Tried to create a global %q with value %s\n%s",
key,
tostring(value),
debug.traceback()
),2)
else
rawset(self,key,value)
end
end
}
local sfmt = string.format
local string_fmt_override = {
format = spy.new(function(fmt,...)
local args = {...}
for i = 1,#args do
if args[i] == nil then
args[i] = "nil"
end
end
table.insert(args,1,fmt)
return sfmt(unpack(args))
end)
}
setmetatable(string_fmt_override,{__index = string})
local smr_override_env = {
--Detour assert so we don't actually perform any checks
assert = spy.new(function(bool,msg,level) return bool end),
--Allow string.format to accept nil as arguments
string = string_fmt_override
}
mock.olds = {}
function mock.setup()
setmetatable(_G,smr_mock_env_m)
for k,v in pairs(smr_override_env) do
mock.olds[k] = _G[k]
_G[k] = v
end
end
function mock.mockdb()
local config = require("config")
config.db = "data/unittest.db"
assert(os.execute("rm " .. config.db))
package.loaded.db = nil
local db = require("db")
end
function mock.teardown()
setmetatable(_G,{})
for k,v in pairs(mock.olds) do
_G[k] = v
end
end
local session_m = {__index = {
login = function(self, who, pass)
if not self.args then
error("Request should have a .args table")
end
print("Right before requireing login_post endpoint, self.args is " .. tostring(self.args))
print("After requireing login_post edpoint, self.args is " .. tostring(self.args))
self.args.user = who
self.args.pass = pass
login_post(self)
error("TODO")
end,
logout = function(self)
error("TODO")
end,
req = function(self, args)
end
}}
function mock.session(tbl)
if post_login == nil then
login_post = require("endpoints.login_post")
fuzzy = require("spec.fuzzgen")
claim_post = require("endpoints.claim_post")
configure()
end
local username = fuzzy.subdomain()
local claim_req = {
method = "POST",
host = "test.host",
path = "/_claim",
args = {
user = username
}
}
claim_post(claim_req)
local login_req = {
method = "POST",
host = "test.host",
path = "/_login",
args = {
user = username
},
file = {
pass = claim_req.response
}
}
login_post(login_req)
local cookie = login_req.response_headers["set-cookie"]
local sessionid = cookie:match("session=([^;]+)")
local req = {
host = "test.host",
cookies = {
session = sessionid
}
}
return req, username
end
return mock

View File

@ -1,44 +0,0 @@
local rng = {}
function rng.markup() return math.random() > 0.5 and "plain" or "imageboard" end
function rng.generate_str(length,characters)
return function()
local t = {}
local rnglength = math.random(2,length)
for i = 1,rnglength do
local rngpos = math.random(#characters)
local rngchar = string.sub(characters,rngpos,rngpos)
table.insert(t,rngchar)
end
local ret = table.concat(t)
return ret
end
end
function rng.characters(mask)
local t = {}
for i = 1,255 do
if string.match(string.char(i), mask) then
table.insert(t,string.char(i))
end
end
return table.concat(t)
end
function rng.maybe(input,chance)
chance = chance or 0.5
if math.random() < chance then
return input
end
end
rng.any = rng.generate_str(1024,rng.characters("."))
rng.subdomain = rng.generate_str(30,rng.characters("[0-9a-z]"))
rng.storyname = rng.generate_str(10,"[a-zA-Z0-9$+!*'(),-]")
rng.storyid = function() return tostring(math.random(1,10)) end
rng.tags = function()
local tag_gen = rng.generate_str(10,"[%w%d ]")
local t = {}
for i = 1,10 do
table.insert(t,tag_gen())
end
return table.concat(t,";")
end
return rng

View File

@ -1,224 +0,0 @@
_G.spy = spy
local mock_env = require("spec.env_mock")
local rng = require("spec.fuzzgen")
describe("smr login",function()
setup(mock_env.setup)
teardown(mock_env.teardown)
it("should allow someone to claim an account",function()
mock_env.mockdb()
local claim_post = require("endpoints.claim_post")
configure()
claim_req = {
method = "POST",
host = "test.host",
path = "/_claim",
args = {
user = "user"
}
}
claim_post(claim_req)
assert(
claim_req.responsecode == 200,
"Login did not respond with a 200 code"
)
assert(
claim_req.response_headers,
"Login did not have response headers."
)
assert(
claim_req.response_headers["Content-Disposition"],
"Login did not have a Content Disposition header to set filename"
)
assert(
string.find(claim_req.response_headers["Content-Disposition"],"attachment"),
"Login did not mark passfile as an attachment"
)
assert(
claim_req.response_headers["Content-Disposition"]:find(".passfile"),
"Login did not name the returned file with the .passfile extension."
)
assert(
claim_req.response_headers["Content-Type"],
"Login did not respond with a Content-Type"
)
assert(
claim_req.response_headers["Content-Type"] == "application/octet-stream",
"Login did not mark Content-Type correctly (application/octet-stream)"
)
assert(
claim_req.response,
"Login did not return a passfile"
)
end)
it("should give a session cookie when logging in with a user",function()
local claim_post = require("endpoints.claim_post")
local login_post = require("endpoints.login_post")
local config = require("config")
local db = require("db")
local session = require("session")
configure()
local username = rng.subdomain()
local claim_req = {
method = "POST",
host = "test.host",
path = "/_claim",
args = {
user = username
}
}
claim_post(claim_req)
login_req = {
method = "POST",
host = "test.host",
path = "/_login",
args = {
user = username
},
file = {
pass = claim_req.response
}
}
sessionspy = spy.on(session,"start")
login_post(login_req)
assert.spy(sessionspy).was.called()
local code = login_req.responsecode
assert(
code >= 300 and code <= 400,
"Sucessful login should redirect the user, code:" .. tostring(code)
)
assert(
login_req.response_headers,
"Sucessful login should have response headers"
)
assert(
login_req.response_headers["set-cookie"],
"Sucessful login should set a cookie on the client"
)
local cookie = login_req.response_headers["set-cookie"]
local domain_noport = string.match(config.domain,"(.-):?%d*$")
assert(
string.find(cookie,"session="),
"Sucessful login should set a cookie named 'session'"
)
assert(
string.find(cookie,"Domain="..domain_noport),
"Cookies should only be set for the configured domain"
)
assert(
string.find(cookie,"HttpOnly"),
"Cookies should have the HttpOnly flag set"
)
assert(
string.find(cookie,"Secure"),
"Cookies should have the secure flag set"
)
assert(
login_req.response_headers["Location"],
"Sucessful login should redirect to a location"
)
assert(
login_req.response_headers["Location"] == "https://" .. username .. "." .. config.domain,
"Login redirect should get domain from config file"
)
end)
it("should allow logged in users the option of posting under their username",function()
local claim_post = require("endpoints.claim_post")
local login_post = require("endpoints.login_post")
local paste_get = require("endpoints.paste_get")
local paste_post = require("endpoints.paste_post")
local read_get = require("endpoints.read_get")
local db = require("db")
local config = require("config")
config.domain = "test.host"
configure()
local username = rng.subdomain()
local claim_req = {
method = "POST",
host = "test.host",
path = "/_claim",
args = {
user = username
}
}
claim_post(claim_req)
login_req = {
method = "POST",
host = "test.host",
path = "/_login",
args = {
user = username
},
file = {
pass = claim_req.response
}
}
login_post(login_req)
local cookie = login_req.response_headers["set-cookie"]
local sessionid = cookie:match("session=([^;]+)")
local paste_req_get = {
method = "GET",
host = username .. ".test.host",
path = "/_paste",
cookies = {
session = sessionid
}
}
paste_get(paste_req_get)
local option = '<option value="' .. username .. '">' .. username .. '</option>'
assert(
paste_req_get.response:find(option),
"After logging in the user should have an option to "..
"make posts as themselves. Looking for " .. option ..
" but didn't find it in " .. paste_req_get.response
)
local paste_req_post = {
method = "POST",
host = username .. ".test.host",
path = "/_paste",
cookies = {
session = sessionid
},
args = {
title = "post title",
text = "post text",
markup = "plain",
tags = "",
}
}
paste_post(paste_req_post)
for row in db.conn:rows("SELECT COUNT(*) FROM posts") do
assert(row[1] == 1, "Expected exactly 1 post in sample db")
end
local code = paste_req_post.responsecode
assert(code >= 300 and code <= 400, "Should receive a redirect after posting, got:" .. tostring(code))
assert(paste_req_post.response_headers, "Should have received some response headers")
assert(paste_req_post.response_headers.Location, "Should have received a location in response headers")
local redirect = paste_req_post.response_headers.Location:match("(/[^/]*)$")
local read_req_get = {
method = "GET",
host = username .. ".test.host",
path = redirect,
cookies = {
session = sessionid
},
args = {}
}
read_get(read_req_get)
local response = read_req_get.response
assert(
response:find([[post title]]),
"Failed to find post title in response."
)
assert(
response:find('By <a href="https://' .. username .. '.test.host">' .. username .. '</a>'),
"Failed to find the author name after a paste."
)
assert(
response:find([[post text]]),
"Failed to find post text in response."
)
end)
end)

View File

@ -251,7 +251,6 @@ describe("smr",function()
end)
it("should be named appropriately",function()
local f = assert(io.open("endpoints/"..fname .. ".lua","r"))
f:close()
end)
it("should run without errors",function()
require("endpoints." .. fname)
@ -264,9 +263,9 @@ describe("smr",function()
local pagefunc = assert(require("endpoints." .. fname))
assert(type(pagefunc) == "function")
end)
it("should call http_response() at some point #slow",function()
it("should call http_response() at some point",function()
local pagefunc = require("endpoints." .. fname)
for i = 1,10 do
for i = 1,1000 do
local req = {}
req.method = method
req.path = obj.route

View File

@ -1,9 +1,5 @@
function assertf(stmt, fmt, ...)
if not stmt then
error(string.format(fmt,...))
end
end
describe("smr imageboard parser #parsers",function()
describe("smr imageboard parser",function()
it("should load without error",function()
local parser = require("parser_imageboard")
end)
@ -13,149 +9,4 @@ describe("smr imageboard parser #parsers",function()
local output = parser(input)
assert(type(output) == "string","Expected string, got: %s",type(output))
end)
it("should spoiler text in asterisks ",function()
local parser = require("parser_imageboard")
local input = "Hello, **world**!"
local output = parser(input)
local expected = [[<p>Hello, <span class="spoiler">world</span>!</p> ]]
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
end)
it("should spoiler text in [spoiler] tags",function()
local parser = require("parser_imageboard")
local input = "Hello, [spoiler]world[/spoiler]!"
local output = parser(input)
local expected = [[<p>Hello, <span class="spoiler2">world</span>!</p> ]]
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
end)
it("should italicize words in double single quotes ('')",function()
local parser = require("parser_imageboard")
local input = "Hello, ''world''!"
local output = parser(input)
local expected = [[<p>Hello, <i>world</i>!</p> ]]
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
end)
it("should bold words in tripple single quotes (''')",function()
local parser = require("parser_imageboard")
local input = "Hello, '''world'''!"
local output = parser(input)
local expected = [[<p>Hello, <b>world</b>!</p> ]]
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
end)
it("should underline words in double underscores (__)",function()
local parser = require("parser_imageboard")
local input = "Hello, __world__!"
local output = parser(input)
local expected = [[<p>Hello, <u>world</u>!</p> ]]
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
end)
it("should make a heading out of things in double equals(==)",function()
local parser = require("parser_imageboard")
local input = "Hello, ==world==!"
local output = parser(input)
local expected = [[<p>Hello, <h2>world</h2>!</p> ]]
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
end)
it("should strikethrough words in double tildes (~~)",function()
local parser = require("parser_imageboard")
local input = "Hello, ~~world~~!"
local output = parser(input)
local expected = [[<p>Hello, <s>world</s>!</p> ]]
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
end)
it("should codify words in [code] tags",function()
local parser = require("parser_imageboard")
local input = "Hello, [code]world[/code]!"
local output = parser(input)
local expected = [[<p>Hello, <pre><code>world</code></pre>!</p> ]]
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
end)
it("should greentext lines that start with >",function()
local parser = require("parser_imageboard")
local input = "Hello,\n> world!"
local output = parser(input)
local expected = [[<p>Hello,</p> <p><span class="greentext">&gt; world!</span></p> ]]
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
end)
it("should pinktext lines that start with <",function()
local parser = require("parser_imageboard")
local input = "Hello,\n< world!"
local output = parser(input)
local expected = [[<p>Hello,</p> <p><span class="pinktext">&lt; world!</span></p> ]]
assertf(output == expected, "Expected\n%s\ngot\n%s\n", expected, output)
end)
it("should allow for bold+italic text",function()
local parser = require("parser_imageboard")
local input = "Hello,'''''world!'''''"
local output = parser(input)
local expected = [[<p>Hello,<i><b>world!</b></i></p> ]]
end)
local formatting = {
{"**","**"},
{"[spoiler]","[/spoiler]"},
{"''","''"},
{"'''","'''"},
{"__","__"},
{"==","=="},
{"~~","~~"},
{"[code]","[/code]"}
}
local formatting_line = {"> ", "< "}
for k,v in pairs(formatting) do
for i = 1, 50 do
it("should not break with " .. i .. " " .. v[1] .. " indicators in a row ",function()
local parser = require("parser_imageboard")
local input = "Hello, " .. string.rep(v[1],i) .. " world!"
local start_time = os.clock()
local output = parser(input)
local end_time = os.clock()
assert(end_time - start_time < 1, "Took too long")
end)
end
end
for i = 1, 50 do
it("Should withstand a random string of " .. i .. " formatters and words. ",function()
local parser = require("parser_imageboard")
local input = {}
local function random_text()
if math.random() > 0.5 then
return "Hello"
else
return "world"
end
end
local function random_wrap(text)
local rngwrap = formatting[math.random(#formatting)]
return rngwrap[1] .. text .. rngwrap[2]
end
local function random_text_recursive(i)
if i == 0 then
return ""
end
local j = math.random()
if j < 0.33 then
return random_text_recursive(i-1) .. random_wrap(random_text())
elseif j < 0.66 then
return random_wrap(random_text() .. random_text_recursive(i-1)) .. random_wrap(random_text())
else
return random_wrap(random_text() .. random_text_recursive(i - 1))
end
end
input = random_text_recursive(i)
local start_time = os.clock()
local output = parser(input)
local end_time = os.clock()
assert(end_time - start_time < 1, "Took too long")
end)
end
for _,file_name in ipairs{
"Beauty_and_the_Banchou_1"
} do
it("should parser " .. file_name,function()
local parser = require("parser_imageboard")
local input = require("spec.parser_tests." .. file_name)
local output = parser(input)
--print("output:",output)
end)
end
end)

View File

@ -1,79 +0,0 @@
_G.spy = spy
function assertf(stmt, fmt, ...)
if not stmt then
error(string.format(fmt,...))
end
end
local mock_env = require("spec.env_mock")
describe("smr search parser #parsers #working",function()
setup(mock_env.setup)
teardown(mock_env.teardown)
it("should load without error",function()
local parser = require("parser_search")
end)
it("should accept a string and return a string",function()
local parser = require("parser_search")
local input = "Hello, world!"
local output = parser(input)
assert(type(output) == "string","Expected string, got: %s",type(output))
end)
it("should parse a string into it's components",function()
local parser = require("parser_search")
local input = "+search +test +author=admin"
local search_tag, test_tag, author_parsed = false, false, false
local sql, ast = parser(input)
for _,v in pairs(ast.tags) do
if v[3] == "Search" then
search_tag = true
elseif v[3] == "Test" then
test_tag = true
end
end
for _,v in pairs(ast.author) do
if v[3] == "%admin%" then
author_parsed = true
end
end
assert(search_tag, "Search tag must be found")
assert(test_tag, "Test tag must be found")
assert(author_parsed, "Author tag must be found")
end)
it("should parse tags with a hyphen in the middle",function()
local parser = require("parser_search")
local input = "+post-modern"
local sql, ast = parser(input)
assert(#ast.tags == 1, "+post-modern should be one tag")
end)
it("should parse an empty string without errors",function()
local parser = require("parser_search")
local input = ""
local sql, ast = parser(input)
assert(sql,"Did not receive sql")
assert(ast,"Did not receive ast")
end)
it("should parse a hits request",function()
local parser = require("parser_search")
local input = "+hits>=0"
local sql, ast = parser(input)
assert(ast.hits, "should have a .hits table")
local hit = ast.hits[1]
assert(hit[1] == "+", "Failed to have an intersect constraint for hits, got " .. hit[1])
assert(hit[2] == ">=", "Failed to have a greater-than-or-equal constraint for hits, got " .. hit[2])
assert(hit[3] == 0, "Failed to find >=0 for hits, got " .. hit[3])
end)
it("should parse a title request", function()
local parser = require("parser_search")
local input = "+title=the balled of pala-al-din"
local sql, ast = parser(input)
assert(ast.title, "should have a .title table")
local title = ast.title[1]
assert(title[1] == "+", "Failed to have an intersect constraint for title, got " .. title[1])
assert(title[2] == "=", "Failed to have a like constraint for title, got " .. title[2])
assert(title[3] == "%the balled of pala-al-din%", "Failed to find title name, got " .. title[3])
end)
end)

View File

@ -1,105 +0,0 @@
return [==[
The angry dogs, sirens, and the occasional angry shout in the distance. A normal high schooler would be afraid to walk a rough road like this. The road to the local high-school, it's a testament to one's strength in itself. "Last Shot High" they call it, the school that takes on all the kids that aren't accepted at any other educational institution. That includes the massive bodybuilder-esque man in a school uniform walking down the road right now. His pants are baggy and held up by a studded belt around his waist with a chain dangling on his leg. His blazer is modified to hang down to his knees, his long pompadour stands strong against the wind. Who is this menace to society? This rebellious youth?
He is Tankaroshi Ryuji, the ones who orbit him call him Tank or Tank-sama. And for who he is? He's the second year banchou of Last Shot High.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
I open the front door of the school and march past the lockers to the stairway. A group of first years are squatting at the bottom and keep their heads down as I pass. I climb the stairs to the second year floor. When I pass one of the science rooms I can hear jeers and sounds of fighting. The door opens and a teacher hustles out, deciding it's best to go somewhere else and wait for it to blow over. The usual scenes that play out before me cause me to daze out before I realize I've reached my destination. The old Art room. It's long since been abandoned. After all, not much use for an art room when most of the kids here have never even picked up a pencil. So now it's the hangout for me and anyone who associates with me. As I reach for the doorknob a sound catches my attention, the sound of a feminine yelp. I stand there with my hand on the door, listening for any familiar voices in the mix. All I can hear is a few guys and that one voice.
"Hmm..." Situations like this are tricky, this isn't exactly a school with damsels in distress, usually when I try to help some girl out it turns out she started the fight and now I'm the asshole for interfering. But as usual I can't keep myself from it, I'm a man after all. I walk down the hall and turn the corner, from the back it looks like three third years are gathered around someone.
"Oi!" I yell at the three seniors who turn towards me.
"EH?! Well if isn't the big bad Tankaroshi-kun." The tall one seems to be the leader here as his cronies laugh.
"Picking on a girl in my hallway eh? I hope you weren't planning on getting out of here alive." I slide my blazer off and set it to the side. A freezing wind blows right through my tank top and bites at my arms, I can't help but shiver lightly. Jesus we can't afford heat anymore?
"Oh!? Is that right? Look at the little second year shivering in his britches." They shove someone behind them as they saunter up to me.
The tallest one stops right in front of me and looks up. He starts to poke my chest as he talks to me. "You think just because you're some roided up gorilla with half a foot on us you can take all three of us? How about you just go back to your classroo-"
I cut him off my grabbing his finger with my hand and lifting him up off the ground by it, his feet flailing wildly under him. I rear my head back and slam my forehead into his shocked face. Quickly, I throw him onto the mook on the left before turning my attention to the senior on my right. He swings his arm into my frame but it stops like he had just punched a wall. I grab onto his hair and swing him face first into the wall beside me.
"Ora!" The leader had gotten back up and tried to tackle me in the back. I stumble a foot forward and drop my elbow backwards on top of him, slamming him straight down into the linoleum. I turn around to see the last one staring at me in shock.
"Well come on senior, teach me something." I sneer at him. And with that the fight is over, he ran away as his "friend" is clutching his face on the ground beside the wall. I take a step and kick the leader in the stomach. "Ora! You thought you could take me?! You're a thousand years too early!" He grabs his stomach and I throw my hands into my pockets, then turn to walk away.
"W-Wait please!" the sound of wood against tile approaches me at the sound of the feminine voice. Oh right, I did come here to save some chick huh?
I turn around to see a slender, tall girl wearing wooden sandals, white socks, and a long white kimono. Oh it's her.
"You're Tankaroshi-san Ryuji." She asks smiling.
I look her up and down, up to this point I've got glempses of her but haven't gotten to really see her up close yet.
I reach my hand out towards her cheek and pluck a frozen tear off of her and flick it down the hallway. "Yeah, Fubuki right?"
Her cheeks turn a dark shade of blue, "Oh I didn't know you knew me...and you're using my first name too..."
Hard not to, she sticks out more than any other person here. The new kid Tsuma Fubuki, and she's a Yuki-Onna.Now, monster girls aren't too common outside of the large metropolises, even a big city like ours seeing one is a rare sight. But even still that's not the only reason she sticks out.
She wears long outdated traditional garb around. (The girls here wouldn't be caught in anything so lady-like)
She scores the highest in all of the school. (Not a hard achievement by any means, but they rival even outside schools as well.)
And she's probably the only one here who wont end up dead in the streets or in the Yakuza.
Hell she's a model student, I'd say the only reason she ended up here is because no school wants a ghost haunting their halls.
We sit there in silence for a a few minutes as we awkwardly stare at each other. Her white kimono grips all the right spots, her hourglass figure and modest bust accented by it. Her face is spotless and pure as snow. Her light blue skin and white, almost dead looking eyes are pretty in a way. Whitish blue hair hangs down to her lower back right above her Kimono's sash. Well this conversation is going nowhere.
"Right...bye." I turn around with my hands in my pocket and walk away.
"Oh! Please wait!"
I sigh and turn back around.
"I wanted to thank you for saving me." She gives me a deep bow at the waist and holds it for an uncomfortable amount of time.
"Right...bye." I turn around and take a step forward before I feel something on the back of my arm.
"Tankaroshi-san!" Her small hand tries to grip the back of my arm. It felt icy cold at first but has quickly grown to a comforting coolness. After she notices me staring at her hand she takes it back and folds her hands into her lap.
"Tankaroshi-san!"
I pinch the bridge of my nose, "Just call me tank or Tankaroshi if you'd rather. Drop the san."
She nods and continues, "Tankaroshi! What do you like to eat?"
"Meat. Protein." I answer her out of left field question.
She nods her head and looks at me with a look of fiery determination, "From now on I will make you lunch! Chicken and Beef!" She bows deeply and walks off down the hall, she walks so elegantly she seems to glide over the linoleum floors.
I shrug and go back to the Art Room.
"Please accept this!" Fubuki hands me a small box. It's of course cold to the touch, like it has been for the last week. I crack open the lid and pick out a brownish ice cube with my fingers, I toss it into my mouth. I never thought I could get brain freeze from fried chicken.
"Well how is it?" She studies my face as I crunch through the ice.
"It's...homemade."
She nods her head and keeps staring; I guess she wasn't satisfied with that answer.
I sigh, "Well it's the thought that counts Fubuki, you don't have to keep doing this we're even now you can forget about last week."
She cocks her head at me, "Hmm? OH! I'm not cooking for you because of that!" She hides her mouth as she giggles. I guess I'm left out of the loop here. "A woman cooks for her husband right?"
The new ice cube I had thrown in my mouth now slowly slides out from my lips and falls back into the box with a clatter.
"Uh...yeah." I raise my brow and lean back from the box. "So what does that have to do with this situation here?"
"Oh I've taking a liking to you! So now we're married." She leans in and tries to wrap herself around my arm, she can just barely do it. Uh...I've never been confessed to before. Is this a confession? Is this how they work? And she's confessing to me? I'm terrible with the ladies, I'm rough, they say I'm way too muscly and big, and the worst part, they say my face is scary. I'm not going around trying to make my face scary dammit!
Her cold body feels comfortable on my arm even in the middle of fall, I've always had a high body temperature so I've grown fond of her touch over the past week, but now I kind of feel self conscious about it.
"So married now huh? I don't think that's really how it works-"
"Oh maybe not for you humans but for Yuki-Onnas it is." She cuts me off. "Yeah I saw you walking down the hall one day and took an interest in you. You saving me is what made me decide to keep you." She smiles as if she's remembering a good childhood memory.
Yeah, this isn't a proposal this is a fucking notice of marriage.
"You know we haven't even been on a date yet, right?"
She jumps in the bench we're sitting in, "Oh! Yes let's go on a date!" She claps as she pesters me about where we're going.
Man I want to say she misunderstood, but I got a feeling she knows what she's doing. Maybe I should just roll with it. Really, she's extremely attractive, like one of the prettiest women I've ever seen. Plus if I don't she'll probably freeze me to death or some shit, and I really don't want to die some Yanki Virgin.
"Alright." I groan, but a grin forms on my face. "This weekend then." She hugs my arm tightly before opening up a box of her own.
I see she doesn't eat her own cooking...
"Hey come on man, I left my wallet on the bus, just let me borrow a bit huh? Everything in it should do." The kid from a rival school shrinks and hands me the money from his wallet.
"Hey you're the best pal." I pat him on the back and put it into my wallet.
"That should be enough." I mumble to myself as I make my way to the school gates. These posers want to act tough but they sure get shook down easy. "Man I'm like crazy nervous right now." I wipe the sweat from my palms on my jeans as I walk down the road. First date, and with the prettiest girl you've ever seen...who is apparently your new wife. The air around me grows cold and I look up to see Fubuki waiting by the school. I jog the distance up to her.
"Wow..." Her kimono looks fancy, icicle like trinkets dangle from it and ornate designs are woven into the fabric.
"Now love don't stare, it's embarrassing." She shifts to the side giving me a real good look at her side profile.
>deposited into spank bank
I clear my throat, "Y-you-" *Ahem*, "You look really pretty today Fubuki."
Her smile is blinding. She holds out her arms, her light blue hands and perfect nails glisten. Unsure of what to do I step forward. She wraps herself around my forearm. "Alright husband, where are we going?"
I've thought of a few places over the past few days but I sure as hell don't know what girls like. I think the arcade's a bad idea, she probably doesn't want to go walking around looking for trouble, the gym would make a bad date. I'll stick with a classic, dinner. Just got to think of a place to eat at now.
"Welcome to NcDaniels, what can I get for you today?"
My eyes squeeze shut in frustration. This is the best I could come up with...?
"Oh, hamburgers, I'm fond of these. Love, will you order for me?" She squeezes the arm she's wrapped around.
She seems genuinely happy with the place I picked, looking at her I figured she'd have a more expensive pallet.
"Don't see why not." I order my food first to get the easy part out of the way. I sweat bullets as the menu hanging over the employee starts to blur. Oh shit what do I get here? She's a girl so she shouldn't eat too much right? But if I get a kids meal or something I'll definitely be in for it then. I take a breath and grab the employee's collar, "I want a NcSingle with small fry, it better be the best burger you all ever made if you know what's good for ya!" I show my top teeth as I let go of him. Ah fuck, I panicked and went into delinquent mode in front of her, now she'll think I'm just a big, dumb brute like the rest of the gir-
"Oh that's so sweet Tankaroshi! Taking care of me like that." She puts her hand on her cheek as she blushes a dark blue.
Alright so far so good...
I look around to find the best seat in the building, in the back corner there is a fantastic view of the bustling city, but of course there's obstacles. I stare a hole in the back of one of the kid's head. He rubs his neck and turns around to see me staring at him. He grabs his friend and runs out as fast as he can. Seat secured.
I escort the lady back to the booth and help her into her side.
"Oh what wonderful seats! Not as lovely as the view on the mountains are, but the city's activity sure is fun to watch!" She gazes out the window in awe as the passing cars blur and the occasional pedestrian walks by.
"Hey Fubuki, just a question here..." I rub the back of my head awkwardly. She perks up, giving me her undivided attention. "Why did you decide to date me?" It's kind of a shitty question on a first date, but I'm genuinely curious.
She puts her delicate finger to her pouty, blue lips. "Well...Maybe it's your strength." She smiles and continues, "Your hair is super cool, the way you talk when you get into a fight, that look of determination."
So she's saying she just likes me because I'm the toughest kid in school, that's disappointing.
"Even when you're out of a fight and you're thinking that same look is on your face. I guess I like your face." She giggles behind her hand as she squeezes her eyes shut in embarrassment. She looks back at the window, "I think...maybe it's fate really..."
I switch the subject to keep from getting too red in front of everybody. And as we enjoy each others company while the food is prepared we overhear a conversation behind us.
"Ugh it sure is ugly out, I know it was so pretty out just a while ago. I don't know how it got so cold so quickly, I even thought I saw a snowflake outside."
The girl in front of me shifts uncomfortable as she hangs her head in shame, "Umm...Tankaroshi. I'm sorry..."
My body and mind wants me to beat the people behind me until an apology is cried out from them, but even I know that wouldn't help this situation. I have to think tactfuly here but I can't lie to her either. "Fubuki. I love the winter, I love snow. The grey sky out is as beautiful to me as any sunny day and to be honest I think that's because I've gotten used to being around you." I nod earnestly as I say that. She looks in my eyes shocked to see that I'm telling the truth. She smiles at me. I've grown soft to this girl over the past couple weeks, more than I thought normal, maybe it's just a Yuki-Onna's ability over men, maybe I'm just actually into her. The prospect of such a ludicrously sudden marriage seems less and less profound as I spend time with her.
The bell rings giving me the perfect time to run from this face redening moment. I shake my thoughts out of my head. No it's definitely crazy.
As I sit down with the food she does a little prayer and we dig in.
"You sure eat a lot Tankaroshi." She giggles as she watches me stuff the burgers into my mouth.
"Well you know, I'm still growing after all..." I mumble to myself as she keeps giggling.
We enjoy our dinner and laugh with each other, a date well done if it wasn't for the people who walked in next.
Four delinquent chicks had walked by the window and caught eye of me. Now I'm the biggest, baddest banchou in all of the world. Though I do have one weakness...
"Oi! It's Tank-Chan!" One of the girls slam their hands on the table as they hover around us.
I don't hit women. And around my school the girls are just as bad as the boys.
"Finally got yourself a girl eh? Your ugly ass wasn't too interested in me huh? But you'll settle for some monster bitch!?" She points at Fubuki's face.
I shrug, never looking her in the eye "It isn't any of your business, please leave sis."
She sneers and laughs, "Oh am I ruining your date? What are you going to do about it? You gunna take a swing at me big man?" She pats her cheek as she leans in to give me a shot.
"Go away, you're bothering me again." I roll my eyes as I ignore her. After a while she gets her jeers in. Guess she gets off on talking down to people she has to look up to. I slump into the booth, ready for another four or five minutes of this shit.
"Yeah just lay back and ta-huurk*" My eyes widen as I see her face being slammed into the table by her blond hair.
"Eh!? You ruining my date bitch?" I see Fubuki's usually feminine and beautiful face twisted into a jeer as she shows her teeth and curls her lip, her eyelids half open as if uninterested in the prey in front of her. The bully is grabbing at her head where Fubuki has her hand wrapped up. "Tch, you shouldn't poke your nose where it doesn't belong, it's bad for your health you know?"
As Fubuki's fist tightens my fem-bully winces. "Now what did you call my stud? Ugly? I'll show you ugly." She slams her face on the table one more time before letting her fall backwards onto the floor. Fubuki lights up a cigarette as she stands up. Even for a guy she's quite tall, so she has a good five or six inches over the other girls. She lets the cigarette dangle from her pretty blue lips as she leans over them, "Idiots. You bore me, go home and get stuffed." The air kicks up inside the NcDaniels as drinks freeze in peoples hands and nipples poke through sweaters. They all scramble off the floor and run out as she tosses the cigarette at them.
"Tch, shit eaters..." She mumbles as she sits in the booth next to me and takes my arm around her.
"Mmm, I love this date dear! Let's have many more!" She smiles cutely as she nuzzles into my side.
On that day Tankaroshi, the banchou of Last Chance High fell in love with Fubuki the ex-banchou of Frozen Pass high.
]==]

View File

@ -1,43 +0,0 @@
_G.spy = spy
local mock_env = require("spec.env_mock")
local fuzzy = require("spec.fuzzgen")
require("spec.utils")
describe("smr",function()
setup(mock_env.setup)
teardown(mock_env.teardown)
it("should display an anonymously submitted post on the front page", function()
local paste_post = require("endpoints.paste_post")
local index_get = require("endpoints.index_get")
local pages = require("pages")
local config = require("config")
config.domain = "test.host"
pages_mock = mock(pages)
configure()
assert.stub(pages_mock.index).was_not_called()
local post_req = {
host = "test.host",
method = "POST",
path = "/_paste",
args = {
title = fuzzy.any(),
text = fuzzy.any(),
pasteas = "anonymous",
markup = "plain",
tags = "one;two;",
}
}
paste_post(post_req)
local get_req = {
host = "test.host",
method = "GET",
path = "/",
args = {},
}
index_get(get_req)
assert.stub(pages_mock.index).was_called()
assertf(get_req.responsecode >= 200 and get_req.responsecode < 300, "Should give a 2XX response code, got %d", get_req.responsecode)
assert(get_req.responsecode == 200, "Error code should be 200 - OK")
assert(get_req.response:find(get_req.response,1,true), "Failed to find title in string")
end)
end)

View File

@ -1,26 +0,0 @@
--Make sure the type checking works
describe("smr type checking",function()
it("should load without errors",function()
local types = require("types")
end)
it("should not error when an argument is a number",function()
local types = require("types")
local n = 5
assert(types.number(n))
end)
it("should error when an argument is a table",function()
local types = require("types")
local t = {}
assert.has.errors(function()
types.number(t)
end)
end)
it("should check multiple types passed as arugments", function()
local types = require("types")
local num, tbl = 5, {}
types.check(num, types.number, nil)
end)
end)

View File

@ -1,8 +0,0 @@
function assertf(bool, ...)
if bool then return end
local args = {...}
local assertmsg = args[1] or "Assetion failed"
table.remove(args,1)
error(string.format(assertmsg, table.unpack(args)),2)
end

View File

@ -30,7 +30,6 @@ void KeccakF1600(void *s)
void Keccak(ui r, ui c, const u8 *in, u64 inLen, u8 sfx, u8 *out, u64 outLen)
{
/*initialize*/ u8 s[200]; ui R=r/8; ui i,b=0; FOR(i,200) s[i]=0;
/*san-check*/ if (((r+c)!= 1600) || ((r % 8 ) != 0)) return;
/*absorb*/ while(inLen>0) { b=(inLen<R)?inLen:R; FOR(i,b) s[i]^=in[i]; in+=b; inLen-=b; if (b==R) { KeccakF1600(s); b=0; } }
/*pad*/ s[b]^=sfx; if((sfx&0x80)&&(b==(R-1))) KeccakF1600(s); s[R-1]^=0x80; KeccakF1600(s);
/*squeeze*/ while(outLen>0) { b=(outLen<R)?outLen:R; FOR(i,b) out[i]=s[i]; out+=b; outLen-=b; if(outLen>0) KeccakF1600(s); }

View File

@ -45,157 +45,148 @@ lhttp_response(lua_State *L){
return 0;
}
char response[] = "0\r\n\r\n";
/*Helpers for response coroutines*/
int
coroutine_iter_sent(struct netbuf *buf){
printf("Iter sent called\n");
struct co_obj *obj = (struct co_obj*)buf->extra;
int ret;
lua_State *L = obj->L;
printf("\tbuf:%p\n",(void*)buf);
printf("\tobj:%p\n",(void*)obj);
printf("\tL:%p\n",(void*)L);
printf("Top is: %d\n",lua_gettop(L));
printf("Getting status...\n");
lua_getglobal(L,"coroutine");
printf("Found coroutine...\n");
lua_getfield(L,-1,"status");
printf("Found status...\n");
lua_rawgeti(L,LUA_REGISTRYINDEX,obj->ref);
printf("About to get status\n");
lua_call(L,1,1);
printf("Status got\n");
const char *status = luaL_checklstring(L,-1,NULL);
printf("status in sent: %s\n",status);
if(strcmp(status,"dead") == 0){
ret = KORE_RESULT_OK;
printf("Cleanup\n");
return KORE_RESULT_OK;
}else{
ret = coroutine_iter_next(obj);
printf("About to call iter_next from iter_sent\n");
return coroutine_iter_next(obj);
}
if(ret == KORE_RESULT_RETRY){
ret = KORE_RESULT_OK;
}else{
if(obj->removed == 0){
http_start_recv(obj->c);
}
obj->c->hdlr_extra = NULL;
obj->c->disconnect = NULL;
obj->c->flags &= ~CONN_IS_BUSY;
net_send_queue(obj->c,response,strlen(response));
net_send_flush(obj->c);
free(obj);
}
return (ret);
}
const char response[] = "0\r\n\r\n";
int coroutine_iter_next(struct co_obj *obj){
printf("Coroutine iter next called\n");
lua_State *L = obj->L;
lua_getglobal(L,"coroutine");
lua_getfield(L,-1,"status");
lua_rawgeti(L,LUA_REGISTRYINDEX,obj->ref);
lua_call(L,1,1);
const char *status = luaL_checklstring(L,-1,NULL);
if(strcmp(status,"dead") == 0){
kore_log(LOG_ERR,"Coroutine was dead when it was passed to coroutine iter next");
lua_pushstring(L,"Coroutine was dead when passed to coroutine iter next");
lua_error(L);
}
printf("status in next: %s\n",status);
lua_pop(L,lua_gettop(L));
printf("Calling resume\n");
lua_getglobal(L,"coroutine");
printf("Getting resume()\n");
lua_getfield(L,-1,"resume");
printf("Getting function\n");
lua_rawgeti(L,LUA_REGISTRYINDEX,obj->ref);
printf("Checking type\n");
luaL_checktype(L,-1,LUA_TTHREAD);
printf("About to call resume()\n");
int err = lua_pcall(L,1,2,0);
if(err != 0){
return (KORE_RESULT_ERROR);
}
printf("Done calling resume()\n");
if(!lua_toboolean(L,-2)){ //Runtime error
printf("Runtime error\n");
lua_pushstring(L,":\n");//"error",":"
printf("top1:%d\n",lua_gettop(L));
lua_getglobal(L,"debug");//"error",":",{debug}
printf("top2:%d\n",lua_gettop(L));
lua_getfield(L,-1,"traceback");//"error",":",{debug},debug.traceback()
printf("top3:%d\n",lua_gettop(L));
lua_call(L,0,1);//"error",":",{debug},"traceback"
printf("top4:%d\n",lua_gettop(L));
lua_remove(L,-2);//"error",":","traceback"
printf("top5:%d\n",lua_gettop(L));
lua_concat(L,3);
printf("top6:%d\n",lua_gettop(L));
size_t size;
const char *s = luaL_checklstring(L,-1,&size);
kore_log(LOG_ERR,"Error: %s\n",s);
printf("Error: %s\n",s);
lua_pop(L,lua_gettop(L));
return (KORE_RESULT_ERROR);
}
//No runtime error
if(lua_type(L,-1) == LUA_TSTRING){
printf("Data yielded\n");
size_t size;
const char *data = luaL_checklstring(L,-1,&size);
struct netbuf *nb;
struct kore_buf *kb = kore_buf_alloc(4096);
kore_buf_appendf(kb,"%lu\r\n",size);
printf("Yielding data stream size %lld\n",size);
struct kore_buf *kb = kore_buf_alloc(0);
kore_buf_appendf(kb,"%x\r\n",size);
kore_buf_append(kb,data,size);
kore_buf_appendf(kb,"\r\n");
//size_t ssize;
//char *sstr = kore_buf_stringify(kb,&ssize);
net_send_stream(obj->c, kb->data, kb->offset, coroutine_iter_sent, &nb);
size_t ssize;
char *sstr = kore_buf_stringify(kb,&ssize);
net_send_stream(obj->c, sstr, ssize, coroutine_iter_sent, &nb);
nb->extra = obj;
lua_pop(L,lua_gettop(L));
kore_buf_free(kb);
return (KORE_RESULT_RETRY);
//return err == 0 ? (KORE_RESULT_OK) : (KORE_RESULT_RETRY);
}else if(lua_type(L,-1) == LUA_TNIL){
printf("Done with function\n");
struct netbuf *nb;
struct kore_buf *kb = kore_buf_alloc(4096);
kore_buf_appendf(kb,"0\r\n\r\n");
net_send_queue(obj->c, kb->data, kb->offset);
net_send_stream(obj->c, response, strlen(response) + 0, coroutine_iter_sent, &nb);
printf("About to send final bit\n");
net_send_stream(obj->c, response, strlen(response) + 1, coroutine_iter_sent, &nb);
nb->extra = obj;
printf("Done sending final bit\n");
lua_pop(L,lua_gettop(L));
kore_buf_free(kb);
printf("Poped everything\n");
return (KORE_RESULT_OK);
}else{
kore_log(LOG_CRIT,"Coroutine used for response returned something that was not a string:%s\n",lua_typename(L,lua_type(L,-1)));
printf("Coroutine used for response returned something that was not a string:%s\n",lua_typename(L,lua_type(L,-1)));
return (KORE_RESULT_ERROR);
}
}
static void
coroutine_disconnect(struct connection *c){
kore_log(LOG_ERR,"Disconnect routine called\n");
printf("Disconnect routine called\n");
struct co_obj *obj = (struct co_obj*)c->hdlr_extra;
lua_State *L = obj->L;
int ref = obj->ref;
int Lref = obj->Lref;
obj->removed = 1;
luaL_unref(L,LUA_REGISTRYINDEX,ref);
luaL_unref(L,LUA_REGISTRYINDEX,Lref);
c->hdlr_extra = NULL;
free(obj);
printf("Done with disconnect\n");
}
/*
The coroutine passed to this function should yield() the data to send to the
client, then return when done.
TODO: Broken and leaks memory
http_response_co(request::userdata, co::coroutine)
*/
int
lhttp_response_co(lua_State *L){
struct connection *c;
printf("Start response coroutine\n");
int coroutine_ref = luaL_ref(L,LUA_REGISTRYINDEX);
struct http_request *req = luaL_checkrequest(L,-1);
c = req->owner;
if(c->state == CONN_STATE_DISCONNECTING){
return 0;
}
lua_pop(L,1);
struct co_obj *obj = (struct co_obj*)malloc(sizeof(struct co_obj));
obj->removed = 0;
obj->L = lua_newthread(L);
obj->Lref = luaL_ref(L,LUA_REGISTRYINDEX);
obj->ref = coroutine_ref;
obj->c = c;
obj->c->disconnect = coroutine_disconnect;
obj->c->hdlr_extra = obj;
obj->c->flags |= CONN_IS_BUSY;
req->flags |= HTTP_REQUEST_NO_CONTENT_LENGTH;
http_response_header(req,"transfer-encoding","chunked");
http_response(req,200,NULL,0);
struct co_obj *obj = (struct co_obj*)malloc(sizeof(struct co_obj));
obj->L = lua_newthread(L);
obj->ref = coroutine_ref;
obj->c = req->owner;
obj->c->flags |= CONN_IS_BUSY;
obj->c->disconnect = coroutine_disconnect;
obj->c->hdlr_extra = obj;
printf("About to call iter next\n");
http_response(req,200,NULL,0);
coroutine_iter_next(obj);
printf("Done calling iter next\n");
return 0;
}
/*
@ -447,7 +438,6 @@ lhttp_set_flags(lua_State *L){
struct http_request *req = luaL_checkrequest(L,-2);
lua_pop(L,2);
req->flags = flags;
return 0;
}
/*
@ -458,7 +448,6 @@ lhttp_get_flags(lua_State *L){
struct http_request *req = luaL_checkrequest(L,-1);
lua_pop(L,1);
lua_pushnumber(L,req->flags);
return 1;
}
/*

View File

@ -2,16 +2,12 @@
struct co_obj {
lua_State *L;
int ref;
int Lref;
int removed;
struct connection *c;
};
int lhttp_response(lua_State *L);
int lhttp_response_co(lua_State *L);
int coroutine_iter_sent(struct netbuf *buf);
int coroutine_iter_next(struct co_obj *obj);
int lhttp_response_header(lua_State *L);
int lhttp_request_header(lua_State *L);
int lhttp_method_text(lua_State *L);
int lhttp_request_get_path(lua_State *L);
int lhttp_request_get_host(lua_State *L);
@ -23,8 +19,6 @@ int lhttp_argument_get_string(lua_State *L);
int lhttp_request_get_ip(lua_State *L);
int lhttp_populate_cookies(lua_State *L);
int lhttp_file_get(lua_State *L);
int lhttp_set_flags(lua_State *L);
int lhttp_get_flags(lua_State *L);
int lhttp_populate_multipart_form(lua_State *L);
int lkore_log(lua_State *L);
void load_kore_libs(lua_State *L);

View File

@ -9,7 +9,6 @@ local sql = require("lsqlite3")
local queries = require("queries")
local util = require("util")
local db = require("db")
local ret = {}
@ -17,11 +16,10 @@ local stmnt_cache, stmnt_insert_cache, stmnt_dirty_cache
local oldconfigure = configure
function configure(...)
local cache = db.sqlassert(sql.open_memory())
ret.cache = cache -- Expose db for testing
local cache = util.sqlassert(sql.open_memory())
--A cache table to store rendered pages that do not need to be
--rerendered. In theory this could OOM the program eventually and start
--swapping to disk. TODO
--swapping to disk. TODO: fixme
assert(cache:exec([[
CREATE TABLE IF NOT EXISTS cache (
path TEXT PRIMARY KEY,
@ -56,7 +54,7 @@ end
--Render a page, with cacheing. If you need to dirty a cache, call dirty_cache()
function ret.render(pagename,callback)
stmnt_cache:bind_names{path=pagename}
local err = db.do_sql(stmnt_cache)
local err = util.do_sql(stmnt_cache)
if err == sql.DONE then
stmnt_cache:reset()
--page is not cached
@ -74,7 +72,7 @@ function ret.render(pagename,callback)
path=pagename,
data=text,
}
err = db.do_sql(stmnt_insert_cache)
err = util.do_sql(stmnt_insert_cache)
if err == sql.ERROR or err == sql.MISUSE then
error("Failed to update cache for page " .. pagename)
end
@ -82,13 +80,11 @@ function ret.render(pagename,callback)
return text
end
-- Dirty a cached page, causing it to be re-rendered the next time it's
-- requested. Doesn't actually delete it or anything, just sets it's dirty bit
function ret.dirty(url)
stmnt_dirty_cache:bind_names{
path = url
}
db.do_sql(stmnt_dirty_cache)
util.do_sql(stmnt_dirty_cache)
stmnt_dirty_cache:reset()
end

View File

@ -5,6 +5,5 @@ A one-stop-shop for runtime configuration
return {
domain = "<{get domain}>",
production = false,
legacy_url_cutoff = 144,
db = "data/posts.db"
legacy_url_cutoff = 144
}

View File

@ -6,88 +6,10 @@ local sql = require("lsqlite3")
local queries = require("queries")
local util = require("util")
local config = require("config")
local db = {}
--[[
Runs an sql query and receives the 3 arguments back, prints a nice error
message on fail, and returns true on success.
]]
function db.sqlassert(r, errcode, err)
if not r then
error(string.format("%d: %s",errcode, err))
end
return r
end
--[[
Continuously tries to perform an sql statement until it goes through
]]
function db.do_sql(stmnt)
if not stmnt then error("No statement",2) end
local err
local i = 0
repeat
err = stmnt:step()
if err == sql.BUSY then
i = i + 1
coroutine.yield()
end
until(err ~= sql.BUSY or i > 10)
assert(i < 10, "Database busy")
return err
end
--[[
Provides an iterator that loops over results in an sql statement
or throws an error, then resets the statement after the loop is done.
]]
function db.sql_rows(stmnt)
if not stmnt then error("No statement",2) end
local err
return function()
err = stmnt:step()
if err == sql.BUSY then
coroutine.yield()
elseif err == sql.ROW then
return unpack(stmnt:get_values())
elseif err == sql.DONE then
stmnt:reset()
return nil
else
stmnt:reset()
local msg = string.format(
"SQL Iteration failed: %s : %s\n%s",
tostring(err),
db.conn:errmsg(),
debug.traceback()
)
log(LOG_CRIT,msg)
error(msg)
end
end
end
--[[
Binds an argument to as statement with nice error reporting on failure
stmnt :: sql.stmnt - the prepared sql statemnet
call :: string - a string "bind" or "bind_blob"
position :: number - the argument position to bind to
data :: string - The data to bind
]]
function db.sqlbind(stmnt,call,position,data)
assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call)
local f = stmnt[call](stmnt,position,data)
if f ~= sql.OK then
error(string.format("Failed call %s(%d,%q): %s", call, position, data, db.conn:errmsg()),2)
end
end
local oldconfigure = configure
db.conn = db.sqlassert(sql.open(config.db))
db.conn = util.sqlassert(sql.open("data/posts.db"))
function configure(...)
--Create sql tables

View File

@ -8,21 +8,40 @@ local stmnt_tags_get
local oldconfigure = configure
function configure(...)
stmnt_tags_get = db.sqlassert(db.conn:prepare(queries.select_suggest_tags))
stmnt_tags_get = util.sqlassert(db.conn:prepare(queries.select_suggest_tags))
return oldconfigure(...)
end
local function suggest_tags(req,data)
print("Suggesting tags!")
stmnt_tags_get:bind_names{
match = data .. "%"
}
local err = util.do_sql(stmnt_tags_get)
if err == sql.ROW or err == sql.DONE then
local tags = {data}
for tag in stmnt_tags_get:rows() do
print("Found tag:",tag[1])
table.insert(tags,tag[1])
end
stmnt_tags_get:reset()
http_response_header(req,"Content-Type","text/plain")
http_response(req,200,table.concat(tags,";"))
else
log(LOG_ALERT,"Failed to get tag suggestions in an unusual way:" .. err .. ":" .. db.conn:errmsg())
--This is bad though
local page = pages.error({
errcode = 500,
errcodemsg = "Server error",
explanation = string.format(
"Failed to retreive tags from database:%d:%q",
err,
db.conn:errmsg()
),
})
stmnt_tags_get:reset()
http_response(req,500,page)
end
end
local function api_get(req)
@ -36,7 +55,7 @@ local function api_get(req)
we're searching for, potentially causing a DoS with a
sufficiently backtrack-ey search/tag combination.
]]
assert(data:match("^[a-zA-Z0-9,%s-]+$"),string.format("Bad characters in tag: %q",data))
assert(data:match("^[a-zA-Z0-9,%s-]+$"),"Bad characters in tag")
return suggest_tags(req,data)
end
end

View File

@ -1,72 +0,0 @@
local zlib = require("zlib")
local sql = require("lsqlite3")
local db = require("db")
local queries = require("queries")
local util = require("util")
local pages = require("pages")
local tags = require("tags")
local session = require("session")
local config = require("config")
local stmnt_bio
local oldconfigure = configure
function configure(...)
stmnt_bio = assert(db.conn:prepare(queries.select_author_bio))
return oldconfigure(...)
end
local function bio_edit_get(req)
local host = http_request_get_host(req)
local path = http_request_get_path(req)
local author, authorid = session.get(req)
http_request_populate_qs(req)
local ret
if (not author) or (not authorid) then
ret = pages.error{
errcode = 401,
errcodemsg = "Not authorized",
explanation = "You must be logged in to edit your biography."
}
http_response(req,401,ret)
end
--Get the logged in author's bio to display
stmnt_bio:bind_names{
authorid = authorid
}
local err = db.do_sql(stmnt_bio)
if err == sql.DONE then
--No rows, we're logged in but an author with our id doesn't
--exist? Something has gone wrong.
ret = pages.error{
errcode = 500,
errcodemsg = "Server error",
explanation = string.format([[
Tried to get the biography of author %q (%d) but no author with that id was
found, please report this error.
]], author, authorid),
should_traceback=true
}
stmnt_bio:reset()
http_response(req,500,ret)
return
end
assert(err == sql.ROW)
local data = stmnt_bio:get_values()
local bio_text = data[1]
if data[1] ~= "" then
bio_text = zlib.decompress(data[1])
end
stmnt_bio:reset()
ret = pages.edit_bio{
text = bio_text,
user = author,
domain = config.domain,
}
http_response(req,200,ret)
end
return bio_edit_get

View File

@ -1,50 +0,0 @@
local sql = require("lsqlite3")
local zlib = require("zlib")
local db = require("db")
local queries = require("queries")
local pages = require("pages")
local parsers = require("parsers")
local util = require("util")
local tagslib = require("tags")
local cache = require("cache")
local config = require("config")
local session = require("session")
local stmnt_update_bio
local oldconfigure = configure
function configure(...)
stmnt_update_bio = assert(db.conn:prepare(queries.update_bio))
return oldconfigure(...)
end
local function edit_bio(req)
local host = http_request_get_host(req)
local path = http_request_get_path(req)
local author, author_id = session.get(req)
http_request_populate_post(req)
local text = http_argument_get_string(req,"text") or ""
local parsed = parsers.plain(text) -- Make sure the plain parser can deal with it, even though we don't store this result.
local compr_raw = zlib.compress(text)
local compr = zlib.compress(parsed)
db.sqlbind(stmnt_update_bio, "bind_blob", 1,compr_raw)
db.sqlbind(stmnt_update_bio, "bind", 2, author_id)
if db.do_sql(stmnt_update_bio) ~= sql.DONE then
stmnt_update_bio:reset()
error("Faled to update biography")
end
stmnt_update_bio:reset()
local loc = string.format("https://%s.%s",author,config.domain)
-- Dirty the cache for the author's index, the only place where the bio is displayed.
cache.dirty(string.format("%s.%s",author,config.domain))
http_response_header(req,"Location",loc)
http_response(req,303,"")
return
end
return edit_bio

View File

@ -9,14 +9,10 @@ local config = require("config")
local stmnt_author_create
--We prevent people from changing their password file, this way we don't really
--need to worry about logged in accounts being hijacked if someone gets at the
--database. The attacker can still paste & edit from the logged in account for
--a while, but whatever.
local oldconfigure = configure
function configure(...)
stmnt_author_create = db.sqlassert(db.conn:prepare(queries.insert_author))
stmnt_author_create = util.sqlassert(db.conn:prepare(queries.insert_author))
return oldconfigure(...)
end
@ -46,7 +42,7 @@ local function claim_post(req)
}
stmnt_author_create:bind_blob(2,salt)
stmnt_author_create:bind_blob(3,hash)
local err = db.do_sql(stmnt_author_create)
local err = util.do_sql(stmnt_author_create)
if err == sql.DONE then
log(LOG_INFO,"Account creation successful:" .. name)
--We sucessfully made the new author

View File

@ -1,64 +0,0 @@
local tags = require("tags")
local util = require("util")
local pages = require("pages")
local config = require("config")
local session = require("session")
local db = require("db")
local queries = require("queries")
local sql = require("lsqlite3")
local cache = require("cache")
local oldconfigure = configure
local stmnt_delete
function configure(...)
stmnt_delete = assert(db.conn:prepare(queries.delete_post),db.conn:errmsg())
return oldconfigure(...)
end
local function delete_post(req)
local host = http_request_get_host(req)
local path = http_request_get_path(req)
http_request_populate_post(req)
local storystr = assert(http_argument_get_string(req,"story"))
print("Looking at storystr:",storystr)
local storyid = util.decode_id(storystr)
local author, authorid = session.get(req)
if not author then
http_response(req, 401, pages.error{
errcode = 401,
errcodemsg = "Not authorized",
explanation = "You must be logged in to delete posts. You are either not logged in or your session has expired.",
should_traceback = true
})
return
end
log(LOG_DEBUG,string.format("Deleting post %d with proposed owner %d",storyid, authorid))
stmnt_delete:bind_names{
postid = storyid,
authorid = authorid
}
local err = db.do_sql(stmnt_delete)
if err ~= sql.DONE then
log(LOG_DEBUG,string.format("Failed to delete: %d:%s",err, db.conn:errmsg()))
http_response(req,500,pages.error{
errcode = 500,
errcodemsg = "Internal error",
explanation = "Failed to delete posts from database:" .. db.conn:errmsg(),
should_traceback = true,
})
stmnt_delete:reset()
else
local loc = string.format("https://%s/%s",config.domain,storystr)
http_response_header(req,"Location",loc)
http_response(req,303,"")
stmnt_delete:reset()
cache.dirty(string.format("%s",config.domain))
cache.dirty(string.format("%s-logout",config.domain))
cache.dirty(string.format("%s.%s",author,config.domain))
cache.dirty(string.format("%s",storystr))
cache.dirty(string.format("%s?comments=1",storystr))
end
end
return delete_post

View File

@ -25,7 +25,7 @@ local function download_get(req)
stmnt_download:bind_names{
postid = story_id
}
local err = db.do_sql(stmnt_download)
local err = util.do_sql(stmnt_download)
if err == sql.DONE then
--No rows, story not found
http_response(req,404,pages.nostory{path=story})

View File

@ -32,7 +32,7 @@ local function edit_get(req)
postid = story_id,
authorid = authorid
}
local err = db.do_sql(stmnt_edit)
local err = util.do_sql(stmnt_edit)
if err == sql.DONE then
--No rows, we're probably not the owner (it might
--also be because there's no such story)
@ -46,6 +46,7 @@ local function edit_get(req)
assert(err == sql.ROW)
local data = stmnt_edit:get_values()
local txt_compressed, markup, isanon, title, unlisted = unpack(data)
print("from query, unlisted was:",unlisted)
local text = zlib.decompress(txt_compressed)
local tags = tags.get(story_id)
local tags_txt = table.concat(tags,";")

View File

@ -38,19 +38,10 @@ local function edit_post(req)
stmnt_author_of:bind_names{
id = storyid
}
local err = db.do_sql(stmnt_author_of)
local err = util.do_sql(stmnt_author_of)
if err ~= sql.ROW then
stmnt_author_of:reset()
local msg = string.format("No author found for story: %d", storyid)
log(LOG_ERR,msg)
local response = pages.error{
errcode = 404,
errcodemsg = "Not Found",
explanation = msg,
should_traceback = true,
}
http_response(req,404,response)
return
error("No author found for story:" .. storyid)
end
local data = stmnt_author_of:get_values()
stmnt_author_of:reset()
@ -66,14 +57,14 @@ local function edit_post(req)
assert(stmnt_update_raw:bind_blob(1,compr_raw) == sql.OK)
assert(stmnt_update_raw:bind(2,markup) == sql.OK)
assert(stmnt_update_raw:bind(3,storyid) == sql.OK)
assert(db.do_sql(stmnt_update_raw) == sql.DONE, "Failed to update raw")
assert(util.do_sql(stmnt_update_raw) == sql.DONE, "Failed to update raw")
stmnt_update_raw:reset()
assert(stmnt_update:bind(1,title) == sql.OK)
assert(stmnt_update:bind_blob(2,compr) == sql.OK)
assert(stmnt_update:bind(3,pasteas == "anonymous" and 1 or 0) == sql.OK)
assert(stmnt_update:bind(4,unlisted) == sql.OK)
assert(stmnt_update:bind(5,storyid) == sql.OK)
assert(db.do_sql(stmnt_update) == sql.DONE, "Failed to update text")
assert(util.do_sql(stmnt_update) == sql.DONE, "Failed to update text")
stmnt_update:reset()
tagslib.set(storyid,tags)
local id_enc = util.encode_id(storyid)
@ -81,7 +72,7 @@ local function edit_post(req)
local loc = string.format("https://%s/%s",config.domain,id_enc)
if unlisted then
stmnt_hash:bind_names{id=storyid}
local err = db.do_sql(stmnt_hash)
local err = util.do_sql(stmnt_hash)
if err ~= sql.ROW then
error("Failed to get a post's hash while trying to make it unlisted")
end
@ -101,7 +92,6 @@ local function edit_post(req)
cache.dirty(string.format("%s/%s",config.domain,id_enc)) -- This place to read this post
cache.dirty(string.format("%s",config.domain)) -- The site index (ex, if the author changed the paste from their's to "Anonymous", the cache should reflect that).
cache.dirty(string.format("%s.%s",author,config.domain)) -- The author's index, same reasoning as above.
cache.dirty(string.format("%s-logout",config.domain))
http_response_header(req,"Location",loc)
http_response(req,303,"")
return

View File

@ -8,8 +8,6 @@ local config = require("config")
local pages = require("pages")
local libtags = require("tags")
local session = require("session")
local parsers = require("parsers")
local zlib = require("zlib")
local stmnt_index, stmnt_author, stmnt_author_bio
@ -28,7 +26,7 @@ local function get_site_home(req, loggedin)
log(LOG_DEBUG,"Cache miss, rendering site index")
stmnt_index:bind_names{}
local latest = {}
for idr, title, iar, dater, author, hits in db.sql_rows(stmnt_index) do
for idr, title, iar, dater, author, hits in util.sql_rows(stmnt_index) do
table.insert(latest,{
url = util.encode_id(idr),
title = title,
@ -45,12 +43,13 @@ local function get_site_home(req, loggedin)
loggedin = loggedin
}
end
local function get_author_home(req, loggedin)
local function get_author_home(req)
--print("Looking at author home...")
local host = http_request_get_host(req)
local subdomain = host:match("([^\\.]+)")
stmnt_author_bio:bind_names{author=subdomain}
local err = util.do_sql(stmnt_author_bio)
local author, authorid = session.get(req)
local err = db.do_sql(stmnt_author_bio)
if err == sql.DONE then
log(LOG_INFO,"No such author:" .. subdomain)
stmnt_author_bio:reset()
@ -58,20 +57,14 @@ local function get_author_home(req, loggedin)
author = subdomain
}
end
if err ~= sql.ROW then
stmnt_author_bio:reset()
error(string.format("Failed to get author %q error: %q",subdomain, tostring(err)))
end
assert(err == sql.ROW,"failed to get author:" .. subdomain .. " error:" .. tostring(err))
local data = stmnt_author_bio:get_values()
local bio_text = data[1]
if data[1] ~= "" then
bio_text = zlib.decompress(data[1])
end
local bio = parsers.plain(bio_text)
local bio = data[1]
stmnt_author_bio:reset()
stmnt_author:bind_names{author=subdomain}
local stories = {}
for id, title, time, hits, unlisted, hash in db.sql_rows(stmnt_author) do
for id, title, time, hits, unlisted, hash in util.sql_rows(stmnt_author) do
--print("Looking at:",id,title,time,hits,unlisted)
if unlisted == 1 and author == subdomain then
local url = util.encode_id(id) .. "?pwd=" .. util.encode_unlisted(hash)
table.insert(stories,{
@ -119,19 +112,18 @@ local function index_get(req)
end)
elseif host == config.domain and author then
--Display home page with "log out" button
local cachepath = string.format("%s-logout",config.domain)
text = cache.render(cachepath, function()
text = cache.render(config.domain .. "-logout", function()
return get_site_home(req,true)
end)
elseif host ~= config.domain and author ~= subdomain then
--author home page
local cachepath = string.format("%s.%s",subdomain,config.domain)
text = cache.render(cachepath, function()
return get_author_home(req, author ~= nil)
return get_author_home(req)
end)
elseif host ~= config.domain and author == subdomain then
--author's home page for the author, don't cache, display unlisted
text = get_author_home(req, author ~= nil)
text = get_author_home(req)
end
assert(text)
http_response(req,200,text)

View File

@ -27,7 +27,7 @@ local function login_post(req)
name = name
}
local text
local err = db.do_sql(stmnt_author_acct)
local err = util.do_sql(stmnt_author_acct)
if err == sql.ROW then
local id, salt, passhash = unpack(stmnt_author_acct:get_values())
stmnt_author_acct:reset()
@ -35,10 +35,7 @@ local function login_post(req)
local hash = sha3(todigest)
if hash == passhash then
local mysession = session.start(id)
local domain_no_port = config.domain:match("(.*):.*") or config.domain
http_response_header(req,"set-cookie",string.format(
[[session=%s; SameSite=Lax; Path=/; Domain=%s; HttpOnly; Secure]],mysession,domain_no_port
))
http_response_cookie(req,"session",mysession,"/",0,0)
local loc = string.format("https://%s.%s",name,config.domain)
http_response_header(req,"Location",loc)
http_response(req,303,"")

View File

@ -13,6 +13,7 @@ local function paste_get(req)
http_response(req,303,"")
return
elseif host == config.domain and author == nil then
print("doing anon paste")
text = cache.render(string.format("%s/_paste",host),function()
log(LOG_DEBUG, "Cache missing, rendering post page")
return assert(pages.paste{
@ -25,6 +26,7 @@ local function paste_get(req)
end)
http_response(req,200,text)
elseif host ~= config.domain and author then
print("doing author paste")
text = assert(pages.author_paste{
domain = config.domain,
user = author,

View File

@ -43,15 +43,16 @@ local function anon_paste(req,ps)
--with a more elegent solution.
log(LOG_DEBUG,string.format("new story: %q, length: %d",ps.title,string.len(ps.text)))
print("Unlisted:",ps.unlisted)
local textsha3 = sha3(ps.text .. get_random_bytes(32))
db.sqlbind(stmnt_paste,"bind_blob",1,ps.text)
db.sqlbind(stmnt_paste,"bind",2,ps.title)
db.sqlbind(stmnt_paste,"bind",3,-1)
db.sqlbind(stmnt_paste,"bind",4,true)
db.sqlbind(stmnt_paste,"bind_blob",5,"")
db.sqlbind(stmnt_paste,"bind",6,ps.unlisted)
db.sqlbind(stmnt_paste,"bind_blob",7,textsha3)
local err = db.do_sql(stmnt_paste)
util.sqlbind(stmnt_paste,"bind_blob",1,ps.text)
util.sqlbind(stmnt_paste,"bind",2,ps.title)
util.sqlbind(stmnt_paste,"bind",3,-1)
util.sqlbind(stmnt_paste,"bind",4,true)
util.sqlbind(stmnt_paste,"bind_blob",5,"")
util.sqlbind(stmnt_paste,"bind",6,ps.unlisted)
util.sqlbind(stmnt_paste,"bind_blob",7,textsha3)
err = util.do_sql(stmnt_paste)
stmnt_paste:reset()
if err == sql.DONE then
local rowid = stmnt_paste:last_insert_rowid()
@ -62,7 +63,7 @@ local function anon_paste(req,ps)
assert(stmnt_raw:bind(1,rowid) == sql.OK)
assert(stmnt_raw:bind_blob(2,ps.raw) == sql.OK)
assert(stmnt_raw:bind(3,ps.markup) == sql.OK)
err = db.do_sql(stmnt_raw)
err = util.do_sql(stmnt_raw)
stmnt_raw:reset()
if err ~= sql.DONE then
local msg = string.format(
@ -112,9 +113,9 @@ local function author_paste(req,ps)
assert(stmnt_paste:bind(3,authorid) == sql.OK)
assert(stmnt_paste:bind(4,asanon == "anonymous") == sql.OK)
assert(stmnt_paste:bind_blob(5,"") == sql.OK)
db.sqlbind(stmnt_paste,"bind",6,ps.unlisted)
db.sqlbind(stmnt_paste,"bind_blob",7,textsha3)
local err = db.do_sql(stmnt_paste)
util.sqlbind(stmnt_paste,"bind",6,ps.unlisted)
util.sqlbind(stmnt_paste,"bind_blob",7,textsha3)
local err = util.do_sql(stmnt_paste)
stmnt_paste:reset()
if err == sql.DONE then
local rowid = stmnt_paste:last_insert_rowid()
@ -125,7 +126,7 @@ local function author_paste(req,ps)
assert(stmnt_raw:bind(1,rowid) == sql.OK)
assert(stmnt_raw:bind_blob(2,ps.raw) == sql.OK)
assert(stmnt_raw:bind(3,ps.markup) == sql.OK)
err = db.do_sql(stmnt_raw)
err = util.do_sql(stmnt_raw)
stmnt_raw:reset()
if err ~= sql.DONE then
local msg = string.format(
@ -148,7 +149,6 @@ local function author_paste(req,ps)
cache.dirty(string.format("%s.%s",author,config.domain))
cache.dirty(string.format("%s/%s",config.domain,url))
cache.dirty(string.format("%s",config.domain))
cache.dirty(string.format("%s-logout",config.domain))
end
http_response_header(req,"Location",loc)
http_response(req,303,"")

View File

@ -1,4 +1,4 @@
local sql = require("lsqlite3")
local sql = require("sqlite3")
local session = require("session")
local tags = require("tags")
@ -8,7 +8,6 @@ local util = require("util")
local cache = require("cache")
local pages = require("pages")
local config = require("config")
local zlib = require("zlib")
local stmnt_read, stmnt_update_views, stmnt_comments
@ -28,7 +27,7 @@ local function add_view(storyid)
stmnt_update_views:bind_names{
id = storyid
}
local err = db.do_sql(stmnt_update_views)
local err = util.do_sql(stmnt_update_views)
assert(err == sql.DONE, "Failed to update view counter:"..tostring(err))
stmnt_update_views:reset()
end
@ -42,7 +41,7 @@ local function populate_ps_story(req,ps)
stmnt_read:bind_names{
id = ps.storyid,
}
local err = db.do_sql(stmnt_read)
local err = util.do_sql(stmnt_read)
if err == sql.DONE then
--We got no story
stmnt_read:reset()
@ -81,7 +80,7 @@ local function get_comments(req,ps)
id = ps.storyid
}
local comments = {}
for com_author, com_isanon, com_text in db.sql_rows(stmnt_comments) do
for com_author, com_isanon, com_text in util.sql_rows(stmnt_comments) do
table.insert(comments,{
author = com_author,
isanon = com_isanon == 1, --int to boolean
@ -98,10 +97,6 @@ local function read_get(req)
host = http_request_get_host(req),
path = http_request_get_path(req),
method = http_method_text(req),
extra_load = {
'<script src="/_js/bookmark.js"></script>',
'<script src="/_js/intervine_deletion.js"></script>',
}
}
local err
--Get our story id
@ -153,6 +148,7 @@ local function read_get(req)
table.insert(params,"pwd=" .. hashstr)
end
local cachestrparts = {
ps.host,
ps.path,
}
if #params > 0 then
@ -177,12 +173,6 @@ local function read_get(req)
text = pages.read(ps)
end
end
--If this isn't unlisted, dirty everywhere the hit counter is shown
cache.dirty(string.format("%s",config.domain))
cache.dirty(string.format("%s/%s",config.domain,ps.idp)) -- This place to read this post
cache.dirty(string.format("%s.%s",config.domain,ps.idp)) -- The author's index page
assert(text)
http_response(req,200,text)
return

View File

@ -38,7 +38,7 @@ local function read_post(req)
isanon = isanon,
comment_text = comment_text,
}
local err = db.do_sql(stmnt_comment_insert)
local err = util.do_sql(stmnt_comment_insert)
stmnt_comment_insert:reset()
if err ~= sql.DONE then
http_response(req,500,"Internal error, failed to post comment. Go back and try again.")

View File

@ -1,9 +0,0 @@
-- Various global functions to cause less typing.
function assertf(bool, fmt, ...)
fmt = fmt or "Assetion Failed"
if not bool then
error(string.format(fmt,...),2)
end
end

View File

@ -11,17 +11,39 @@ local et = require("etlua")
local sql = require("lsqlite3")
local zlib = require("zlib")
--stubs for detouring
--stub for detouring
function configure(...) end
--smr code
require("global")
local cache = require("cache")
local pages = require("pages")
local util = require("util")
local config = require("config")
local db = require("db")
--Pages
local endpoint_names = {
read = {"get","post"},
preview = {"post"},
index = {"get"},
paste = {"get","post"},
download = {"get"},
login = {"get","post"},
logout = {"get"},
edit = {"get","post"},
claim = {"get","post"},
search = {"get"},
archive = {"get"},
api = {"get"},
}
local endpoints = {}
for name, methods in pairs(endpoint_names) do
for _,method in pairs(methods) do
local epn = string.format("%s_%s",name,method)
endpoints[epn] = require("endpoints." .. epn)
end
end
print("Hello from init.lua")
local oldconfigure = configure
function configure(...)
@ -35,79 +57,71 @@ function configure(...)
end
print("Created configure function")
-- TODO: Fill this out
local http_methods = {"GET","POST"}
local http_m_rev = {}
for k,v in pairs(http_methods) do
http_m_rev[v] = true
function home(req)
local method = http_method_text(req)
if method == "GET" then
endpoints.index_get(req)
end
end
--Endpoints, all this stuff gets required here.
for funcname, spec in pairs({
home = {
GET = require("endpoints.index_get"),
},
claim = {
GET = require("endpoints.claim_get"),
POST = require("endpoints.claim_post"),
},
paste = {
GET = require("endpoints.paste_get"),
POST = require("endpoints.paste_post"),
},
read = {
GET = require("endpoints.read_get"),
POST = require("endpoints.read_post"),
},
login = {
GET = require("endpoints.login_get"),
POST = require("endpoints.login_post"),
},
logout = {
GET = require("endpoints.logout_get"),
},
edit = {
GET = require("endpoints.edit_get"),
POST = require("endpoints.edit_post"),
},
delete = {
POST = require("endpoints.delete_post"),
},
edit_bio = {
GET = require("endpoints.bio_get"),
POST = require("endpoints.bio_post"),
},
download = {
GET = require("endpoints.download_get"),
},
preview = {
POST = require("endpoints.preview_post"),
},
search = {
GET = require("endpoints.search_get"),
},
archive = {
GET = require("endpoints.archive_get"),
},
api = {
GET = require("endpoints.api_get"),
},
}) do
assert(_G[funcname] == nil, "Tried to overwrite an endpoint, please define endpoints exactly once")
for k,v in pairs(spec) do
assert(http_m_rev[k], "Unknown http method '" .. k .. "' defined for endpoint '" .. funcname .. "'")
assert(type(v) == "function", "Endpoint %s %s must be a function, but was a %s",funcname, k, type(v))
end
_G[funcname] = function(req)
--We prevent people from changing their password file, this way we don't really
--need to worry about logged in accounts being hijacked if someone gets at the
--database. The attacker can still paste & edit from the logged in account for
--a while, but whatever.
function claim(req)
local method = http_method_text(req)
if spec[method] == nil then
log(LOG_WARNING,string.format("Endpoint %s called with http method %s, but no such route defined.", funcname, method))
else
log(LOG_DEBUG,string.format("Endpoint %s called with method %s",funcname,method))
if method == "GET" then
endpoints.claim_get(req)
elseif method == "POST" then
endpoints.claim_post(req)
end
spec[method](req)
end
--Create a new paste on the site
function paste(req)
local method = http_method_text(req)
if method == "GET" then
endpoints.paste_get(req)
elseif method == "POST" then
endpoints.paste_post(req)
end
log(LOG_INFO,string.format("Associateing endpoint %q", funcname))
end
function read(req)
local method = http_method_text(req)
if method == "GET" then
endpoints.read_get(req)
elseif method == "POST" then
endpoints.read_post(req)
end
end
function login(req)
local method = http_method_text(req)
if method == "GET" then
endpoints.login_get(req)
elseif method == "POST" then
endpoints.login_post(req)
end
end
function logout(req)
endpoints.logout_get(req)
end
--Edit a story
function edit(req)
local method = http_method_text(req)
if method == "GET" then
endpoints.edit_get(req)
elseif method == "POST" then
endpoints.edit_post(req)
end
end
--TODO
function edit_bio()
error("Not yet implemented")
end
function teardown()
@ -121,4 +135,30 @@ function teardown()
print("Finished lua teardown")
end
function download(req)
endpoints.download_get(req)
end
function preview(req)
endpoints.preview_post(req)
end
function search(req)
endpoints.search_get(req)
end
function archive(req)
print("archive method:",http_method_text(req))
endpoints.archive_get(req)
end
function api(req)
local method = http_method_text(req)
if method == "GET" then
endpoints.api_get(req)
elseif method == "POST" then
endpoints.api_post(req)
end
end
print("Done with init.lua")

View File

@ -18,7 +18,6 @@ local pagenames = {
"author_edit",
"search",
"error",
"edit_bio",
}
local pages = {}
for k,v in pairs(pagenames) do

View File

@ -60,9 +60,8 @@ local function wrap(seq,format,V"sup")
end
end
]]
local function wrap(seq,format,s)
return P(seq) * Cs(((s + word + P"\n"))^0) * P(seq) / function(a)
return P(seq) * Cs((((V"marked" - s) + word + P"\n"))^1) * P(seq) / function(a)
return string.format(format,a)
end
end
@ -72,22 +71,21 @@ end
local function tag(name,format)
local start_tag = P(string.format("[%s]",name))
local end_tag = P(string.format("[/%s]",name))
return start_tag * Cs(((1 - end_tag))^0) * end_tag / function(a)
return start_tag * Cs(((1 - end_tag))^1) * end_tag / function(a)
return string.format(format,sanitize(a))
end
end
--local grammar = P(require('pegdebug').trace({
local grammar = P{
"chunk";
--regular
heading = wrap("==",[[<h2>%s</h2>]], V"underline" + V"strike" + V"italic"),
bold = wrap("'''",[[<b>%s</b>]], V"italic" + V"underline" + V"strike"),
italic = wrap("''",[[<i>%s</i>]], V"underline" + V"strike"),
underline = wrap("__",[[<u>%s</u>]], V"strike"),
strike = wrap("~~",[[<s>%s</s>]], P("blah")),
spoiler = wrap("**",[[<span class="spoiler">%s</span>]],V"spoiler2" + V"bold" + V"italic" + V"underline" + V"strike"),
spoiler2 = tag("spoiler",[[<span class="spoiler2">%s</span>]],V"spoiler" + V"bold" + V"italic" + V"underline" + V"strike"),
spoiler = wrap("**",[[<span class="spoiler">%s</span>]],V"spoiler"),
spoiler2 = tag("spoiler",[[<span class="spoiler2">%s</span>]]),
italic = wrap("''",[[<i>%s</i>]], V"italic"),
bold = wrap("'''",[[<b>%s</b>]], V"bold"),
underline = wrap("__",[[<u>%s</u>]], V"underline"),
heading = wrap("==",[[<h2>%s</h2>]], V"heading"),
strike = wrap("~~",[[<s>%s</s>]], V"strike"),
code = tag("code",[[<pre><code>%s</code></pre>]]),
greentext = P">" * (B"\n>" + B">") * Cs((V"marked" + word)^0) / function(a)
return string.format([[<span class="greentext">&gt;%s</span>]],a)
@ -99,7 +97,7 @@ local grammar = P{
plainline = (V"marked" + word)^0,
line = Cs(V"greentext" + V"pinktext" + V"plainline" + P"") * P"\n" / function(a)
if a == "\r" then
return [[<p class="spacer"></p>]]
return "<br/>"
else
return string.format("<p>%s</p>",a)
end

View File

@ -41,7 +41,7 @@ local fields
local grammar = P{
"chunk";
whitespace = S" \t\n"^0,
itm = C((P(1 - (P" " * S"+-")))^0), --go until the next '+' or '-'
itm = C(P(1-S"+-")^0), --go until the next '+' or '-'
likefield = C(P"title" + P"author") * V"whitespace" * C(P"=") * V"whitespace" * V"itm",
rangeop = P"<=" + P">=" + P">" + P"<" + P"=",
rangefield = C(P"date" + P"hits") * V"whitespace" * C(V"rangeop") * V"whitespace" * C(V"itm"),
@ -54,7 +54,7 @@ local grammar = P{
table.insert(fields.tags,{pn,"=",field})
end
end,
chunk = V"field" * (P" " * V"field")^0
chunk = V"field"^0
}
--Grammar

View File

@ -28,9 +28,8 @@ function session.get(req)
stmnt_get_session:bind_names{
key = sessionid
}
local err = db.do_sql(stmnt_get_session)
local err = util.do_sql(stmnt_get_session)
if err ~= sql.ROW then
stmnt_get_session:reset()
return nil, "No such session by logged in users"
end
local data = stmnt_get_session:get_values()
@ -57,7 +56,7 @@ function session.start(who)
sessionid = session,
authorid = who
}
local err = db.do_sql(stmnt_insert_session)
local err = util.do_sql(stmnt_insert_session)
stmnt_insert_session:reset()
assert(err == sql.DONE)
return session
@ -71,7 +70,7 @@ function session.finish(who,sessionid)
authorid = who,
sessionid = sessionid
}
local err = db.do_sql(stmnt_delete_session)
local err = util.do_sql(stmnt_delete_session)
stmnt_delete_session:reset()
assert(err == sql.DONE)
return true

View File

@ -41,13 +41,13 @@ end
function tags.set(storyid,tags)
assert(stmnt_drop_tags:bind_names{postid = storyid} == sql.OK)
db.do_sql(stmnt_drop_tags)
util.do_sql(stmnt_drop_tags)
stmnt_drop_tags:reset()
local err
for _,tag in pairs(tags) do
assert(stmnt_ins_tag:bind(1,storyid) == sql.OK)
assert(stmnt_ins_tag:bind(2,tag) == sql.OK)
err = db.do_sql(stmnt_ins_tag)
err = util.do_sql(stmnt_ins_tag)
stmnt_ins_tag:reset()
end
if err ~= sql.DONE then

View File

@ -1,51 +0,0 @@
--[[
Type checking, vaguely inspired by Python3's typing module.
]]
local types = {}
function types.positive(arg)
local is_number, err = types.number(arg)
if not is_number then
return false, err
end
if arg < 0 then
return false, string.format("was not positive")
end
return true
end
--Basic lua types
local builtin_types = {
"nil","boolean","number","string","table","function","coroutine","userdata"
}
for _,type_ in pairs(builtin_types) do
types[type_] = function(arg)
local argtype = type(arg)
if not argtype == type_ then
return false, string.format("was not a %s, was a %s",type_,argtype)
end
end
end
function types.matches_pattern(pattern)
return function(arg)
local is_string, err = types.string(arg)
if not is_string then
return false, err
end
if not string.match(arg, pattern) then
return false, string.format(
"Expected %q to match pattern %q, but it did not.",
arg,
pattern
)
end
end
end
function types.check(...)
end
return types

View File

@ -1,9 +1,83 @@
local sql = require("lsqlite3")
local config = require("config")
local types = require("types")
local util = {}
--[[
Runs an sql query and receives the 3 arguments back, prints a nice error
message on fail, and returns true on success.
]]
function util.sqlassert(...)
local r,errcode,err = ...
if not r then
error(string.format("%d: %s",errcode, err))
end
return r
end
--[[
Continuously tries to perform an sql statement until it goes through
]]
function util.do_sql(stmnt)
if not stmnt then error("No statement",2) end
local err
local i = 0
repeat
err = stmnt:step()
if err == sql.BUSY then
i = i + 1
coroutine.yield()
end
until(err ~= sql.BUSY or i > 10)
assert(i < 10, "Database busy")
return err
end
--[[
Provides an iterator that loops over results in an sql statement
or throws an error, then resets the statement after the loop is done.
]]
function util.sql_rows(stmnt)
if not stmnt then error("No statement",2) end
local err
return function()
err = stmnt:step()
if err == sql.BUSY then
coroutine.yield()
elseif err == sql.ROW then
return unpack(stmnt:get_values())
elseif err == sql.DONE then
stmnt:reset()
return nil
else
stmnt:reset()
local msg = string.format(
"SQL Iteration failed: %s : %s\n%s",
tostring(err),
db.conn:errmsg(),
debug.traceback()
)
log(LOG_CRIT,msg)
error(msg)
end
end
end
--[[
Binds an argument to as statement with nice error reporting on failure
stmnt :: sql.stmnt - the prepared sql statemnet
call :: string - a string "bind" or "bind_blob"
position :: number - the argument position to bind to
data :: string - The data to bind
]]
function util.sqlbind(stmnt,call,position,data)
assert(call == "bind" or call == "bind_blob","Bad bind call, call was:" .. call)
local f = stmnt[call](stmnt,position,data)
if f ~= sql.OK then
error(string.format("Failed to %s at %d with %q: %s", call, position, data, db.conn:errmsg()),2)
end
end
--see https://perishablepress.com/stop-using-unsafe-characters-in-urls/
--no underscore because we use that for our operative pages
local url_characters =
@ -70,8 +144,10 @@ function util.decode_id(s)
return n
end)
if res then
print("returning id:",id)
return id
else
print("Failed to decode id:" .. s)
return false,"Failed to decode id:" .. s
end
end
@ -115,6 +191,7 @@ end
--hex encoded string to arbitrary data
function util.decode_unlisted(str)
print("str was:",str)
local output = {}
for byte in str:gmatch("%x%x") do
table.insert(output, string.char(tonumber(byte,16)))
@ -147,36 +224,6 @@ function util.parse_tags(str)
return tags
end
if config.debugging then
function util.checktypes(...)
local args = {...}
if #args == 1 then
args = table.unpack(args)
end
assert(
#args % 3 == 0,
"Arguments to checktypes() must be triplets of " ..
"<variable>, <lua type>, <type check function> "
)
for i = 1,#args,3 do
local var, ltype, veri_f = args[i+0], args[i+1], args[i+2]
assert(
type(var) == ltype,
string.format(
"Expected argument %d (%q) to be type %s, but was %s",
i/3
)
)
if veri_f then
assert(veri_f(var))
end
end
end
else
function util.checktypes(...)
end
end
local function decodeentity(capture)
return string.char(tonumber(capture,16)) --Decode base 16 and conver to character
end

View File

@ -5,28 +5,11 @@
<a href="https://<%= author %>.<%= domain %>"><%= author %></a>.<a href="https://<%= domain %>"><%= domain %></a>
</h1>
<div class="container">
<div class="row">
<a href="/_paste" class="button column column-0">New paste</a>
<% if not loggedin then %>
<a href="/_login" class="button column column-0">Log in</a>
<% else %>
<a href="/_logout" class="button column column-0">Log out</a>
<a href="/_bio" class="button column column-0">Edit bio</a>
<% end %>
<span class="column column-0"></span>
<form action="https://<%= domain %>/_search" method="get" class="search column row">
<input class="column" type="text" name="q" placeholder="+greentext -dotr +hits>20"/>
<input class="column column-0 button button-clear" type="submit" value="&#x1F50E;"/>
</form>
</div>
<a href="/_paste" class="button">New paste</a>
</div>
<div class="content">
<%= bio %>
</div>
<% if bio ~= "" then %>
<div class="container">
<blockquote class="biography">
<%- bio %>
</blockquote>
</div>
<% end %>
<div class="content">
<% if #stories == 0 then %>
This author has not made any pastes yet.

View File

@ -6,7 +6,7 @@
<form action="https://<%= user %>.<%= domain %>/_edit" method="post" class="container">
<fieldset>
<div class="row">
<input type="text" name="title" placeholder="Title" class="column column-60" value="<%= title %>"></input>
<input type="text" name="title" placeholder="Title" class="column column-70" value="<%= title %>"></input>
<input type="hidden" name="story" value="<%= story %>">
<select id="pasteas" name="pasteas" class="column column-10">
<% if isanon then %>
@ -21,7 +21,7 @@
<option value="plain">Plain</option>
<option value="imageboard">Imageboard</option>
</select>
<div class="column column-20">
<div class="column column-10">
<label for="unlisted" class="label-inline">Unlisted</label>
<input
type="checkbox"

View File

@ -1,42 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<% if author then %>
<meta name="author" content="<%= author %>">
<% end %>
<% if title then %>
<title><%- title %></title>
<% else %>
<title>&#x1f351;</title>
<% end %>
<link href="/_css/milligram.css" rel="stylesheet">
<link href="/_css/style.css" rel="stylesheet">
<% if extra_load then %>
<% for _,load in ipairs(extra_load) do %>
<%- load %>
<% end %>
<% end %>
</head>
<body class="container">
<main class="wrapper">
<h1 class="title">
Edit Biography for <%= user %>
</h1>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="https://<%= user %>.<%= domain %>/_bio" method="post" class="container">
<fieldset>
<input type="hidden" name="author" value="<%= user %>">
<div class="row">
<textarea name="text" cols=80 rows=24 class="column"><%= text %></textarea><br/>
</div>
<div class="row">
<input type="submit">
</div>
</fieldset>
</form>

View File

@ -1,18 +0,0 @@
<{system cat src/pages/parts/header.etlua}>
<h1 class="title">
Edit Biography for <%= user %>
</h1>
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="https://<%= user %>.<%= domain %>/_bio" method="post" class="container">
<fieldset>
<input type="hidden" name="author" value="<%= user %>">
<div class="row">
<textarea name="text" cols=80 rows=24 class="column"><%= text %></textarea><br/>
</div>
<div class="row">
<input type="submit">
</div>
</fieldset>
</form>
<{cat src/pages/parts/footer.etlua}>

View File

@ -8,20 +8,17 @@
<div class="container">
<div class="row">
<a href="/_paste" class="button column column-0">New paste</a>
<% if not loggedin then %>
<a href="/_login" class="button column column-0">Log in</a>
<a href="/_claim" class="button column column-0">Register</a>
<% else %>
<a href="/_logout" class="button column column-0">Log out</a>
<span class="column column-0"></span>
<% end %>
<form action="https://<%= domain %>/_search" method="get" class="search column row">
<input class="column" type="text" name="q" placeholder="+greentext -dotr +hits>20"/>
<input class="column column-0 button button-clear" type="submit" value="&#x1F50E;"/>
</form>
</div>
<p>
<{ system cat src/pages/parts/motd.etlua }>
Welcome to slash.monster, stories of fiction and fantasy<br/>
Not safe for work<br/>
18+
</p>
</div>
<div class="content">

View File

@ -1,6 +1,6 @@
<tr><td>
<% if story.unlisted then %>
<% if unlisted then %>
&#9940;
<% end %>
</td><td>

View File

@ -5,12 +5,12 @@
<% if err then %><em class="error"><%= err %></em><% end %>
<form action="https://<%= domain %>/_paste" method="post" class="container"><fieldset>
<div class="row">
<input type="text" name="title" placeholder="Title" class="column column-60"></input>
<input type="text" name="title" placeholder="Title" class="column column-70"></input>
<select id="markup" name="markup" class="column column-20">
<option value="plain">Plain</option>
<option value="imageboard">Imageboard</option>
</select>
<div class="column column-20">
<div class="column column-10">
<label for="unlisted" class="label-inline">Unlisted</label>
<input type="checkbox" name="unlisted" id="unlisted"></input>
</div>

View File

@ -3,16 +3,10 @@
<a href="https://<%= domain %>"><%= domain %></a>/<a href="https://<%= domain %>/<%= idp %>"><%= idp %></a>
</nav>
<% if owner then -%>
<div class="row">
<form action="https://<%= domain %>/_edit" method="get"><fieldset>
<input type="hidden" name="story" value="<%= idp %>"/>
<input type="submit" value="edit" class="button column column-0"/>
<input type="submit" value="edit" class="button"/>
</fieldset></form>
<form action="https://<%= domain %>/_delete" method="post"><fieldset>
<input type="hidden" name="story" value="<%= idp %>"/>
<input type="submit" value="delete" class="button column column-0"/>
</fieldset></form>
</div>
<% end -%>
<article>
<h2 class="title"> <%- title %> </h2>

View File

@ -29,13 +29,10 @@ int archive(struct http_request *);
int api(struct http_request *);
int style(struct http_request *);
int miligram(struct http_request *);
int delete(struct http_request *);
int do_lua(struct http_request *req, const char *name);
int errhandeler(lua_State *);
lua_State *L;
/* These should be defined in in kore somewhere and included here */
void kore_worker_configure(void);
void kore_worker_teardown(void);
/*
static / index
static / _post post
@ -59,32 +56,32 @@ KORE_SECCOMP_FILTER("app",
);
int
errhandeler(lua_State *state){
printf("Error: %s\n",lua_tostring(state,1));//"error"
lua_getglobal(state,"debug");//"error",{debug}
lua_getglobal(state,"print");//"error",{debug},print()
lua_getfield(state,-2,"traceback");//"error",{debug},print(),traceback()
lua_call(state,0,1);//"error",{debug},print(),"traceback"
lua_call(state,1,0);//"error",{debug}
errhandeler(lua_State *L){
printf("Error: %s\n",lua_tostring(L,1));//"error"
lua_getglobal(L,"debug");//"error",{debug}
lua_getglobal(L,"print");//"error",{debug},print()
lua_getfield(L,-2,"traceback");//"error",{debug},print(),traceback()
lua_call(L,0,1);//"error",{debug},print(),"traceback"
lua_call(L,1,0);//"error",{debug}
printf("Called print()\n");
lua_getfield(state,-1,"traceback");//"error",{debug},traceback()
lua_getfield(L,-1,"traceback");//"error",{debug},traceback()
printf("got traceback\n");
lua_call(state,0,1);//"error",{debug},"traceback"
lua_pushstring(state,"\n");
lua_call(L,0,1);//"error",{debug},"traceback"
lua_pushstring(L,"\n");
printf("called traceback\n");
lua_pushvalue(state,-4);//"error",{debug},"traceback","error"
lua_pushvalue(L,-4);//"error",{debug},"traceback","error"
printf("pushed error\n");
lua_concat(state,3);//"error",{debug},"traceback .. error"
lua_concat(L,3);//"error",{debug},"traceback .. error"
printf("concated\n");
int ref = luaL_ref(state,LUA_REGISTRYINDEX);//"error",{debug}
lua_pop(state,2);//
lua_rawgeti(state,LUA_REGISTRYINDEX,ref);//"traceback .. error"
int ref = luaL_ref(L,LUA_REGISTRYINDEX);//"error",{debug}
lua_pop(L,2);//
lua_rawgeti(L,LUA_REGISTRYINDEX,ref);//"traceback .. error"
return 1;
}
int
do_lua(struct http_request *req, const char *name){
//printf("About to do lua %s\n",name);
printf("About to do lua %s\n",name);
lua_pushcfunction(L,errhandeler);
lua_getglobal(L,name);//err(),name()
if(!lua_isfunction(L,-1)){
@ -107,51 +104,61 @@ do_lua(struct http_request *req, const char *name){
int
post_story(struct http_request *req){
printf("We want to post!\n");
return do_lua(req,"paste");
}
int
edit_story(struct http_request *req){
printf("We want to edit!\n");
return do_lua(req,"edit");
}
int
edit_bio(struct http_request *req){
printf("We want to edit bio!\n");
return do_lua(req,"edit_bio");
}
int
read_story(struct http_request *req){
printf("We want to read!\n");
return do_lua(req,"read");
}
int
login(struct http_request *req){
printf("We want to login!\n");
return do_lua(req,"login");
}
int
logout(struct http_request *req){
printf("We want to log out!\n");
return do_lua(req,"logout");
}
int
claim(struct http_request *req){
printf("We want to claim!\n");
return do_lua(req,"claim");
}
int
download(struct http_request *req){
printf("We want to do download!\n");
return do_lua(req,"download");
}
int
preview(struct http_request *req){
printf("We want to do preview!\n");
return do_lua(req,"preview");
}
int
search(struct http_request *req){
printf("We want to do search!\n");
return do_lua(req,"search");
}
@ -169,11 +176,13 @@ archive(struct http_request *req){
return KORE_RESULT_OK;
}
*/
printf("We want to do archive!\n");
return do_lua(req,"archive");
}
int
api(struct http_request *req){
printf("Api call!\n");
return do_lua(req,"api");
}
@ -182,11 +191,6 @@ home(struct http_request *req){
return do_lua(req,"home");
}
int
delete(struct http_request *req){
return do_lua(req,"delete");
}
void
kore_worker_configure(void){
printf("Configuring worker...\n");

View File

@ -1,3 +0,0 @@
DELETE FROM posts
WHERE posts.id = :postid AND
posts.authorid = :authorid

View File

@ -1,3 +0,0 @@
SELECT biography
FROM authors
WHERE authors.id = :authorid;

View File

@ -3,4 +3,4 @@ FROM authors, sessions
WHERE
sessions.key = :key AND
sessions.author = authors.id AND
sessions.start - strftime('%s','now') <= 60*60*24;
sessions.start - strftime('%s','now') < 60*60*24;

View File

@ -1,3 +0,0 @@
UPDATE authors
SET biography = ?
WHERE authors.id = ?;