1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
|
add = (tmpl) ->
package.path ..= ";#{tmpl}.lua"
package.moonpath ..= ";#{tmpl}.moon"
add '?'
add '?.server'
add '?/init'
add '?/init.server'
require 'mmm'
export UNSAFE
UNSAFE = true
import dir_base, Key, Fileder from require 'mmm.mmmfs.fileder'
import convert, MermaidDebugger from require 'mmm.mmmfs.conversion'
import get_store from require 'mmm.mmmfs.stores'
import Browser from require 'mmm.mmmfs.browser'
import init_cache from require 'mmm.mmmfs.cache'
import decodeURI from require 'http.util'
server = require 'http.server'
headers = require 'http.headers'
class Server
new: (@store, opts={}) =>
opts = {k,v for k,v in pairs opts}
opts.host = 'localhost' unless opts.host
opts.port = 8000 unless opts.port
opts.onstream = @\stream
@server = server.listen opts
@flags = opts.flags
if @flags.unsafe == nil
@flags.unsafe = opts.host == 'localhost' or opts.host == '127.0.0.1'
if @flags.cache
@root = Fileder @store
init_cache!
if @flags['http-cache']
root = @root or Fileder @store
@revision = root\get 'revision: git/hash'
@revision or= os.date!
@revision = @revision\gsub '%s+$', ''
-- @TODO: fix UNSAFE!
UNSAFE = @flags.unsafe
listen: =>
assert @server\listen!
_, ip, port = @server\localname!
print "[#{@@__name}]",
"running at #{ip}:#{port}",
"[#{table.concat [flag for flag,on in pairs @flags when on], ', '}]"
assert @server\loop!
handle_interactive: (fileder, facet) =>
root = @root or Fileder @store
browser = Browser root, fileder.path, facet.name
convert 'mmm/dom+interactive', 'text/html', browser\todom!, fileder, facet.name
handle_debug: (fileder, facet) =>
debugger = MermaidDebugger!
fileder\find facet, nil, nil, nil, debugger
print debugger\render!
convert 'text/mermaid-graph', 'text/html', debugger\render!, fileder, facet.name
handle: (method, path, facet, value) =>
if method != 'GET' and method != 'HEAD'
return 501, "not implemented"
root = @root or Fileder @store
export BROWSER
BROWSER = :path
fileder = root\walk path
if not fileder
-- fileder not found
return 404, "fileder '#{path}' not found"
val = switch facet.name
when '?index', '?tree'
-- serve fileder index
-- '?index': one level deep
-- '?tree': recursively
depth = if facet.name == '?tree' then -1 else 1
index = @store\get_index path, depth
convert 'table', facet.type, index, fileder, facet.name
else
if facet.type == '?'
facet.type = 'text/html'
current = fileder
while current
if type = current\get '_web_view: type'
facet.type = type\match '^%s*(.-)%s*$'
break
if current.path == ''
break
path, _ = dir_base current.path
current = root\walk path
if facet.type == 'text/html+interactive'
@handle_interactive fileder, facet
else if base = facet.type\match '^DEBUG %-> (.*)'
facet.type = base
@handle_debug fileder, facet
else if not fileder\has_facet facet.name
return 404, "facet '#{facet.name}' not found in fileder '#{path}'"
else
fileder\get facet
if val
200, val
else
406, "cant convert facet '#{facet.name}' to '#{facet.type}'"
get_etag: (path, facet) =>
return unless @revision
facet = tostring facet
path = path\gsub '"', '_'
facet = facet\gsub '"', '_'
etag = "#{@revision}:#{path}: #{facet}"
string.format '%q', etag
err_and_trace = (msg) -> debug.traceback msg, 2
stream: (sv, stream) =>
req = stream\get_headers!
method = req\get ':method'
path = req\get ':path'
path = decodeURI path
path_facet, typ = path\match '(.*):(.*)'
path_facet or= path
path, facet = path_facet\match '(.*)/([^/]*)'
facet = if facet == '' and (not typ or typ == '') and method ~= 'GET' and method ~= 'HEAD'
nil
else
typ or= '?'
typ = typ\match '%s*(.*)'
Key facet, typ
res = headers.new!
etag = @get_etag path, facet
if etag
res\append 'etag', etag
if etag == req\get 'if-none-match'
res = headers.new!
res\append ':status', tostring 304
stream\write_headers res, true
return
value = stream\get_body_as_string!
ok, status, body = xpcall @.handle, err_and_trace, @, method, path, facet, value
if not ok
warn "Error handling request (#{method} #{path} #{facet}):\n#{status}"
body = "Internal Server Error:\n#{status}"
status = 500
response_type = if status > 299 then 'text/plain'
elseif facet and facet.type == 'text/html+interactive' then 'text/html'
elseif facet then facet.type
else 'text/plain'
res\append ':status', tostring status
res\append 'content-type', response_type
ok = stream\write_headers res, method == 'HEAD'
if ok and method ~= 'HEAD'
stream\write_chunk body, true
-- usage:
-- moon server.moon [FLAGS] [STORE] [host] [port]
-- * FLAGS - any of the following:
-- --[no-]unsafe - enable/disable server-side code execution when writable is on (default: on if local)
-- --[no-]cache - cache all fileder contents (default: off)
-- --[no-]http-cache - use ETag and Last-Modified headers (default: off)
-- * STORE - see mmm/mmmfs/stores/init.moon:get_store
-- * host - interface to bind to (default localhost, set to 0.0.0.0 for public hosting)
-- * port - port to serve from, default 8000
flags = {}
arguments = for a in *arg
if flag = a\match '^%-%-no%-(.*)$'
flags[flag] = false
continue
elseif flag = a\match '^%-%-(.*)$'
flags[flag] = true
continue
else
a
{ store, host, port } = arguments
store = get_store store
server = Server store, :flags, :host, port: port and tonumber port
server\listen!
|