networking.scm 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. ;;; Mudsync --- Live hackable MUD
  2. ;;; Copyright © 2016-2017 Christine Lemmer-Webber <cwebber@dustycloud.org>
  3. ;;;
  4. ;;; This file is part of Mudsync.
  5. ;;;
  6. ;;; Mudsync is free software; you can redistribute it and/or modify it
  7. ;;; under the terms of the GNU General Public License as published by
  8. ;;; the Free Software Foundation; either version 3 of the License, or
  9. ;;; (at your option) any later version.
  10. ;;;
  11. ;;; Mudsync is distributed in the hope that it will be useful, but
  12. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  13. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. ;;; General Public License for more details.
  15. ;;;
  16. ;;; You should have received a copy of the GNU General Public License
  17. ;;; along with Mudsync. If not, see <http://www.gnu.org/licenses/>.
  18. (define-module (mudsync networking)
  19. #:use-module (8sync actors)
  20. #:use-module (8sync agenda)
  21. #:use-module (8sync systems websocket server)
  22. #:use-module (ice-9 format)
  23. #:use-module (ice-9 match)
  24. #:use-module (ice-9 rdelim)
  25. #:use-module (ice-9 receive)
  26. #:use-module (oop goops)
  27. ;; Formatting
  28. #:use-module (mudsync scrubl)
  29. ;; used by web server only
  30. #:use-module (sxml simple)
  31. #:use-module (web request)
  32. #:use-module (web response)
  33. #:use-module (web uri)
  34. #:use-module (mudsync package-config)
  35. #:use-module (mudsync contrib mime-types)
  36. #:use-module (rnrs io ports)
  37. #:export (;; Should we be exporting these?
  38. %default-server
  39. %default-port
  40. <network-manager>
  41. nm-close-everything))
  42. ;;; Networking
  43. ;;; ==========
  44. (define %default-server #f)
  45. (define %default-port 8889)
  46. (define %default-web-server-port 8888)
  47. (define-actor <network-manager> (<actor>)
  48. ((start-listening
  49. (lambda* (actor message
  50. #:key (server %default-server)
  51. (port %default-port)
  52. (web-server-port %default-web-server-port))
  53. (if web-server-port
  54. (nm-install-web-server actor server web-server-port))
  55. (nm-install-socket actor server port)))
  56. (send-to-client nm-send-to-client-id)
  57. (new-socket-client nm-new-socket-client)
  58. (new-web-client nm-new-web-client)
  59. (client-disconnected nm-client-disconnected)
  60. (incoming-line nm-incoming-line-action))
  61. (web-server #:accessor .web-server)
  62. (server-socket #:getter nm-server-socket)
  63. ;; mapping of client -> client-id
  64. (clients #:getter nm-clients
  65. #:init-thunk make-hash-table)
  66. ;; send input to this actor
  67. (send-input-to #:getter nm-send-input-to
  68. #:init-keyword #:send-input-to))
  69. ;;; TODO: We should provide something like this, but this isn't used currently,
  70. ;;; and uses old deprecated code (the 8sync-port-remove stuff).
  71. ;; (define-method (nm-close-everything (nm <network-manager>) remove-from-agenda)
  72. ;; "Shut it down!"
  73. ;; ;; close all clients
  74. ;; (hash-for-each
  75. ;; (lambda (_ client)
  76. ;; (close client)
  77. ;; (if remove-from-agenda
  78. ;; (8sync-port-remove client)))
  79. ;; (nm-clients nm))
  80. ;; ;; reset the clients list
  81. ;; (set! (nm-clients) (make-hash-table))
  82. ;; ;; close the server
  83. ;; (close (nm-server-socket nm))
  84. ;; (if remove-from-agenda
  85. ;; (8sync-port-remove (nm-server-socket nm))))
  86. ;; Maximum number of backlogged connections when we listen
  87. (define %maximum-backlog-conns 128) ; same as SOMAXCONN on Linux 2.X,
  88. ; says the intarwebs
  89. (define (nm-install-socket nm server port)
  90. "Install socket on SERVER with PORT"
  91. (define s
  92. (socket PF_INET ; ipv4
  93. SOCK_STREAM ; two-way connection-based byte stream
  94. 0))
  95. (define addr
  96. (if server
  97. (inet-pton AF_INET server)
  98. INADDR_LOOPBACK))
  99. ;; Totally mimed from the Guile manual. Not sure if we need this, but:
  100. ;; http://www.unixguide.net/network/socketfaq/4.5.shtml
  101. (setsockopt s SOL_SOCKET SO_REUSEADDR 1) ; reuse port even if port is busy
  102. ;; Connecting to a non-specific address:
  103. ;; (bind s AF_INET INADDR_ANY port)
  104. ;; Should this be an option? Guess I don't know why we'd need it
  105. ;; @@: If we wanted to support listening on a particular hostname,
  106. ;; could see 8sync's irc.scm...
  107. (bind s AF_INET addr port)
  108. ;; Listen to connections
  109. (listen s %maximum-backlog-conns)
  110. ;; Make port non-blocking
  111. (fcntl s F_SETFL (logior O_NONBLOCK (fcntl s F_GETFL)))
  112. ;; @@: This is used in Guile's http server under the commit:
  113. ;; * module/web/server/http.scm (http-open): Ignore SIGPIPE. Keeps the
  114. ;; server from dying in some circumstances.
  115. ;; (sigaction SIGPIPE SIG_IGN)
  116. ;; Will this break other things that use pipes for us though?
  117. (slot-set! nm 'server-socket s)
  118. (format #t "Listening for clients in pid: ~s\n" (getpid))
  119. ;; TODO: set up periodic close of idle connections?
  120. (let loop ()
  121. ;; (yield) ;; @@: Do we need this?
  122. (define client-connection (accept s))
  123. (<- (actor-id nm) 'new-socket-client
  124. s client-connection)
  125. (loop)))
  126. (define (nm-new-socket-client nm message s client-connection)
  127. "Handle new client coming in to socket S"
  128. (define client-details (cdr client-connection))
  129. (define client-socket (car client-connection))
  130. (define client-id (big-random-number))
  131. (format #t "New client: ~s\n" client-details)
  132. (format #t "Client address: ~s\n"
  133. (gethostbyaddr
  134. (sockaddr:addr client-details)))
  135. (fcntl client-socket F_SETFL (logior O_NONBLOCK (fcntl client-socket F_GETFL)))
  136. (hash-set! (nm-clients nm) client-id
  137. (cons 'socket client-socket))
  138. (<- (nm-send-input-to nm) 'new-client #:client client-id)
  139. (nm-client-receive-loop nm client-socket client-id))
  140. (define (nm-new-web-client nm message ws-client-id)
  141. ;; nm client id, as opposed to the websocket one
  142. (define client-id (big-random-number))
  143. (hash-set! (nm-clients nm) client-id
  144. (cons 'websocket ws-client-id))
  145. (<- (nm-send-input-to nm) 'new-client #:client client-id)
  146. (<-reply message client-id))
  147. (define (nm-client-receive-loop nm client-socket client-id)
  148. "Make a method to receive client data"
  149. (define (loop)
  150. (define line (read-line client-socket))
  151. (if (eof-object? line)
  152. (<- (actor-id nm) 'client-disconnected client-id)
  153. (begin
  154. (nm-handle-line nm client-id
  155. (string-trim-right line #\return))
  156. (when (actor-alive? nm)
  157. (loop)))))
  158. (loop))
  159. (define (nm-client-disconnected nm message client-id)
  160. "Handle a closed port"
  161. (format #t "DEBUG: handled closed port ~a\n" client-id)
  162. (hash-remove! (nm-clients nm) client-id)
  163. (<-* `(#:actor ,nm) (nm-send-input-to nm) 'client-closed #:client client-id))
  164. (define (nm-handle-line nm client-id line)
  165. "Handle an incoming line of input from a client"
  166. (<-* `(#:actor ,nm) (nm-send-input-to nm) 'client-input
  167. #:data line
  168. #:client client-id))
  169. (define* (nm-send-to-client-id nm message #:key client data)
  170. "Send DATA to TO-CLIENT id"
  171. (define formatted-data
  172. (scrubl-write scrubl-sxml data))
  173. (define client-obj (hash-ref (nm-clients nm) client))
  174. (match client-obj
  175. (#f (throw 'no-such-client
  176. "Asked to send data to client but that client doesn't exist"
  177. #:client-id client
  178. #:data formatted-data))
  179. (('socket . client-socket)
  180. (display formatted-data client-socket))
  181. (('websocket . ws-client-id)
  182. (<- (.web-server nm) 'ws-send ws-client-id formatted-data))))
  183. (define (nm-incoming-line-action nm message client-id line)
  184. "Handle LINE coming in, probably from an external message handler,
  185. like the web one"
  186. (nm-handle-line nm client-id line))
  187. ;;; Web server interface
  188. (define-class <mudsync-ws-server> (<websocket-server>)
  189. (network-manager #:init-keyword #:network-manager
  190. #:accessor .network-manager)
  191. ;; This is a kludge... we really shouldn't have to double
  192. ;; record these, should we?
  193. (nm-client-ids #:init-thunk make-hash-table
  194. #:accessor .nm-client-ids))
  195. (define (nm-install-web-server nm server web-server-port)
  196. "This installs the web server, which we see in use below...."
  197. (set! (.web-server nm)
  198. (create-actor nm <mudsync-ws-server>
  199. #:network-manager (actor-id nm)
  200. #:port web-server-port
  201. #:http-handler (wrap-apply http-handler)
  202. #:on-ws-message (wrap-apply websocket-new-message)
  203. #:on-ws-client-connect
  204. (wrap-apply websocket-client-connect)
  205. #:on-ws-client-disconnect
  206. (wrap-apply websocket-client-disconnect))))
  207. (define (view:main-display request body)
  208. (define one-entry
  209. '(div (@ (class "stream-entry"))
  210. (p (b "<foo>") " Wow, it's so shiny!")))
  211. (define body-tmpl
  212. `((div (@ (id "stream-metabox"))
  213. (div (@ (id "stream"))
  214. ;; ,@(map (const one-entry) (iota 25))
  215. ;; (div (@ (class "stream-entry"))
  216. ;; (p (b "<bar>") " Last one!"))
  217. ))
  218. (div (@ (id "input-metabox"))
  219. (input (@ (id "main-input")))
  220. " "
  221. (span (@ (id "connection-status")
  222. (class "disconnected"))
  223. "[disconnected]"))))
  224. (define (main-tmpl)
  225. `(html (@ (xmlns "http://www.w3.org/1999/xhtml"))
  226. (head (title "Mudsync!")
  227. (meta (@ (charset "UTF-8")))
  228. (link (@ (rel "stylesheet")
  229. (href "/static/css/main.css")))
  230. (script (@ (type "text/javascript")
  231. (src "/static/js/mudsync.js"))))
  232. (body ,@body-tmpl)))
  233. (define (write-template-to-string)
  234. (with-fluids ((%default-port-encoding "UTF-8"))
  235. (call-with-output-string
  236. (lambda (p)
  237. (sxml->xml (main-tmpl) p)))))
  238. (values (build-response #:code 200
  239. #:headers '((content-type . (application/xhtml+xml))))
  240. (write-template-to-string)))
  241. (define (view:render-static request body static-path)
  242. (values (build-response #:code 200
  243. #:headers `((content-type . (,(mime-type static-path)))))
  244. (call-with-input-file (web-static-filepath static-path) get-bytevector-all)))
  245. (define (view:standard-four-oh-four . args)
  246. (values (build-response #:code 404
  247. #:headers '((content-type . (text/plain))))
  248. "Four-oh-four! Not found."))
  249. (define (route request)
  250. (match (split-and-decode-uri-path (uri-path (request-uri request)))
  251. (() (values view:main-display '()))
  252. (("static" static-path ...)
  253. ;; TODO: make this toggle'able
  254. (values view:render-static
  255. (list (string-append "/" (string-join
  256. static-path "/")))))
  257. ;; Not found!
  258. (_ (values view:standard-four-oh-four '()))))
  259. (define (http-handler request body)
  260. (receive (view args)
  261. (route request)
  262. (apply view request body args)))
  263. ;; Respond to text messages by reversing the message. Respond to
  264. ;; binary messages with "hello".
  265. (define (websocket-new-message websocket-server client-id data)
  266. (cond
  267. ((string? data)
  268. (<- (.network-manager websocket-server) 'incoming-line
  269. (hash-ref (.nm-client-ids websocket-server)
  270. client-id)
  271. data))
  272. ;; binary data is ignored
  273. (else #f)))
  274. (define (websocket-client-connect websocket-server client-id)
  275. (let ((nm-client-id
  276. (mbody-val (<-wait (.network-manager websocket-server)
  277. 'new-web-client client-id))))
  278. (hash-set! (.nm-client-ids websocket-server)
  279. client-id nm-client-id)))
  280. (define (websocket-client-disconnect websocket-server client-id)
  281. (<- (.network-manager websocket-server) 'client-disconnected
  282. (hash-ref (.nm-client-ids websocket-server) client-id))
  283. (hash-remove! (.nm-client-ids websocket-server) client-id))