summaryrefslogblamecommitdiffstats
path: root/web/labitrackd.lua
blob: f30dd425e419ef164e033ebac8edcfb3807a6656 (plain) (tree)































                                                                                             






























                                                   











                                                          
                                     

























































                                                               

                                             


































                                                                       


















                                                            
















                                                                                                                                            

                                                                                                                                                                                                                                                                                                                   











                                                                       








                                      







                                                                           






                                                                                                                      



                                                                               
                                                      






                                                            





















































                                                                                                            






                                           





                                                
                                                          








                                                 



                                           
                                                                                            





                                       
                                                                

                                                                                                                   
                                                                







                                                                                                                     
                                       
 
                                              











                                        

                                                          


                                                            
                                                                                            



















                                                             
#!/usr/bin/env lem
--
-- This file is part of blipserver.
-- Copyright 2011 Emil Renner Berthing
--
-- blipserver is free software: you can redistribute it and/or
-- modify it under the terms of the GNU General Public License as
-- published by the Free Software Foundation, either version 3 of
-- the License, or (at your option) any later version.
--
-- blipserver is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with blipserver.  If not, see <http://www.gnu.org/licenses/>.
--

local function usage()
	print('Labitrack daemon')
	print('usage:    '..arg[0]..' [bind [queue_dir]]')
	print('defaults: bind=*:8080 queue_dir=./queue')
	os.exit(1)
end

--
-- settings
--
local pg_connect_str = 'host=localhost user=labitrack dbname=labitrack password=nerfyoawdAj3'
local bind = arg[1] or '*:8080'
local queue_dir = arg[2] or './queue'

local acl_labitat = {
	create = true,
	update = true,
	printing = true,
}

local acl_everywhere = {
	create = false,
	update = false,
	printing = false,
}

local labitat_addresses = {
	'185.38.175.',
	'2a01:4260:1ab:',
	'10.42.',
}

local function getpermisions(ip)
	if not ip then return acl_everywhere end
	for i=1,#labitat_addresses do
		local prefix = labitat_addresses[i]
		if ip:sub(1,#prefix) == prefix then
			return acl_labitat
		end
	end
	return acl_everywhere
end


--
-- end of settings
--

local bind_colon = string.find(bind, ':', 1, true)
if bind_colon == nil then
	usage()
end
local bind_addr = string.sub(bind, 1, bind_colon-1)
local bind_port = tonumber(string.sub(bind, bind_colon+1))

local utils        = require 'lem.utils'
local io           = require 'lem.io'
local postgres     = require 'lem.postgres'
local qpostgres    = require 'lem.postgres.queued'
local hathaway     = require 'lem.hathaway'
local json         = require 'dkjson'
local base64       = require 'base64'
local filequeue    = require 'filequeue'

local assert = assert
local format = string.format
local tonumber = tonumber

local function sendfile(content, path)
	return function(req, res)
		res.headers['Content-Type'] = content
		res.file = path
	end
end

local function sendfile_js(path)
	return sendfile('text/javascript; charset=UTF-8', path)
end

local function sendfile_css(path)
	return sendfile('text/css; charset=UTF-8', path)
end

local function deserialize_array_from_pg(pgarray)
	local ret, rc = {}, 0
	if pgarray == nil then
		return ret
	end
	local strlen = string.len(pgarray)
	if strlen <= 2 then
		return ret
	end
	local n = 0
	for i = 2, strlen-1 do
		if i > n then
			rc = rc + 1
			n = string.find(pgarray, ',', i)
			if n == nil then
				n = strlen
			end
			ret[rc] = string.sub(pgarray, i, n-1)
		end
	end
	return ret
end

local function add_json_row(res, values, i)
	local n = #values
	if n > 0 then
		local clen = #values[0]
		local point = values[i]
		local d = {}
		for j = 1, clen do
			local k = values[0][j]
			local v = point[j]
			if k == nil then
			elseif k == 'id' then
				d[k] = tonumber(v)
			elseif k == 'created' then
				d[k] = tonumber(v)
			elseif k == 'updated' then
				d[k] = tonumber(v)
			elseif not (k == 'tags') then
				d[k] = v
			else
				d[k] = deserialize_array_from_pg(v)
			end
		end
		res:add('%s', json.encode(d))
	end
end


local function add_json(res, values)
	local n = #values
	res:add('[')
	if n > 0 then
		for i = 1, n do
			add_json_row(res, values, i)
			if not (i == n) then
				res:add(',')
			end
		end
	end
	res:add(']')
end

local function set_json_nocache_headers(res)
	res.headers['Content-Type'] = 'application/json; charset=UTF-8'
	res.headers['Cache-Control'] = 'max-age=0, must-revalidate'
end

local function forbidden(req, res)
	set_json_nocache_headers(res)
	res.status = 403
	res:add('{"count": %d, "objects":', count());
	add_json(res, assert(db:run('since', (since-1)*10)))
	res:add('}');
end

local function allow(perm, req, res)
	local ip = req.headers['x-real-ip']
	local p = getpermisions(ip)
	if p[perm] then
		return true
	else
		forbidden(req, res)
		return false
	end
end

local function unescape(s)
	s = string.gsub(s, "+", " ")
	s = string.gsub(s, "%%(%x%x)", function (h)
		return string.char(tonumber(h, 16))
	end)
	return s
end

-- Connect to database and prepare statements
local db = assert(qpostgres.connect(pg_connect_str))
local cols = 'id, name, "desc", tags, extract(epoch from created)::int8 created, extract(epoch from updated)::int8 updated'
assert(db:prepare('get',    'SELECT '..cols..' FROM objects WHERE id = $1'))
assert(db:prepare('recent', 'SELECT '..cols..' FROM objects ORDER BY updated DESC LIMIT 10;'))
assert(db:prepare('since',  'SELECT '..cols..' FROM objects ORDER BY id LIMIT 10 OFFSET $1;'))
assert(db:prepare('insert', 'INSERT INTO objects (name, "desc", tags) VALUES ($1, $2, string_to_array($3, \',\')::text[]) RETURNING id;'))
assert(db:prepare('update', 'UPDATE objects SET name=$2, "desc"=$3, tags=string_to_array($4, \',\')::text[], updated=now() WHERE id = $1;'))
assert(db:prepare('count',  'SELECT COUNT(id) FROM objects;'))
assert(db:prepare('search', 'SELECT * FROM (SELECT row_number() over (order by rank desc, updated desc) as rn, * FROM (SELECT count(*) over () as cnt, '..cols..', ts_rank_cd(textsearch, query) AS rank FROM objects, to_tsquery($1) query WHERE query @@ textsearch) ss1) ss2 where rn between $2+1 and $2+10;'))
assert(db:prepare('search_plain', 'SELECT plainto_tsquery($1);'))

local function count()
	return assert(db:run('count'))[1][1]
end

-- initialize queue
local queue = filequeue.open(queue_dir)

hathaway.import()

local htmlpage = sendfile('text/html; charset=UTF-8', 'pub/index.html')

GET('/',                     htmlpage)
GET('/browse',               htmlpage)
GETM('^/browse/page/(%d+)$', htmlpage)
GET('/recent',               htmlpage)
GET('/about',                htmlpage)
GETM('^/view/(%d+)$',        htmlpage)
GETM('^/edit/(%d+)$',        htmlpage)
GET('/search',               htmlpage)
GETM('^/search/',            htmlpage)

GET('/js/corelibs.min.js',  sendfile_js('js/dist/corelibs.min.js'))
GET('/js/corelibs.src.js',  sendfile_js('js/dist/corelibs.src.js'))
GET('/js/qrcode.min.js',    sendfile_js('js/dist/qrcode.min.js'))
GET('/js/labitrack.min.js', sendfile_js('js/dist/labitrack.min.js'))
GET('/js/labitrack.src.js', sendfile_js('js/dist/labitrack.src.js'))
GET('/js/templates.js',     sendfile_js('templates/dist/labitrack.min.js'))

GET('/bootstrap/css/bootstrap.css', sendfile_css('pub/bootstrap/css/bootstrap.css'))
GET('/bootstrap/css/bootstrap.min.css', sendfile_css('pub/bootstrap/css/bootstrap.min.css'))
GET('/bootstrap/css/bootstrap-responsive.css', sendfile_css('pub/bootstrap/css/bootstrap-responsive.css'))
GET('/bootstrap/css/bootstrap-responsive.min.css', sendfile_css('pub/bootstrap/css/bootstrap-responsive.min.css'))
GET('/bootstrap/img/glyphicons-halflings-white.png', sendfile_css('pub/bootstrap/img/glyphicons-halflings-white.png'))
GET('/bootstrap/img/glyphicons-halflings.png', sendfile_css('pub/bootstrap/img/glyphicons-halflings.png'))

GET('/css/labitrack.css',           sendfile_css('css/dist/labitrack.min.css'))

GET('/favicon.ico', sendfile('image/x-icon', 'pub/favicon.ico'))

GETM('^/browse/(%d+).json$', function(req, res, since)
	set_json_nocache_headers(res)

	res:add('{"count": %d, "objects":', count());
	add_json(res, assert(db:run('since', (since-1)*10)))
	res:add('}');
end)

local function urldecode(str)
	return str:gsub('+', ' '):gsub('%%(%x%x)', function (str)
		return string.char(tonumber(str, 16))
	end)
end
local function parse_qs(str)
	local t = {}
	for k, v in str:gmatch('([^&]+)=([^&]*)') do
		t[urldecode(k)] = urldecode(v)
	end
	return t
end

GETM('^/search.json%??(.*)$', function(req, res, rawqs)
	set_json_nocache_headers(res)

	qs = parse_qs(rawqs)

	q = qs['q']

	if q == nil then
		q = ''
	end

	offset = qs['offset']
	if offset == nil then
		offset = 0
	end

	_, result = pcall(db.run, db, 'search', q, offset)

	if result == nil then
		q = assert(db:run('search_plain', q))[1][1]
		result = assert(db:run('search', q, offset))
	end

	local n = #result
	if n > 0 then
		cols = #result[0]
		setmetatable(result[0], { __len = function(op)
			return cols
		end })
		result[0][1] = nil
		result[0][2] = nil
		cnt = result[1][2]

		res:add('{"count": %d, "offset": %d, "query": %s, "objects":', cnt, offset, json.encode(q));
		add_json(res, result)
		res:add('}');
	else
		res:add('{"count": 0, "offset": 0, "query": %s}', json.encode(q));
	end
end)

GET('/permissions.json', function(req, res)
	set_json_nocache_headers(res)
	local ip = req.headers['x-real-ip']
	local p = getpermisions(ip)
	res:add('%s', json.encode(p))
end)

GET('/queue.json', function(req, res)
	set_json_nocache_headers(res)
	res:add('%s', json.encode(queue:stat()))
end)

GET('/queue.json?empty', function(req, res)
	if not allow('printing', req, res) then return end
	set_json_nocache_headers(res)
	res:add('%s', json.encode(queue:empty()))
end)

GET('/recent.json', function(req, res)
	set_json_nocache_headers(res)
	add_json(res, assert(db:run('recent')))
end)

local function save_or_update(req, res)
	set_json_nocache_headers(res)

	local expected = "application/json"
	assert(string.sub(req.headers['content-type'], 1, string.len(expected)) == expected)

	local body = req:body()
	local label = json.decode(body)

	local id
	if label['id'] == nil then
		if not allow('create', req, res) then return end
		id = assert(db:run('insert', label['name'], label['desc'], table.concat(label['tags'], ',')))[1][1]
	else
		if not allow('update', req, res) then return end
		assert(db:run('update', label['id'], label['name'], label['desc'], table.concat(label['tags'], ',')))
		id = label['id']
	end

	res:add('{"id": %d}', id)
end

POST('/o', save_or_update)
PUTM('^/o/(%d+).json$', save_or_update)

GETM('^/o/(%d+).json$', function(req, res, id)
	set_json_nocache_headers(res)

	qr = db:run('get', id)
	if #qr == 0 then
		res.status = 404
	else
		add_json_row(res, qr, 1)
	end
end)


POST('/print.json', function(req, res)
	if not allow('printing', req, res) then return end

	set_json_nocache_headers(res)

	local expected = "application/x-www-form-urlencoded"
	assert(string.sub(req.headers['content-type'], 1, string.len(expected)) == expected)

	local body = unescape(req:body())
	expected = "image=data:image/png;base64,"
	local expected_len = string.len(expected)
	assert(string.sub(body, 1, expected_len) == expected)
	image = base64.dec(string.sub(body, expected_len+1))

	local qi = queue:new()
	local outfile = qi:open("wb")
	outfile:write(image)
	outfile:close()
	qi:queue();

	res:add('["%s"]', 'OK')
end)

hathaway.debug = print
assert(Hathaway(bind_addr, bind_port))

-- vim: syntax=lua ts=2 sw=2 noet: