A server-side web framework written in Forth. http://www.1-9-9-1.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

480 lines
16KB

  1. \ 1991
  2. include unix/socket.fs
  3. \ Helper words
  4. : +s ( addr1 u1 addr2 u2 -- addr3 u3 ) \ like s+ but prepend rather than append.
  5. 2swap s+ ;
  6. : str-count ( addr1 u1 addr2 u2 -- u3 ) \ Counts occurrences of addr2 within addr1.
  7. 2swap 0 >r
  8. begin 2over search
  9. while 2over nip /string
  10. r> 1+ >r
  11. repeat 2drop 2drop r> ;
  12. : -leading ( addr len -- addr' len' )
  13. begin over c@ bl = while 1 /string repeat ;
  14. : trim ( addr len -- addr' len')
  15. -leading -trailing ;
  16. : exchange ( a1 a2 -- )
  17. 2dup c@ swap c@ rot c! swap c! ;
  18. : reverse ( caddr u -- ) \ reverse a string
  19. 1- bounds begin 2dup > while
  20. 2dup exchange
  21. -1 /string
  22. repeat 2drop ;
  23. : sourcedir ( -- saddr su )
  24. \ Returns the directory in which the file
  25. \ invoking the word finds itself
  26. \ relative to gforth's execution directory.
  27. \ Useful for specifying in which dir to find
  28. \ specific files (e.g., public/, views/).
  29. sourcefilename \ get the name of our file
  30. pad dup >r place \ copy the string so we don't
  31. r> count \ modify sourcefilename.
  32. 2dup reverse \ reverse and search for first /
  33. s" /" search if \ if found, reverse string to
  34. 2dup reverse \ strip the filename but keep dir.
  35. else
  36. 2drop \ no slash,
  37. s" ./" \ same dir execution.
  38. then ;
  39. : file-exists? ( addr u -- addr u bool )
  40. 2dup file-status nip 0= ;
  41. : pubvar create 0 , 0 , ;
  42. \ Query params
  43. pubvar queryString
  44. : set-query-string ( addr u -- )
  45. queryString 2! ;
  46. : get-query-string ( -- addr u )
  47. queryString 2@ ;
  48. : add-to-query-string ( addr u -- )
  49. get-query-string
  50. dup if \ If queryString isn't empty, add & before
  51. s" &" s+
  52. then
  53. +s \ adding our new query values.
  54. set-query-string ;
  55. pubvar tmpQueryString
  56. : set-tmp-query-string ( addr u -- )
  57. tmpQueryString 2! ;
  58. : get-tmp-query-string ( -- addr u )
  59. tmpQueryString 2@ ;
  60. : add-to-tmp-query-string ( addr u -- )
  61. get-tmp-query-string
  62. dup if \ If queryString isn't empty, add & before
  63. s" &" s+
  64. then
  65. +s \ adding our new query values.
  66. set-tmp-query-string ;
  67. \ Request body
  68. pubvar requestBody
  69. : set-request-body ( addr u -- )
  70. requestBody 2! ;
  71. : get-request-body ( -- addr u )
  72. requestBody 2@ ;
  73. \ Request method
  74. pubvar requestMethod
  75. : set-request-method ( addr u -- )
  76. requestMethod 2! ;
  77. : get-request-method ( -- addr u )
  78. requestMethod 2@ ;
  79. \ User-defined routing
  80. wordlist constant routes
  81. pubvar reqroute
  82. : set-requested-route ( addr u -- )
  83. reqroute 2! ;
  84. : get-requested-route ( -- addr u )
  85. reqroute 2@ ;
  86. : fuzzy-find-route ( xt -- xt' bool )
  87. \ Takes an xt that accepts a name token
  88. \ and returns a bool.
  89. \ Traverse will run as long as xt
  90. \ returns true.
  91. \ Also takes the addr u of the requested
  92. \ route we're trying to validate.
  93. >r routes wordlist-id @ \ Store xt and specify wordlist
  94. begin
  95. dup
  96. while
  97. r@ over >r execute while r> @
  98. repeat r>
  99. then
  100. rdrop
  101. ?dup if
  102. get-tmp-query-string \ Save our fuzzy vars to the request's
  103. add-to-query-string \ query real string.
  104. name>int \ Get the xt of the nt.
  105. -1 \ Return true.
  106. else
  107. 0
  108. then ;
  109. : fuzzy-compare ( nt -- bool )
  110. \ Takes a route name token and returns
  111. \ whether that route name fuzzy matches
  112. \ the requested url
  113. s" " set-tmp-query-string \ Reset tmp query string.
  114. name>string \ Get the string value of the NT.
  115. 2dup s" <" search if \ See if the route expects fuzzy matching.
  116. 2drop \ Drop search results.
  117. 2dup s" /" str-count >r \ Check to see if both routes have the same
  118. get-requested-route s" /" str-count \ number of / occurrences.
  119. r> = if
  120. 2dup s" <" str-count 0 do
  121. 2dup 2>r
  122. 2r@
  123. 2dup s" <" search drop \ Get position of "<",
  124. nip -
  125. nip
  126. get-requested-route rot /string \ crop until there in the requested route,
  127. 2dup s" /" search if \ search for the next / or end of route,
  128. nip -
  129. else
  130. 2drop
  131. then
  132. \ (
  133. 2dup 2r> 2swap 2>r 2>r \ (Store a copy of the real value of <match>.)
  134. \ )
  135. 2r@
  136. 2dup s" <" search drop \ and replace <...> with the requested route word
  137. nip
  138. \ (
  139. 2dup - 1+ 2r@ rot /string \ (Store the beginnings of user's <"match"> word.)
  140. 2dup s" >" search drop \ (Retrieve full <"match"> user word,)
  141. nip - s" =" s+
  142. 2r> 2r> 2swap 2>r
  143. s+ \ (and associate it with the request value,)
  144. add-to-tmp-query-string \ (before saving it to the tmp query string.)
  145. \ )
  146. -
  147. +s \ by prepending pre-< to route word
  148. 2r> s" >" search drop 1 /string \ and then by appending post-> to route word.
  149. s+
  150. 2>r \ Save string progress,
  151. 2drop \ drop old string,
  152. 2r> \ set new string for start of next loop (or end).
  153. loop
  154. get-requested-route compare \ Check to see if the strings match.
  155. else
  156. 2drop \ Drop name>string.
  157. -1 \ Keep looping.
  158. then
  159. else
  160. 2drop \ Drop search results.
  161. 2drop \ Drop name>string.
  162. -1 \ Keep looping.
  163. then ;
  164. : fuzzy-match ( addr u -- xt bool )
  165. set-requested-route
  166. ['] fuzzy-compare fuzzy-find-route ;
  167. : find-route ( addr u -- data )
  168. 2dup 2>r
  169. routes search-wordlist if
  170. 2rdrop \ Exact match found. Drop the dup string.
  171. >body @
  172. else
  173. 2r>
  174. fuzzy-match if \ Fuzzy match found.
  175. >body @
  176. else
  177. 0 \ No match at all.
  178. then
  179. then ;
  180. : register-route ( data addr u -- )
  181. 2dup routes search-wordlist if
  182. routes drop nip nip
  183. >body !
  184. else
  185. routes get-current >r set-current \ switch definition word lists
  186. nextname create ,
  187. r> set-current
  188. then ;
  189. \ Public directory
  190. pubvar public
  191. : set-public-path ( addr u -- )
  192. public 2! ;
  193. : get-public-path ( -- addr u )
  194. public 2@ ;
  195. sourcedir s" public" s+ set-public-path
  196. \ Views directory
  197. pubvar views
  198. : set-view-path ( addr u -- )
  199. views 2! ;
  200. : get-view-path ( -- addr u )
  201. views 2@ ;
  202. sourcedir s" views/" s+ set-view-path \ Needs that trailing slash
  203. \ Handling views
  204. pubvar viewoutput
  205. : set-view-output ( addr u -- )
  206. viewoutput 2! ;
  207. : get-view-output ( -- addr u )
  208. viewoutput 2@ ;
  209. : parse-view ( addr u -- )
  210. \ Get string between <$ $> and invoke `evaluate`.
  211. \ Append to viewoutput as we go.
  212. \ There's probably a better way of doing this
  213. \ but it works for me.
  214. begin
  215. 2dup s" <$" search if
  216. 2over swap 2>r \ If there is a match for <$, save addr and u.
  217. swap >r dup >r \ Store the match and output anything that
  218. - \ comes before it.
  219. get-view-output +s
  220. set-view-output
  221. r> r> swap \ Then reinstate the match "<$...".
  222. 2dup s" $>" search if \ Check to see if there's a closing tag $>.
  223. 2 - \ Add the close tag to the search result.
  224. nip \ Save the end position of $>.
  225. dup >r
  226. - \ Reduce the string to <$ ... $>.
  227. evaluate \ Run user's code (maybe a bad idea?).
  228. r> \ Retrieve our saved end position of $>.
  229. r> r> \ Retrive the addr u from start of loop iter.
  230. rot \ Bring end $> to stack top.
  231. over >r \ Store the real string's length.
  232. - \ Subtract end $> from u to get the pos from top
  233. r> swap \ that we'd like to strip away. Restore saved u.
  234. /string \ Drop top of the string until the end of $>.
  235. 0 \ Keep looping.
  236. else
  237. get-view-output +s \ No closing tag. Just save the full string.
  238. set-view-output
  239. 2rdrop \ And drop the stored addr and u
  240. 2drop \ as well as both the 2dup we made before
  241. 2drop \ searching twice.
  242. -1 \ Exit the loop.
  243. then
  244. else
  245. 2drop \ No match for <$. Drop the 2dup from before search.
  246. get-view-output +s
  247. set-view-output \ Save string as-is to view output
  248. -1 \ exit the loop
  249. then
  250. until ;
  251. : render-view ( addr u -- vaddr vu ) \ Accepts a view filename. Returns parsed contents.
  252. s" " set-view-output
  253. get-view-path +s
  254. file-exists? if
  255. slurp-file
  256. parse-view
  257. else
  258. exit \ Continue to 404, no view.
  259. then
  260. get-view-output ;
  261. : <$ ( -- ) ; \ Do nothing.
  262. : $> ( -- ) ; \ Do nothing.
  263. : $type ( addr u -- ) \ User-land word for outputing via views.
  264. get-view-output +s
  265. set-view-output ;
  266. : import ( -- ) \ User-land word for including other view files.
  267. get-view-path +s
  268. file-exists? if
  269. slurp-file
  270. parse-view
  271. else
  272. s" "
  273. then ;
  274. \ Request's Content-Type
  275. pubvar RequestContentType
  276. : set-content-type ( addr u -- )
  277. RequestContentType 2! ;
  278. : get-content-type ( -- addr u )
  279. RequestContentType 2@ ;
  280. : filetype: ( addr u "extension" -- ) \ takes a content-type and the extension
  281. create here over 1+ allot place
  282. does> count ;
  283. : get-filetype ( addr u -- caddr cu ) \ takes an extension, looks to find a definition
  284. find-name dup if
  285. name>int
  286. execute
  287. else
  288. drop
  289. s" text/plain"
  290. then ;
  291. s" text/plain" filetype: txt \ txt should always be defined
  292. s" text/html" filetype: html
  293. s" text/css" filetype: css
  294. s" text/javascript" filetype: js
  295. s" image/png" filetype: png
  296. s" image/gif" filetype: gif
  297. s" image/jpeg" filetype: jpg
  298. s" image/jpeg" filetype: jpeg
  299. s" image/x-icon" filetype: ico
  300. \ Internal request handling
  301. : HTTP/1.1 s" HTTP/1.1 " ;
  302. : response-status ( u -- addr u )
  303. dup case \ get status code info
  304. 200 of s" OK" endof
  305. 404 of s" Not Found" endof
  306. endcase
  307. s\" \n" s+
  308. rot s>d <# #s #> +s \ convert status code to string and prepend to info
  309. HTTP/1.1 +s ; \ prepend HTTP/1.1
  310. : content-type ( addr u -- caddr cu )
  311. s" Content-Type: " +s \ Prepend to the provided content type
  312. s\" \n\n" s+ ; \ Append 2 new lines
  313. : set-header ( u addr u -- raddr ru ) \ Accepts status code and content type string
  314. rot response-status \ Set response status
  315. 2swap content-type \ Set content-type
  316. s+ ; \ Join
  317. : get-content-length ( addr u -- clen )
  318. s" Content-Length:" search if
  319. 2dup s\" \n" search drop nip -
  320. s" Content-Length:" nip /string
  321. trim
  322. s>number?
  323. 2drop
  324. else
  325. 2drop
  326. 0
  327. then ;
  328. : read-request-body ( socket u -- )
  329. \ Takes the socket and the length of the
  330. \ body (Content-Length).
  331. here swap aligned read-socket
  332. set-request-body ;
  333. : read-request ( socket -- addr u )
  334. \ Returns the request header
  335. \ but also collects the request body.
  336. dup >r
  337. pad 4096 read-socket
  338. r> dup 2over rot drop
  339. get-content-length ?dup if
  340. read-request-body
  341. else
  342. s" " set-request-body
  343. drop
  344. then ;
  345. : send-response ( addr u socket -- )
  346. dup >r write-socket r> close-socket ;
  347. : store-query-string ( addr u -- raddr ru )
  348. 2dup s" ?" search if
  349. 2dup 1 /string set-query-string \ Store query string (without leading "?").
  350. nip -
  351. else
  352. s" " set-query-string \ Store empty query string (reset).
  353. 2drop
  354. then ;
  355. : requested-route ( addr u -- raddr ru )
  356. bl scan 1- swap 1+ swap
  357. 2dup bl scan nip - \ get the space-separated route
  358. store-query-string ; \ strip and store the query, leave route
  359. : .extension ( addr u -- addr u )
  360. 2dup reverse \ reverse the file name
  361. 2dup s" ." search \ search for the first occurance of "."
  362. if
  363. nip - \ remove the "." from the search results
  364. else
  365. s" txt"
  366. then
  367. 2dup reverse ; \ reverse reversed extension
  368. : serve-file-type ( addr u -- )
  369. .extension get-filetype set-content-type ;
  370. : serve-file ( addr u -- addr u )
  371. slurp-file ;
  372. : 404content-type txt ;
  373. : 404html s" 404" ;
  374. : valid-method? ( addr u -- addr' u' bool )
  375. 2dup s" GET" search if
  376. 2nip
  377. -1
  378. s" GET" set-request-method
  379. exit
  380. then
  381. 2drop
  382. 2dup s" POST" search if
  383. 2nip
  384. -1
  385. s" POST" set-request-method
  386. exit
  387. then
  388. 2drop
  389. 2dup s" PUT" search if
  390. 2nip
  391. -1
  392. s" PUT" set-request-method
  393. exit
  394. then
  395. 2drop
  396. 2dup s" DELETE" search if
  397. 2nip
  398. -1
  399. s" DELETE" set-request-method
  400. exit
  401. then
  402. 2drop
  403. 0 ;
  404. : either-resolve ( addr u -- resolveaddr resolveu )
  405. valid-method? if
  406. s" html" get-filetype set-content-type \ reset the request's content-type
  407. requested-route
  408. 2dup find-route dup if
  409. >r 2drop r> \ keep xt, drop the route string
  410. execute \ execute the user's route handler
  411. else
  412. drop \ drop the null xt
  413. get-public-path +s \ see if route exists in public dir
  414. file-exists? if
  415. 2dup serve-file \ collect file contents
  416. 2swap serve-file-type \ set the file type
  417. else
  418. exit \ continue to 404
  419. then
  420. then
  421. 200 get-content-type set-header +s
  422. rdrop exit
  423. then ;
  424. : or-404 ( addr u -- 404addr 404u )
  425. 2drop
  426. 404 404content-type set-header 404html s+ ;
  427. : prepare-response ( addr u -- returnaddr returnu)
  428. either-resolve or-404 ;
  429. : start-server { server client }
  430. begin
  431. server 255 listen
  432. server accept-socket to client
  433. client read-request prepare-response client send-response
  434. again ;
  435. \ Userland
  436. : 1991: ( port -- )
  437. create-server 0 start-server ;
  438. : /1991 ( "<path> <word>" -- )
  439. bl word ' swap count register-route ;