Initial Commit
[ancientarts.git] / api / blog.lua
1 box.cfg {
2 log_level = 5;
3 }
4
5 local log = require('log')
6 local crypto = require('crypto')
7 local digest = require('digest')
8 local config = require('config')
9 local fio = require('fio')
10 local yaml = require('yaml')
11 local json = require('json')
12 local aes_key = digest.urandom(32)
13 local aes_iv = digest.urandom(16)
14
15 -- Init configuration
16 config.output_directory = config.output_directory or fio.pathjoin(config.hugo_directory,'public')
17 local hugo_config, err = fio.open(fio.pathjoin(config.hugo_directory,"config.yaml"),{'O_WRONLY','O_CREAT'})
18 if (err) then error(err) end
19 hugo_config:truncate(0)
20 hugo_config:write(yaml.encode(config.hugo_config))
21 hugo_config:close()
22 fio.chmod(fio.pathjoin(config.hugo_directory,"config.yaml"),tonumber('0664',8))
23
24 box.once('init', function()
25 box.schema.create_space('users')
26 box.space.users:format({
27 {name='email',type='string'},
28 {name='pass', type='string'},
29 {name='salt', type='string'},
30 {name='token',type='string'},
31 {name='role', type='unsigned'}
32 })
33 box.space.users:create_index(
34 'primary', {unique=true, type='HASH',parts={1,'string'}}
35 )
36 box.schema.user.grant('guest','read,write,execute','universe')
37 local usalt = digest.urandom(32)
38 local upass = digest.pbkdf2(config.admin_password, usalt)
39 box.space.users:insert({config.admin_email, upass, usalt, '', 1})
40 end)
41
42 local create_account = function(spec)
43 if auth_user:match("[A-Za-z0-9%.%%%+%-]+@[A-Za-z0-9%.%%%+%-]+%.%w%w%w?%w?") == nil then
44 box.error(1,auth_user.." is not a valid e-mail address")
45 end
46 end
47
48 local make_token = function(user)
49 local token = digest.urandom(32)
50 box.space.users:update(user.email,{{'=',4,token}})
51 return digest.base64_encode(digest.aes256cbc.encrypt(user.email..':'..token,aes_key,aes_iv),{urlsafe=true})
52 end
53
54 local text_response = function(req, code, msg, headers)
55 local resp = req:render({text=msg or ""})
56 resp.status = code
57 if headers then for key,val in pairs(headers) do resp.headers[key] = val end end
58 return resp
59 end
60
61 local json_response = function(req, code, msg, headers)
62 local resp = req:render({json=msg or "{}"})
63 resp.status = code
64 if headers then for key,val in pairs(headers) do resp.headers[key] = val end end
65 return resp
66 end
67
68 local auth_methods = {
69 Basic = function(auth_encoded)
70 local auth_user, auth_pass = digest.base64_decode(auth_encoded):match("(.*):(.*)")
71 local user = box.space.users:select({auth_user})[1]
72 if not user or digest.pbkdf2(auth_pass, user.salt) ~= user.pass then return nil end
73 return user
74 end,
75 Bearer = function(auth_encoded)
76 local token_enc = digest.base64_decode(auth_encoded)
77 if token_enc:len() ~= 64 then return nil end
78 local token
79 local decrypt = function(enc,key,iv) token = digest.aes256cbc.decrypt(token_enc,aes_key,aes_iv) end
80 if not pcall(decrypt) then return nil end
81 local auth_user, auth_token = token:match("(.*):(.*)")
82 local user = box.space.users:select({auth_user})[1]
83 if not user or auth_token ~= user.token then return nil end
84 return user
85 end
86 }
87
88 local file_commands = {
89 GET = function(user, req, target)
90 if not fio.path.exists(target) then return text_response(req,404) end
91 if fio.path.is_dir(target) then
92 local files = fio.listdir(target)
93 local file_dict = {}
94 for i, f in ipairs(files) do file_dict[f] = fio.path.is_dir(fio.pathjoin(target,f)) end
95 return json_response(req,200,json.encode(file_dict))
96 end
97 local file, err = fio.open(target, {'O_RDONLY'})
98 if err then return text_response(req,500,err) end
99 local data = file:read()
100 file:close()
101 return text_response(req,200,data)
102 end,
103 POST = function(user, req, target)
104 if fio.path.is_dir(target) then return text_response(req,405,"File is a directory") end
105 fio.mktree(fio.dirname(target))
106 local file, err = fio.open(target, {'O_WRONLY','O_CREAT'})
107 if err then return text_response(req,500,err) end
108 file:truncate(0)
109 file:write(req:read())
110 file:close()
111 fio.chmod(target,tonumber('0664',8))
112 os.execute('hugo -s '..config.hugo_directory..' -d '..config.output_directory)--..' 1> /dev/null')
113 return text_response(req,200)
114 end,
115 PUT = function(user, req, target)
116 if fio.path.is_dir(target) then return text_response(req,405,"File is a directory") end
117 fio.mktree(fio.dirname(target))
118 local file, err = fio.open(target, {'O_WRONLY','O_CREAT','O_APPEND'})
119 if err then return text_response(req,500,err) end
120 file:write(req:read())
121 file:close()
122 fio.chmod(target,tonumber('0664',8))
123 os.execute('hugo -s '..config.hugo_directory..' -d '..config.output_directory)--..' 1> /dev/null')
124 return text_response(req,200)
125 end,
126 DELETE = function(user, req, target)
127 if not fio.path.exists(target) then return text_response(req,404) end
128 if fio.path.is_dir(target) then
129 local _ok, err = fio.rmtree(target)
130 if not _ok then return text_response(req,500,err) end
131 else
132 os.remove(target)
133 end
134 os.execute('hugo -s '..config.hugo_directory..' -d '..config.output_directory)--..' 1> /dev/null')
135 return text_response(req,200)
136 end
137 }
138
139 local routes = {
140 ['/login'] = function(user,req)
141 local token = make_token(user)
142 local token_header = {
143 ["Set-Cookie"] = { "auth_token="..token.."; Secure; HttpOnly; SameSite=Strict", "live-edit=true; path=/" },
144 ["Location"] = ".."
145 }
146 local response = text_response(req,302,"",token_header)
147 return response
148 end,
149 }
150
151 local function webapi(self,req)
152 local user = nil
153 local auth_cookie = req:cookie('auth_token')
154 log.info(req.headers.authorization)
155 log.info(auth_cookie)
156 if req.headers.authorization then
157 local auth_type, auth_encoded = req.headers.authorization:match("(.*) (.*)")
158 local auth_func = auth_methods[auth_type]
159 if auth_func then user = auth_func(auth_encoded) end
160 end
161 if not user and auth_cookie then user = auth_methods.Bearer(auth_cookie) end
162 if not user then return text_response(req,401,nil,{['WWW-Authenticate']='Basic'}) end
163 local command = routes[req.path]
164 if not command then
165 local path = req.path:match("^([-_a-zA-Z0-9/%.]*)")
166 local target = fio.abspath(fio.pathjoin(config.hugo_directory,path))
167 if not target:match("^"..config.hugo_directory) then
168 return text_response(req,403)
169 end
170 command = file_commands[req.method]
171 if not command then return text_response(req,405) end
172 return command(user,req,target)
173 end
174 return command(user,req)
175 end
176
177 require('http.server').new(config.host,config.port,{handler=webapi,display_errors=true,log_errors=true,log_requests=true}):start()