Compare commits

...

70 Commits

Author SHA1 Message Date
Robin Malley 1930ade3f7 Add a border around author biographies 2022-11-25 04:07:38 +00:00
Robin Malley 8b03c78346 Fix bug where author index cache is not invalidated correctly 2022-11-25 02:25:36 +00:00
Robin Malley afe144d554 Correct wrong formatting string for coroutines. 2022-11-23 22:12:04 +00:00
Robin Malley 720e826d4d Remove extranious print 2022-11-23 22:11:40 +00:00
Robin Malley 9daf7e90cd Refactoring and bugfixes
A few bugfixes for author biographies.
Also moved a few functions that used to be in "util" into "db"
2022-11-23 21:44:46 +00:00
Robin Malley 411bcb494d Allow users to edit biographies 2022-11-21 00:32:49 +00:00
Robin Malley e25d2fd06a More work on author biographies
Start work on unit tests for author biographies
Fix a bug in the biography get endpoint
Add the biography get page to the page list
2022-11-19 23:59:50 +00:00
Robin Malley 3431daee0b Refactoring 2022-09-19 22:07:35 +00:00
Robin Malley 223cfb9e46 Work on bio editing 2022-09-19 22:03:45 +00:00
Robin Malley 49d0b0a397 Move motd into it's own file. 2022-09-19 21:16:01 +00:00
Robin Malley d5ec6d6864 Get started on author biography.
Get started on implementing author biographies, add endpoints.
2022-09-02 23:32:17 +00:00
Robin Malley f5c729bfde Finish the unit test to check anonymous posting works.
Finish up a unit test that was previously marked TODO, which tests that
a story posted anonymously appears on the home page.
2022-09-02 23:23:19 +00:00
Robin Malley f0c7ae13fe Modifications to allow biography
Start work on allowing users to modify their biography
2022-09-02 23:22:47 +00:00
Robin Malley acebec5d73 Fix code coverage
Actually generate code coverage reports.
2022-09-02 23:22:08 +00:00
Robin Malley 872760c9ff Refactor makefile
* Move SPP cli flags into it's own variable
* Remove a bunch of legacy code around settinig up a chroot environment
2022-09-02 23:06:38 +00:00
Robin Malley 040587701e Rewrite config for kore 4.2.0
Kore 4.2.0 has a new format for the configuration file.
2022-06-26 23:09:28 +00:00
Robin Malley ec6aed9866 Add a types module to help refactor type checking
Type checking will be performed in unit tests, but will be an
empty function at runtime.
2022-06-26 23:08:46 +00:00
Robin Malley 647e7f2ac2 Add a unit test for posting
Add a unit test that checks that the posting api works.
2022-06-26 23:08:03 +00:00
Robin Malley 3b1d3dd910 Add function signatures
Add two function signatures that are added by kore.
2022-06-26 23:07:09 +00:00
Robin Malley e0fabca908 Better error message on failed coroutines
Add a full error message with traceback if the coroutine_iter_next()
function is passed a dead coroutine.
2022-06-26 23:05:24 +00:00
Robin Malley a0c8907f71 Fix minor bug in paste_post method
Make a variable that was global local when an error
is thrown.
2022-06-26 22:52:58 +00:00
Robin Malley 77d8c0e66b Typo corrections, add to roadmap 2022-02-20 00:21:50 +00:00
Robin Malley 138cf12028 Add a type system
Add a check_types method that can check lua types for correctness.
2022-02-20 00:17:36 +00:00
Robin Malley 87556f77cc Less fuzzing
Limit the number of runs of the markup fuzzer in order to save
time.
2022-02-20 00:13:44 +00:00
Robin Malley 63e2b0b663 Make makefile self documenting 2022-02-20 00:12:52 +00:00
Robin Malley ac3fc81741 Fix the coroutine response function 2022-02-20 00:08:07 +00:00
Robin Malley 9667aa1c3e Pretty error message when post does not have an author
Replace the basic error message when a post does not have an author
with a nice looking error message page.
2022-01-23 18:19:20 +00:00
Robin Malley 16054156a1 Finished writing cacheing tests
Cacheing tests now make sure that logged in users don't cause
pages to cache.

Also fixed creating a logged in session automatically in the mock
environment.
2021-10-21 23:44:23 +00:00
Robin Malley 68561443a5 Mark unlisted posts
There was a bug where unlisted posts would not have a marker next to
them to show they are unlisted.
2021-10-11 01:01:18 +00:00
Robin Malley 069c75b72e Add a delete button.
Add a delete button to posts that will show up if the user is logged in
and is the owner of a post. If javascript is enabled, the user will be
prompted for conformation before deleting a post.
2021-10-11 00:59:50 +00:00
Robin Malley 81ad49ae80 Fix warnings
Compileing should happen without warnings, even with extra warnings.
2021-10-11 00:57:11 +00:00
Robin Malley 680a341db5 More errors for dev builds. 2021-10-11 00:55:07 +00:00
Robin Malley 296777d3fc Add a comment to the top of the javascript. 2021-10-11 00:54:47 +00:00
Robin Malley 1487835478 Allow assets to be templated 2021-10-11 00:54:15 +00:00
Robin Malley e0a8b3d60a Add code coverage
Add another target in the makefile to run code coverage.
2021-09-12 17:37:12 +00:00
Robin Malley de76d31fe8 Bugfix add random generation
Add a file that holds all the random generation for things like
usernames and posts.
2021-09-12 16:33:29 +00:00
Robin Malley 37a9bbd63d Use random usernames to unittest
Don't replace the database with a unittest database, just use
the usual database and user randomly generated usernames so we
don't have collisions.
2021-09-12 16:26:09 +00:00
Robin Malley d5a3197262 Add logging levels for unit tests
Add logging levels for the mock environment for unit tests. Values
map to a string name instead of an integer like in the normal
environment.
2021-09-12 16:24:43 +00:00
Robin Malley c00903505b Remove debugging statement 2021-09-12 01:53:34 +00:00
Robin Malley 9dcc743199 Reset session correctly
Sometimes logging in doesn't work correctly, fix it.
2021-09-12 01:53:06 +00:00
Robin Malley 6398e97498 Add a logout button
Add a button that only shows when users are logged in, allowing
them to log out.
2021-09-12 01:47:40 +00:00
Robin Malley 4e0a23ee95 Add some files to gitignore
Add some files that are preprocessed with spp to the gitignore.
2021-09-11 21:51:54 +00:00
Robin Malley 1ddd446297 Add some unit tests to search parser.
Add some unit tests that ensure the search parser is outputting
search parameters that look correct.
2021-09-11 21:51:22 +00:00
Robin Malley 9444d300b8 Allow logged in users to use their session immediately.
Fix a bug where unit tests are attempting to log in and use their
session key in the same second that the session key was created.
2021-09-11 21:50:23 +00:00
Robin Malley 3bd07ebf6a Fix search parser
Allow + or - in the search string as long as it is not preceeded by
a space. If it is preceeded by a space, it starts a new search
constraint.
2021-09-11 21:49:15 +00:00
Robin Malley a16f2dfe02 Mark unit test as slow.
Mark a test as slow that runs fuzzing.
2021-09-11 21:46:23 +00:00
Robin Malley ffc34295e9 Add a log in the mock environment
Add a function that implements `log()` in the mock environment.
2021-09-11 21:45:29 +00:00
Robin Malley e0a8e01513 Move some tests to pending
Write a bunch of unit tests and mark some of them as pending.
2021-09-11 21:44:57 +00:00
Robin Malley ab6572314e Exclude slow tests in make test
Exclude tests that take a long time to run in the `make test`
to speed up automated unit testing.
2021-09-11 21:43:32 +00:00
Robin Malley 41f68f45b8 Add more tests 2021-08-27 01:09:29 +00:00
Robin Malley d11695b5eb Remove extra prints 2021-08-27 01:08:51 +00:00
Robin Malley eac2a38c6c Add a logout button
Add a button so users know if they're logged in.
2021-08-27 01:08:30 +00:00
Robin Malley 4913e7765e Extra cache dirtying
Some pages weren't getting dirtied appropriately on change.
2021-08-27 01:08:02 +00:00
Robin Malley f88ec0e22a Use database from config
Use the database file that came from the config file.
2021-08-27 01:06:55 +00:00
Robin Malley 7e5e38c3f2 Add database to config
Add the database to the config file so we can override it for
unittests.
2021-08-27 01:06:24 +00:00
Robin Malley e3468136e5 Add more stuff to cloc
Count etlua as html files.
2021-08-27 01:05:18 +00:00
Robin Malley 3db891800b Improve cacheing
Caches domain/a and author.domain/a as the same page.
2021-07-28 00:39:04 +00:00
Robin Malley 3b6a631dc4 hotfix 2021-04-09 20:15:01 +00:00
Robin Malley e5d1904b1f Try to hint reader mode better.
Line breaks in the imageboard parser now use an empty paragraph tag
to try to hint that the page should use reader mode.
2021-04-09 20:03:24 +00:00
Robin Malley 7cc5e8d0ef Add information to failed api requests
Add addition information when requests for tag suggestions fail.
2021-04-09 19:17:22 +00:00
Robin Malley 9e51de6c8e Fix CSRF
Domain name was hardcoded, use the config file instead.
2021-04-09 19:16:36 +00:00
Robin Malley fd87cf95ee Add bookmark script.
Add a script that saves the position on read pages to localstorage, and
restores the page position when the page is reloaded.
2021-04-04 06:11:55 +00:00
Robin Malley fdf0b67f3a Remove prints.
Remove some extranious prints.
2021-04-04 06:11:14 +00:00
Robin Malley 33a23ef20c Set samesite on cookies.
Set the SameSite attribute on all cookies issed to Lax.
2021-04-04 06:03:03 +00:00
Robin Malley 58565bc088 Remove extra print statements. 2021-04-04 05:14:53 +00:00
Robin Malley 73df8d400e Updates to imageboard parser 2021-04-04 05:13:09 +00:00
Robin Malley 53b1a19c05 Fix hangups in imageboard parser
Fix some hangups in the imageboard parser by allowing multiple
markup characters in a row to immediately close the markup
segment instead of going forward to try to find a non-markup
segment to include.
2021-03-20 06:39:46 +00:00
Robin Malley 55923a9cd6 Update readme 2021-02-22 07:46:57 +00:00
Robin Malley 8cf7344e7b Minor fixups
Add dark theme to tag suggestions, and remove printf() call.
2021-02-22 07:08:50 +00:00
Robin Malley 701800cfe2 Finish adding tag suggestions
Lots of changes, did an inital wack at adding tag suggestions.
2021-02-22 06:59:51 +00:00
65 changed files with 2386 additions and 563 deletions

3
.gitignore vendored
View File

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

View File

@ -6,7 +6,9 @@ COPY=cp
RM=rm -f
SPP=spp
CD=cd
AWK=awk
GREP=grep
SORT=sort
# Config
chroot_dir=kore_chroot/
@ -19,7 +21,8 @@ user=robin
port=8888
domain=test.monster:$(port)
#squelch prints
SPPFLAGS=-D port=$(port) -D kore_chroot=$(chroot_dir) -D chuser=$(user) -D domain=$(domain)
# squelch prints, flip to print verbose information
Q=@
#Q=
@ -38,28 +41,34 @@ 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=%)
all: $(chroot_dir) smr.so $(built_files) $(built_pages) $(built_sql)
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
$(Q)$(ECHO) "[running] $@"
$(Q)$(KODEV) run
conf/smr.conf : conf/smr.conf.in Makefile
$(Q)$(ECHO) "[preprocess] $@"
$(Q)$(SPP) -o $@ -D port=$(port) -D kore_chroot=$(chroot_dir) -D chuser=$(user) $<
$(Q)$(SPP) -o $@ $(SPPFLAGS) $<
apk-tools-static-$(version).apk:
# wget -q $(mirror)latest-stable/main/$(arch)/apk-tools-static-$(version).apk
clean:
clean: ## clean up all the files generated by this makefile
$(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:
cloc src
cloc: ## calculate source lines of code in smr
cloc --force-lang="HTML",etlua.in src assets
$(chroot_dir): apk-tools-static-$(version).apk
$(Q)$(MKDIR) $(chroot_dir)
@ -68,31 +77,6 @@ $(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)
@ -106,15 +90,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) -o $@ -D domain=$(domain) $<
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
$(page_files) : % : %.in $(part_files)
$(Q)$(ECHO) "[preprocess] $@"
$(Q)$(SPP) -o $@ $<
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
src/pages/parts/story_breif.etlua : src/pages/parts/story_breif.etlua.in
$(Q)$(ECHO) "[preprocess] $@"
$(Q)$(SPP) -o $@ $<
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
$(built_sql): $(chroot_dir)sql/%.sql : src/sql/%.sql
$(Q)$(ECHO) "[copy] $@"
@ -124,9 +108,19 @@ $(built_tests) : $(chroot_dir)% : %
$(Q)$(ECHO) "[copy] $@"
$(Q)$(COPY) $^ $@
smr.so : $(src_files) conf/smr.conf conf/build.conf
$(asset_files) : % : %.in
$(Q)$(ECHO) "[preprocess] $@"
$(Q)$(SPP) $(SPPFLAGS) -o $@ $<
smr.so : $(src_files) conf/smr.conf conf/build.conf $(asset_files)
$(Q)$(ECHO) "[build] $@"
$(Q)$(KODEV) build
test : $(built)
$(Q)$(CD) kore_chroot && busted
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."

View File

@ -6,8 +6,26 @@ 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 just over
2k SLOC and is expected to never exceed 5k SLOC. Contributions welcome.
[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
-------------------------------------------------------------------------------
```
## Roadmap
@ -15,12 +33,21 @@ be small, fast, and secure. It is built on top of [Kore](https://kore.io), using
* Comments (complete)
* Tags (complete)
* Search (complete)
* Author biographies
* Archive
* Archive (complete)
* Author biographies (complete)
* Kore 4.2.0 (complete)
* addon api
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
@ -37,9 +64,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

12
assets/bookmark.js Normal file
View File

@ -0,0 +1,12 @@
/*
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

@ -0,0 +1,36 @@
/*
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,6 +9,7 @@ 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}
@ -31,6 +32,7 @@ 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{

32
assets/suggest_tags.css Normal file
View File

@ -0,0 +1,32 @@
.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,11 +1,41 @@
console.log("Hello, world!");
tag_suggestion_list = {
list_element: null,
/*Singleton object*/
var tag_suggestion_list = {
input_el: null,
list_element: document.createElement('ol'),
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;";
}
}
var lel = document.createElement('div');
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(";");
}
}
}
/**
* Stolen from medium.com/@jh3y
@ -15,75 +45,114 @@ var lel = document.createElement('div');
* @param {number} selectionPoint - the selection point for the input
*/
function getCursorXY(input, selectionPoint){
const {
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,
}
const {
offsetLeft: inputX,
offsetTop: inputY,
} = input
const div = document.createElement('div')
const copyStyle = getComputedStyle(input)
for (const prop of copyStyle) {
div.style[prop] = copyStyle[prop]
}
const swap = '.'
const inputValue = input.tagName === 'INPUT' ? input.value.replace(/ /g, swap) : input.value
const textContent = inputValue.substr(0, selectionPoint)
div.textContent = textContent
if (input.tagName === 'TEXTAREA') div.style.height = 'auto'
if (input.tagName === 'INPUT') div.style.width = 'auto'
const span = document.createElement('span')
span.textContent = inputValue.substr(selectionPoint) || '.'
div.appendChild(span)
document.body.appendChild(div)
const { offsetLeft: spanX, offsetTop: spanY } = span
document.body.removeChild(div)
return {
x: inputX + spanX,
y: inputY + spanY,
}
}
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 sugx, sugy = getCursorXY(elem,elem.value.length);
console.log("Looking at position to display suggestions:",sugx, sugy);
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;
for(var i in sugg){
console.log("Displaying suggestion:",sugg[i]);
lel.setAttribute('style',`left: $(sugx)px; top: $(sugy)px;`);
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();
}
}
}
}
function hint_tags(elem, event){
//Get the most recent tag
recent = elem.value.split(";").pop().trim();
var 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
xhr = new XMLHttpRequest();
var 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);
}
@ -93,13 +162,34 @@ function hint_tags(elem, event){
}
function init(){
tag_el_list = document.getElementsByName("tags");
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");
console.assert(tag_el_list.length == 1);
tag_el = tag_el_list[0];
var tag_el = tag_el_list[0];
tag_suggestion_list.input_el = tag_el;
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 -Wextra
cflags=-g -Wall -Wextra -Werror
cflags=-I/usr/include/lua5.1
cxxflags=-g -Wextra
}

View File

@ -6,14 +6,21 @@ 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
@ -38,83 +45,169 @@ domain * {
#I run kore behind a lighttpd reverse proxy, so this is a bit useless to me
accesslog /dev/null
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 ^/[^_].* read_story
route / {
handler home
methods get
}
params get /_edit {
validate story v_storyid
route /_css/style.css {
handler asset_serve_style_css
methods get
}
params get /_download {
validate story v_storyid
validate pwd v_hex_128
route /_css/suggest_tags.css {
handler asset_serve_style_css
methods get
}
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
route /_css/milligram.css {
handler asset_serve_milligram_css
methods get
}
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
route /_css/milligram.min.css.map {
handler asset_serve_milligram_min_css_map
methods get
}
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
route /_faq {
handler asset_serve_faq_html
methods get
}
params get /_search {
validate q v_any
route /_js/suggest_tags.js {
handler asset_serve_suggest_tags_js
methods get
}
params get /_archive {
validate t v_time
route /_js/bookmark.js {
handler asset_serve_bookmark_js
methods get
}
params get ^/[^_].* {
validate comments v_bool
validate pwd v_hex_128
route /_js/intervine_deletion.js {
handler asset_serve_intervine_deletion_js
methods get
}
params post ^/[^_].* {
validate text v_any
validate postas v_subdomain
validate pwd v_hex_128
route /favicon.ico {
handler asset_serve_favicon_ico
methods get
}
params post /_login {
validate user v_subdomain
validate pass v_any
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
}
params post ^/_claim {
validate user v_subdomain
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
}
params get /_api {
validate call v_any
validate data v_any
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
}
# 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
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
}
}

118
spec/author_bio_spec.lua Normal file
View File

@ -0,0 +1,118 @@
_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)

120
spec/cacheing_spec.lua Normal file
View File

@ -0,0 +1,120 @@
_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)

249
spec/env_mock.lua Normal file
View File

@ -0,0 +1,249 @@
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

44
spec/fuzzgen.lua Normal file
View File

@ -0,0 +1,44 @@
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

224
spec/login_spec.lua Normal file
View File

@ -0,0 +1,224 @@
_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,6 +251,7 @@ 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)
@ -263,9 +264,9 @@ describe("smr",function()
local pagefunc = assert(require("endpoints." .. fname))
assert(type(pagefunc) == "function")
end)
it("should call http_response() at some point",function()
it("should call http_response() at some point #slow",function()
local pagefunc = require("endpoints." .. fname)
for i = 1,1000 do
for i = 1,10 do
local req = {}
req.method = method
req.path = obj.route

View File

@ -1,5 +1,9 @@
describe("smr imageboard parser",function()
function assertf(stmt, fmt, ...)
if not stmt then
error(string.format(fmt,...))
end
end
describe("smr imageboard parser #parsers",function()
it("should load without error",function()
local parser = require("parser_imageboard")
end)
@ -9,4 +13,149 @@ describe("smr imageboard parser",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

@ -0,0 +1,79 @@
_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

@ -0,0 +1,105 @@
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.
]==]

43
spec/posting_spec.lua Normal file
View File

@ -0,0 +1,43 @@
_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)

26
spec/typeing.lua Normal file
View File

@ -0,0 +1,26 @@
--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)

8
spec/utils.lua Normal file
View File

@ -0,0 +1,8 @@
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,6 +30,7 @@ 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,148 +45,157 @@ 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){
printf("Cleanup\n");
return KORE_RESULT_OK;
ret = KORE_RESULT_OK;
}else{
printf("About to call iter_next from iter_sent\n");
return coroutine_iter_next(obj);
ret = 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);
printf("status in next: %s\n",status);
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);
}
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);
printf("Done calling resume()\n");
if(err != 0){
return (KORE_RESULT_ERROR);
}
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);
printf("Error: %s\n",s);
kore_log(LOG_ERR,"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;
printf("Yielding data stream size %lld\n",size);
struct kore_buf *kb = kore_buf_alloc(0);
kore_buf_appendf(kb,"%x\r\n",size);
struct kore_buf *kb = kore_buf_alloc(4096);
kore_buf_appendf(kb,"%lu\r\n",size);
kore_buf_append(kb,data,size);
size_t ssize;
char *sstr = kore_buf_stringify(kb,&ssize);
net_send_stream(obj->c, sstr, ssize, coroutine_iter_sent, &nb);
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);
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;
printf("About to send final bit\n");
net_send_stream(obj->c, response, strlen(response) + 1, coroutine_iter_sent, &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);
nb->extra = obj;
printf("Done sending final bit\n");
lua_pop(L,lua_gettop(L));
printf("Poped everything\n");
kore_buf_free(kb);
return (KORE_RESULT_OK);
}else{
printf("Coroutine used for response returned something that was not a string:%s\n",lua_typename(L,lua_type(L,-1)));
kore_log(LOG_CRIT,"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){
printf("Disconnect routine called\n");
kore_log(LOG_ERR,"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);
free(obj);
printf("Done with disconnect\n");
luaL_unref(L,LUA_REGISTRYINDEX,Lref);
c->hdlr_extra = NULL;
}
/*
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);
req->flags |= HTTP_REQUEST_NO_CONTENT_LENGTH;
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 = req->owner;
obj->c->flags |= CONN_IS_BUSY;
obj->c = c;
obj->c->disconnect = coroutine_disconnect;
obj->c->hdlr_extra = obj;
printf("About to call iter next\n");
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);
printf("About to call iter next\n");
coroutine_iter_next(obj);
printf("Done calling iter next\n");
return 0;
}
/*
@ -438,6 +447,7 @@ lhttp_set_flags(lua_State *L){
struct http_request *req = luaL_checkrequest(L,-2);
lua_pop(L,2);
req->flags = flags;
return 0;
}
/*
@ -448,6 +458,7 @@ 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,12 +2,16 @@
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);
@ -19,6 +23,8 @@ 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,6 +9,7 @@ local sql = require("lsqlite3")
local queries = require("queries")
local util = require("util")
local db = require("db")
local ret = {}
@ -16,10 +17,11 @@ local stmnt_cache, stmnt_insert_cache, stmnt_dirty_cache
local oldconfigure = configure
function configure(...)
local cache = util.sqlassert(sql.open_memory())
local cache = db.sqlassert(sql.open_memory())
ret.cache = cache -- Expose db for testing
--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: fixme
--swapping to disk. TODO
assert(cache:exec([[
CREATE TABLE IF NOT EXISTS cache (
path TEXT PRIMARY KEY,
@ -54,7 +56,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 = util.do_sql(stmnt_cache)
local err = db.do_sql(stmnt_cache)
if err == sql.DONE then
stmnt_cache:reset()
--page is not cached
@ -72,7 +74,7 @@ function ret.render(pagename,callback)
path=pagename,
data=text,
}
err = util.do_sql(stmnt_insert_cache)
err = db.do_sql(stmnt_insert_cache)
if err == sql.ERROR or err == sql.MISUSE then
error("Failed to update cache for page " .. pagename)
end
@ -80,11 +82,13 @@ 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
}
util.do_sql(stmnt_dirty_cache)
db.do_sql(stmnt_dirty_cache)
stmnt_dirty_cache:reset()
end

View File

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

View File

@ -6,10 +6,88 @@ 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 = util.sqlassert(sql.open("data/posts.db"))
db.conn = db.sqlassert(sql.open(config.db))
function configure(...)
--Create sql tables

View File

@ -8,40 +8,21 @@ local stmnt_tags_get
local oldconfigure = configure
function configure(...)
stmnt_tags_get = util.sqlassert(db.conn:prepare(queries.select_suggest_tags))
stmnt_tags_get = db.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)
local tags = {data}
for tag in stmnt_tags_get:rows() do
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,";"))
end
local function api_get(req)
@ -55,7 +36,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-]+$"),"Bad characters in tag")
assert(data:match("^[a-zA-Z0-9,%s-]+$"),string.format("Bad characters in tag: %q",data))
return suggest_tags(req,data)
end
end

View File

@ -0,0 +1,72 @@
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

@ -0,0 +1,50 @@
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,10 +9,14 @@ 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 = util.sqlassert(db.conn:prepare(queries.insert_author))
stmnt_author_create = db.sqlassert(db.conn:prepare(queries.insert_author))
return oldconfigure(...)
end
@ -42,7 +46,7 @@ local function claim_post(req)
}
stmnt_author_create:bind_blob(2,salt)
stmnt_author_create:bind_blob(3,hash)
local err = util.do_sql(stmnt_author_create)
local err = db.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

@ -0,0 +1,64 @@
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 = util.do_sql(stmnt_download)
local err = db.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 = util.do_sql(stmnt_edit)
local err = db.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,7 +46,6 @@ 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,10 +38,19 @@ local function edit_post(req)
stmnt_author_of:bind_names{
id = storyid
}
local err = util.do_sql(stmnt_author_of)
local err = db.do_sql(stmnt_author_of)
if err ~= sql.ROW then
stmnt_author_of:reset()
error("No author found for story:" .. storyid)
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
end
local data = stmnt_author_of:get_values()
stmnt_author_of:reset()
@ -57,14 +66,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(util.do_sql(stmnt_update_raw) == sql.DONE, "Failed to update raw")
assert(db.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(util.do_sql(stmnt_update) == sql.DONE, "Failed to update text")
assert(db.do_sql(stmnt_update) == sql.DONE, "Failed to update text")
stmnt_update:reset()
tagslib.set(storyid,tags)
local id_enc = util.encode_id(storyid)
@ -72,7 +81,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 = util.do_sql(stmnt_hash)
local err = db.do_sql(stmnt_hash)
if err ~= sql.ROW then
error("Failed to get a post's hash while trying to make it unlisted")
end
@ -92,6 +101,7 @@ 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,6 +8,8 @@ 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
@ -26,7 +28,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 util.sql_rows(stmnt_index) do
for idr, title, iar, dater, author, hits in db.sql_rows(stmnt_index) do
table.insert(latest,{
url = util.encode_id(idr),
title = title,
@ -43,13 +45,12 @@ local function get_site_home(req, loggedin)
loggedin = loggedin
}
end
local function get_author_home(req)
--print("Looking at author home...")
local function get_author_home(req, loggedin)
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()
@ -57,14 +58,20 @@ local function get_author_home(req)
author = subdomain
}
end
assert(err == sql.ROW,"failed to get author:" .. subdomain .. " error:" .. tostring(err))
if err ~= sql.ROW then
stmnt_author_bio:reset()
error(string.format("Failed to get author %q error: %q",subdomain, tostring(err)))
end
local data = stmnt_author_bio:get_values()
local bio = data[1]
local bio_text = data[1]
if data[1] ~= "" then
bio_text = zlib.decompress(data[1])
end
local bio = parsers.plain(bio_text)
stmnt_author_bio:reset()
stmnt_author:bind_names{author=subdomain}
local stories = {}
for id, title, time, hits, unlisted, hash in util.sql_rows(stmnt_author) do
--print("Looking at:",id,title,time,hits,unlisted)
for id, title, time, hits, unlisted, hash in db.sql_rows(stmnt_author) do
if unlisted == 1 and author == subdomain then
local url = util.encode_id(id) .. "?pwd=" .. util.encode_unlisted(hash)
table.insert(stories,{
@ -112,18 +119,19 @@ local function index_get(req)
end)
elseif host == config.domain and author then
--Display home page with "log out" button
text = cache.render(config.domain .. "-logout", function()
local cachepath = string.format("%s-logout",config.domain)
text = cache.render(cachepath, 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)
return get_author_home(req, author ~= nil)
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)
text = get_author_home(req, author ~= nil)
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 = util.do_sql(stmnt_author_acct)
local err = db.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,7 +35,10 @@ local function login_post(req)
local hash = sha3(todigest)
if hash == passhash then
local mysession = session.start(id)
http_response_cookie(req,"session",mysession,"/",0,0)
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
))
local loc = string.format("https://%s.%s",name,config.domain)
http_response_header(req,"Location",loc)
http_response(req,303,"")

View File

@ -13,7 +13,6 @@ 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{
@ -26,7 +25,6 @@ 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,16 +43,15 @@ 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))
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)
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)
stmnt_paste:reset()
if err == sql.DONE then
local rowid = stmnt_paste:last_insert_rowid()
@ -63,7 +62,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 = util.do_sql(stmnt_raw)
err = db.do_sql(stmnt_raw)
stmnt_raw:reset()
if err ~= sql.DONE then
local msg = string.format(
@ -113,9 +112,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)
util.sqlbind(stmnt_paste,"bind",6,ps.unlisted)
util.sqlbind(stmnt_paste,"bind_blob",7,textsha3)
local err = util.do_sql(stmnt_paste)
db.sqlbind(stmnt_paste,"bind",6,ps.unlisted)
db.sqlbind(stmnt_paste,"bind_blob",7,textsha3)
local err = db.do_sql(stmnt_paste)
stmnt_paste:reset()
if err == sql.DONE then
local rowid = stmnt_paste:last_insert_rowid()
@ -126,7 +125,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 = util.do_sql(stmnt_raw)
err = db.do_sql(stmnt_raw)
stmnt_raw:reset()
if err ~= sql.DONE then
local msg = string.format(
@ -149,6 +148,7 @@ 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("sqlite3")
local sql = require("lsqlite3")
local session = require("session")
local tags = require("tags")
@ -8,6 +8,7 @@ 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
@ -27,7 +28,7 @@ local function add_view(storyid)
stmnt_update_views:bind_names{
id = storyid
}
local err = util.do_sql(stmnt_update_views)
local err = db.do_sql(stmnt_update_views)
assert(err == sql.DONE, "Failed to update view counter:"..tostring(err))
stmnt_update_views:reset()
end
@ -41,7 +42,7 @@ local function populate_ps_story(req,ps)
stmnt_read:bind_names{
id = ps.storyid,
}
local err = util.do_sql(stmnt_read)
local err = db.do_sql(stmnt_read)
if err == sql.DONE then
--We got no story
stmnt_read:reset()
@ -80,7 +81,7 @@ local function get_comments(req,ps)
id = ps.storyid
}
local comments = {}
for com_author, com_isanon, com_text in util.sql_rows(stmnt_comments) do
for com_author, com_isanon, com_text in db.sql_rows(stmnt_comments) do
table.insert(comments,{
author = com_author,
isanon = com_isanon == 1, --int to boolean
@ -97,6 +98,10 @@ 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
@ -148,7 +153,6 @@ local function read_get(req)
table.insert(params,"pwd=" .. hashstr)
end
local cachestrparts = {
ps.host,
ps.path,
}
if #params > 0 then
@ -173,6 +177,12 @@ 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 = util.do_sql(stmnt_comment_insert)
local err = db.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.")

9
src/lua/global.lua Normal file
View File

@ -0,0 +1,9 @@
-- 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,39 +11,17 @@ local et = require("etlua")
local sql = require("lsqlite3")
local zlib = require("zlib")
--stub for detouring
--stubs 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(...)
@ -57,71 +35,79 @@ function configure(...)
end
print("Created configure function")
function home(req)
local method = http_method_text(req)
if method == "GET" then
endpoints.index_get(req)
-- 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
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
end
--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 method == "GET" then
endpoints.claim_get(req)
elseif method == "POST" then
endpoints.claim_post(req)
_G[funcname] = function(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))
end
spec[method](req)
end
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
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")
log(LOG_INFO,string.format("Associateing endpoint %q", funcname))
end
function teardown()
@ -135,30 +121,4 @@ 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,6 +18,7 @@ local pagenames = {
"author_edit",
"search",
"error",
"edit_bio",
}
local pages = {}
for k,v in pairs(pagenames) do

View File

@ -60,8 +60,9 @@ local function wrap(seq,format,V"sup")
end
end
]]
local function wrap(seq,format,s)
return P(seq) * Cs((((V"marked" - s) + word + P"\n"))^1) * P(seq) / function(a)
return P(seq) * Cs(((s + word + P"\n"))^0) * P(seq) / function(a)
return string.format(format,a)
end
end
@ -71,21 +72,22 @@ 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))^1) * end_tag / function(a)
return start_tag * Cs(((1 - end_tag))^0) * end_tag / function(a)
return string.format(format,sanitize(a))
end
end
--local grammar = P(require('pegdebug').trace({
local grammar = P{
"chunk";
--regular
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"),
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"),
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)
@ -97,7 +99,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 "<br/>"
return [[<p class="spacer"></p>]]
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-S"+-")^0), --go until the next '+' or '-'
itm = C((P(1 - (P" " * 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"^0
chunk = V"field" * (P" " * V"field")^0
}
--Grammar

View File

@ -28,8 +28,9 @@ function session.get(req)
stmnt_get_session:bind_names{
key = sessionid
}
local err = util.do_sql(stmnt_get_session)
local err = db.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()
@ -56,7 +57,7 @@ function session.start(who)
sessionid = session,
authorid = who
}
local err = util.do_sql(stmnt_insert_session)
local err = db.do_sql(stmnt_insert_session)
stmnt_insert_session:reset()
assert(err == sql.DONE)
return session
@ -70,7 +71,7 @@ function session.finish(who,sessionid)
authorid = who,
sessionid = sessionid
}
local err = util.do_sql(stmnt_delete_session)
local err = db.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)
util.do_sql(stmnt_drop_tags)
db.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 = util.do_sql(stmnt_ins_tag)
err = db.do_sql(stmnt_ins_tag)
stmnt_ins_tag:reset()
end
if err ~= sql.DONE then

51
src/lua/types.lua Normal file
View File

@ -0,0 +1,51 @@
--[[
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,83 +1,9 @@
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 =
@ -144,10 +70,8 @@ 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
@ -191,7 +115,6 @@ 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)))
@ -224,6 +147,36 @@ 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,11 +5,28 @@
<a href="https://<%= author %>.<%= domain %>"><%= author %></a>.<a href="https://<%= domain %>"><%= domain %></a>
</h1>
<div class="container">
<a href="/_paste" class="button">New paste</a>
</div>
<div class="content">
<%= bio %>
<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>
</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-70" value="<%= title %>"></input>
<input type="text" name="title" placeholder="Title" class="column column-60" 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-10">
<div class="column column-20">
<label for="unlisted" class="label-inline">Unlisted</label>
<input
type="checkbox"

42
src/pages/edit_bio.etlua Normal file
View File

@ -0,0 +1,42 @@
<!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

@ -0,0 +1,18 @@
<{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,17 +8,20 @@
<div class="container">
<div class="row">
<a href="/_paste" class="button column column-0">New paste</a>
<a href="/_login" class="button column column-0">Log in</a>
<a href="/_claim" class="button column column-0">Register</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>
Welcome to slash.monster, stories of fiction and fantasy<br/>
Not safe for work<br/>
18+
<{ system cat src/pages/parts/motd.etlua }>
</p>
</div>
<div class="content">

View File

View File

@ -1,6 +1,6 @@
<tr><td>
<% if unlisted then %>
<% if story.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-70"></input>
<input type="text" name="title" placeholder="Title" class="column column-60"></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-10">
<div class="column column-20">
<label for="unlisted" class="label-inline">Unlisted</label>
<input type="checkbox" name="unlisted" id="unlisted"></input>
</div>

View File

@ -3,10 +3,16 @@
<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"/>
<input type="submit" value="edit" class="button column column-0"/>
</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,10 +29,13 @@ 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
@ -56,32 +59,32 @@ KORE_SECCOMP_FILTER("app",
);
int
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}
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}
printf("Called print()\n");
lua_getfield(L,-1,"traceback");//"error",{debug},traceback()
lua_getfield(state,-1,"traceback");//"error",{debug},traceback()
printf("got traceback\n");
lua_call(L,0,1);//"error",{debug},"traceback"
lua_pushstring(L,"\n");
lua_call(state,0,1);//"error",{debug},"traceback"
lua_pushstring(state,"\n");
printf("called traceback\n");
lua_pushvalue(L,-4);//"error",{debug},"traceback","error"
lua_pushvalue(state,-4);//"error",{debug},"traceback","error"
printf("pushed error\n");
lua_concat(L,3);//"error",{debug},"traceback .. error"
lua_concat(state,3);//"error",{debug},"traceback .. error"
printf("concated\n");
int ref = luaL_ref(L,LUA_REGISTRYINDEX);//"error",{debug}
lua_pop(L,2);//
lua_rawgeti(L,LUA_REGISTRYINDEX,ref);//"traceback .. error"
int ref = luaL_ref(state,LUA_REGISTRYINDEX);//"error",{debug}
lua_pop(state,2);//
lua_rawgeti(state,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)){
@ -104,61 +107,51 @@ 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");
}
@ -176,13 +169,11 @@ 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");
}
@ -191,6 +182,11 @@ 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");

3
src/sql/delete_post.sql Normal file
View File

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

View File

@ -0,0 +1,3 @@
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;

3
src/sql/update_bio.sql Normal file
View File

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