sharedspace.go 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096
  1. package main
  2. /*
  3. SharedSpace - Collaboration backbone endpoints
  4. author: tobychui / AI assisted
  5. HTTP + WebSocket wiring for mod/sharedspace, the ArozOS collaboration
  6. backbone: multi-user spaces holding chat texts, images, files and
  7. revision-synced collaborative documents, with open / public / private
  8. access control, optional persistence and a realtime channel per space.
  9. MeetRoom runs its meeting signaling over the same channels.
  10. User endpoints (login required; the space ACL is the authorization
  11. layer - see mod/sharedspace/access.go):
  12. POST /system/sharedspace/create name=&access=&persistent=
  13. POST /system/sharedspace/delete spaceid= (manage)
  14. GET /system/sharedspace/info?spaceid=
  15. GET /system/sharedspace/list (joined)
  16. GET /system/sharedspace/listpublic
  17. POST /system/sharedspace/join spaceid=
  18. POST /system/sharedspace/leave spaceid=
  19. POST /system/sharedspace/members/add spaceid=&username=&role=
  20. POST /system/sharedspace/members/remove spaceid=&username=
  21. POST /system/sharedspace/members/role spaceid=&username=&role=
  22. POST /system/sharedspace/access spaceid=&access=
  23. POST /system/sharedspace/meta spaceid=&key=&value=
  24. GET /system/sharedspace/items?spaceid=
  25. POST /system/sharedspace/addtext spaceid=&text=
  26. POST /system/sharedspace/upload multipart (spaceid, file)
  27. GET /system/sharedspace/download?spaceid=&itemid=[&inline=1]
  28. POST /system/sharedspace/removeitem spaceid=&itemid=
  29. POST /system/sharedspace/doc/create spaceid=&name=
  30. GET /system/sharedspace/doc/list?spaceid=
  31. GET /system/sharedspace/doc/get?spaceid=&docid=
  32. POST /system/sharedspace/doc/update spaceid=&docid=&baserev=&content=
  33. POST /system/sharedspace/doc/delete spaceid=&docid=
  34. GET /system/sharedspace/ws?spaceid= WebSocket upgrade
  35. Admin endpoints (System Settings > Shared Spaces):
  36. GET /system/sharedspace/admin/list
  37. POST /system/sharedspace/admin/delete spaceid=
  38. GET /system/sharedspace/admin/config
  39. POST /system/sharedspace/admin/setconfig maxupload=&maxitems=&allowpersistent=&retentiondays=
  40. WebSocket protocol (JSON frames over /system/sharedspace/ws):
  41. client -> server: {"type":"signal","to":subid,"data":{...}} ephemeral relay (WebRTC etc.)
  42. {"type":"broadcast","data":{...}} ephemeral fan-out to peers
  43. {"type":"chat","text":"..."} persisted text item
  44. {"type":"doc-update","docid":"..","baserev":n,"content":".."}
  45. {"type":"ping"} app-level heartbeat
  46. server -> client: {"type":"welcome","subid":n,"username":u,"space":{...},"peers":[...]}
  47. {"type":"peer-join","peer":{...}} / {"type":"peer-leave","subid":n,"username":u}
  48. {"type":"signal","from":n,"data":{...}}
  49. {"type":"broadcast","from":n,"username":u,"data":{...}}
  50. {"type":"item","item":{...}} / {"type":"item-removed","itemid":i}
  51. {"type":"doc","docid":d,"revision":r,"by":u,"patch":{pos,del,ins}}
  52. {"type":"doc-created","doc":{...}} / {"type":"doc-deleted","docid":d}
  53. {"type":"doc-conflict","docid":d,"revision":r} sender only
  54. {"type":"member","action":a,"username":u,"role":r}
  55. {"type":"error","error":"..."} sender only
  56. {"type":"pong"}, {"type":"space-closed"}
  57. Ephemeral frames (signal / broadcast) fan out through the space channel
  58. and are never persisted; chat and documents write through the space so
  59. AGI scripts and, on meeting spaces, MeetRoom clients see them too. A
  60. client that receives a doc revision gap re-fetches with doc/get - the
  61. server content is always authoritative. Document saves should be
  62. debounced client-side (~500ms); every accepted revision is one database
  63. write.
  64. */
  65. import (
  66. "encoding/json"
  67. "mime"
  68. "net/http"
  69. "net/url"
  70. "os"
  71. "path/filepath"
  72. "strconv"
  73. "strings"
  74. "time"
  75. "github.com/gorilla/websocket"
  76. prout "imuslab.com/arozos/mod/prouter"
  77. "imuslab.com/arozos/mod/sharedspace"
  78. "imuslab.com/arozos/mod/utils"
  79. )
  80. const (
  81. ssMaxChatLength = 4000 // runes per chat message over the ws
  82. ssMaxSocketFrame = 512 << 10 // 512KB per ws frame (doc contents included)
  83. ssPingInterval = 20 * time.Second // server keepalive ping cadence
  84. ssReadTimeout = 60 * time.Second // drop a subscriber whose socket goes silent this long
  85. ssWriteTimeout = 10 * time.Second // per-frame write deadline
  86. ssSweepInterval = 1 * time.Hour // retention sweep cadence
  87. //Admin configuration keys, stored in the sharedspace table next to the
  88. //space records (see mod/sharedspace/persistence.go for the layout)
  89. ssDBTable = "sharedspace"
  90. ssConfMaxUpload = "conf/maxupload"
  91. ssConfMaxItems = "conf/maxitems"
  92. ssConfAllowPersistent = "conf/allowpersistent"
  93. ssConfRetentionDays = "conf/retentiondays"
  94. )
  95. var (
  96. sharedSpaceManager *sharedspace.Manager
  97. ssUpgrader = websocket.Upgrader{
  98. ReadBufferSize: 4096,
  99. WriteBufferSize: 4096,
  100. CheckOrigin: func(r *http.Request) bool { return true },
  101. }
  102. )
  103. /* ================= Configuration helpers ================= */
  104. // ssConfInt64 reads an integer admin setting, falling back when unset or
  105. // out of range.
  106. func ssConfInt64(key string, fallback int64) int64 {
  107. if sysdb == nil || !sysdb.KeyExists(ssDBTable, key) {
  108. return fallback
  109. }
  110. value := int64(0)
  111. sysdb.Read(ssDBTable, key, &value)
  112. if value <= 0 {
  113. return fallback
  114. }
  115. return value
  116. }
  117. // ssConfBool reads a boolean admin setting, falling back when unset.
  118. func ssConfBool(key string, fallback bool) bool {
  119. if sysdb == nil || !sysdb.KeyExists(ssDBTable, key) {
  120. return fallback
  121. }
  122. value := fallback
  123. sysdb.Read(ssDBTable, key, &value)
  124. return value
  125. }
  126. /* ================= Descriptors ================= */
  127. // ssDescribeSpace renders the space fields shared with clients. When a
  128. // username is given the caller's own role is included.
  129. func ssDescribeSpace(space *sharedspace.Space, username string) map[string]interface{} {
  130. desc := map[string]interface{}{
  131. "spaceid": space.ID,
  132. "name": space.Name,
  133. "owner": space.Owner,
  134. "access": space.AccessMode(),
  135. "persistent": space.Persistent,
  136. "items": space.ItemCount(),
  137. "docs": space.DocCount(),
  138. "members": space.MemberCount(),
  139. "metadata": space.Metadata(),
  140. "createdat": space.CreatedAt.Unix(),
  141. }
  142. if username != "" {
  143. role, isMember := space.Role(username)
  144. desc["myrole"] = role
  145. desc["ismember"] = isMember
  146. }
  147. return desc
  148. }
  149. // ssDescribeItem renders a space item for clients.
  150. func ssDescribeItem(item *sharedspace.Item) map[string]interface{} {
  151. return map[string]interface{}{
  152. "itemid": item.ID,
  153. "type": item.Type,
  154. "name": item.Name,
  155. "text": item.Text,
  156. "size": item.Size,
  157. "uploader": item.Uploader,
  158. "origin": item.Origin,
  159. "time": item.CreatedAt.Unix(),
  160. }
  161. }
  162. // ssDescribeDoc renders a document snapshot for clients. includeContent
  163. // controls whether the body travels with it.
  164. func ssDescribeDoc(doc *sharedspace.DocSnapshot, includeContent bool) map[string]interface{} {
  165. desc := map[string]interface{}{
  166. "docid": doc.ID,
  167. "name": doc.Name,
  168. "creator": doc.Creator,
  169. "revision": doc.Revision,
  170. "createdat": doc.CreatedAt.Unix(),
  171. "updatedat": doc.UpdatedAt.Unix(),
  172. "updatedby": doc.UpdatedBy,
  173. }
  174. if includeContent {
  175. desc["content"] = doc.Content
  176. }
  177. return desc
  178. }
  179. /* ================= Request helpers ================= */
  180. // ssRequestParam reads a request parameter from POST form or query string
  181. // depending on the endpoint's method convention.
  182. func ssRequestParam(r *http.Request, post bool, key string) (string, error) {
  183. if post {
  184. return utils.PostPara(r, key)
  185. }
  186. return utils.GetPara(r, key)
  187. }
  188. // ssResolveSpaceRequest authenticates the caller and resolves the spaceid
  189. // parameter to a live space the caller may read. On failure the error
  190. // response is already written and ok is false.
  191. func ssResolveSpaceRequest(w http.ResponseWriter, r *http.Request, post bool) (username string, space *sharedspace.Space, ok bool) {
  192. userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
  193. if err != nil {
  194. utils.SendErrorResponse(w, "Not logged in")
  195. return "", nil, false
  196. }
  197. spaceID, err := ssRequestParam(r, post, "spaceid")
  198. if err != nil {
  199. utils.SendErrorResponse(w, "Missing space ID")
  200. return "", nil, false
  201. }
  202. space, exists := sharedSpaceManager.GetSpace(spaceID)
  203. if !exists || !space.CanRead(userinfo.Username) {
  204. //Private spaces are indistinguishable from missing ones to outsiders
  205. utils.SendErrorResponse(w, "Space not found")
  206. return "", nil, false
  207. }
  208. return userinfo.Username, space, true
  209. }
  210. // ssCloseSpace announces the shutdown to live subscribers and deletes the
  211. // space (used by the user delete endpoint and the admin surface).
  212. func ssCloseSpace(spaceID string) {
  213. space, ok := sharedSpaceManager.GetSpace(spaceID)
  214. if !ok {
  215. return
  216. }
  217. space.Channel().Broadcast([]byte(`{"type":"space-closed"}`), -1)
  218. sharedSpaceManager.DeleteSpace(spaceID)
  219. }
  220. // ssBroadcastSpaceEvent builds the event listener that fans space mutations
  221. // out to the space's channel as protocol frames. Registered once per space
  222. // under a fixed key, so repeated registration is idempotent.
  223. func ssBroadcastSpaceEvent(space *sharedspace.Space) func(*sharedspace.SpaceEvent) {
  224. channel := space.Channel()
  225. return func(event *sharedspace.SpaceEvent) {
  226. var frame []byte
  227. switch event.Kind {
  228. case sharedspace.EventItemAdded:
  229. frame = mrMarshalOrDrop(map[string]interface{}{
  230. "type": "item",
  231. "item": ssDescribeItem(event.Item),
  232. })
  233. case sharedspace.EventItemRemoved:
  234. frame = mrMarshalOrDrop(map[string]interface{}{
  235. "type": "item-removed",
  236. "itemid": event.Item.ID,
  237. })
  238. case sharedspace.EventDocCreated:
  239. frame = mrMarshalOrDrop(map[string]interface{}{
  240. "type": "doc-created",
  241. "doc": ssDescribeDoc(event.Doc, false),
  242. })
  243. case sharedspace.EventDocUpdated:
  244. frame = mrMarshalOrDrop(map[string]interface{}{
  245. "type": "doc",
  246. "docid": event.Doc.ID,
  247. "revision": event.Doc.Revision,
  248. "by": event.Doc.UpdatedBy,
  249. "patch": map[string]interface{}{
  250. "pos": event.Patch.Pos,
  251. "del": event.Patch.Del,
  252. "ins": event.Patch.Ins,
  253. },
  254. })
  255. case sharedspace.EventDocDeleted:
  256. frame = mrMarshalOrDrop(map[string]interface{}{
  257. "type": "doc-deleted",
  258. "docid": event.Doc.ID,
  259. })
  260. case sharedspace.EventMemberChanged:
  261. frame = mrMarshalOrDrop(map[string]interface{}{
  262. "type": "member",
  263. "action": event.Action,
  264. "username": event.Member,
  265. "role": event.Role,
  266. })
  267. }
  268. if frame != nil {
  269. channel.Broadcast(frame, -1)
  270. }
  271. }
  272. }
  273. // ssResolvePersistRoot returns the durable blob root for shared spaces.
  274. // Space files live with the user data under the -root directory (default
  275. // ./files) rather than the system folder; a storage tree from the previous
  276. // layout under system/ is migrated over once. Blob disk paths are always
  277. // recomputed from this root at load time (see mod/sharedspace/persistence.go),
  278. // so relocating the tree is safe.
  279. func ssResolvePersistRoot() string {
  280. persistRoot := filepath.Join(*root_directory, "sharedspace")
  281. legacyRoot := filepath.Join("system", "sharedspace")
  282. if utils.FileExists(legacyRoot) && !utils.FileExists(persistRoot) {
  283. os.MkdirAll(filepath.Dir(persistRoot), 0775)
  284. if err := os.Rename(legacyRoot, persistRoot); err != nil {
  285. //e.g. the user root sits on another mount - keep the data usable
  286. systemWideLogger.PrintAndLog("SharedSpace", "Could not move shared space storage to "+persistRoot+", keeping "+legacyRoot, err)
  287. return legacyRoot
  288. }
  289. systemWideLogger.PrintAndLog("SharedSpace", "Shared space storage migrated from "+legacyRoot+" to "+persistRoot, nil)
  290. }
  291. return persistRoot
  292. }
  293. // SharedSpaceInit creates the system-wide shared collaboration space manager
  294. // and wires up its HTTP / WebSocket endpoints and the admin surface.
  295. // Must be initiated before MeetRoomInit and AGIInit (see startup.go).
  296. func SharedSpaceInit() {
  297. sharedSpaceManager = sharedspace.NewManagerWithOptions(sharedspace.ManagerOptions{
  298. PersistentRoot: ssResolvePersistRoot(),
  299. Database: sysdb,
  300. MaxUpload: ssConfInt64(ssConfMaxUpload, sharedspace.DefaultMaxUpload),
  301. DefaultMaxItems: int(ssConfInt64(ssConfMaxItems, sharedspace.DefaultMaxItems)),
  302. })
  303. sharedSpaceManager.SetAllowPersistent(ssConfBool(ssConfAllowPersistent, true))
  304. //Retention: periodically remove persistent spaces idle for longer than
  305. //the admin-configured retention window (0 disables the sweep)
  306. go func() {
  307. ticker := time.NewTicker(ssSweepInterval)
  308. defer ticker.Stop()
  309. for range ticker.C {
  310. retentionDays := ssConfInt64(ssConfRetentionDays, 0)
  311. if retentionDays > 0 {
  312. sharedSpaceManager.SweepStaleSpaces(time.Duration(retentionDays) * 24 * time.Hour)
  313. }
  314. }
  315. }()
  316. //User endpoints: login-only universal router. Spaces carry their own
  317. //access control (open capability / public / private membership), which
  318. //is the authorization layer for everything below.
  319. router := prout.NewModuleRouter(prout.RouterOption{
  320. ModuleName: "",
  321. AdminOnly: false,
  322. UserHandler: userHandler,
  323. DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
  324. errorHandlePermissionDenied(w, r)
  325. },
  326. })
  327. //Create a new space; the creator becomes the owner.
  328. router.HandleFunc("/system/sharedspace/create", func(w http.ResponseWriter, r *http.Request) {
  329. userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
  330. if err != nil {
  331. utils.SendErrorResponse(w, "Not logged in")
  332. return
  333. }
  334. name, _ := utils.PostPara(r, "name")
  335. access, _ := utils.PostPara(r, "access")
  336. persistent, _ := utils.PostPara(r, "persistent")
  337. space, err := sharedSpaceManager.CreateSpaceWithOptions(userinfo.Username, name, sharedspace.SpaceOptions{
  338. Access: access,
  339. Persistent: persistent == "true",
  340. })
  341. if err != nil {
  342. utils.SendErrorResponse(w, err.Error())
  343. return
  344. }
  345. js := mrMarshalOrDrop(ssDescribeSpace(space, userinfo.Username))
  346. utils.SendJSONResponse(w, string(js))
  347. })
  348. //Delete a space; space managers only.
  349. router.HandleFunc("/system/sharedspace/delete", func(w http.ResponseWriter, r *http.Request) {
  350. username, space, ok := ssResolveSpaceRequest(w, r, true)
  351. if !ok {
  352. return
  353. }
  354. if !space.CanManage(username) {
  355. utils.SendErrorResponse(w, "Permission denied")
  356. return
  357. }
  358. ssCloseSpace(space.ID)
  359. utils.SendOK(w)
  360. })
  361. //Describe one space.
  362. router.HandleFunc("/system/sharedspace/info", func(w http.ResponseWriter, r *http.Request) {
  363. username, space, ok := ssResolveSpaceRequest(w, r, false)
  364. if !ok {
  365. return
  366. }
  367. desc := ssDescribeSpace(space, username)
  368. //The member list is only revealed to members and managers
  369. if _, isMember := space.Role(username); isMember || space.CanManage(username) {
  370. desc["memberlist"] = space.Members()
  371. }
  372. js := mrMarshalOrDrop(desc)
  373. utils.SendJSONResponse(w, string(js))
  374. })
  375. //List the spaces the caller has joined (or owns).
  376. router.HandleFunc("/system/sharedspace/list", func(w http.ResponseWriter, r *http.Request) {
  377. userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
  378. if err != nil {
  379. utils.SendErrorResponse(w, "Not logged in")
  380. return
  381. }
  382. joined := sharedSpaceManager.ListSpacesByMember(userinfo.Username)
  383. list := make([]map[string]interface{}, 0, len(joined))
  384. for _, space := range joined {
  385. list = append(list, ssDescribeSpace(space, userinfo.Username))
  386. }
  387. js := mrMarshalOrDrop(list)
  388. utils.SendJSONResponse(w, string(js))
  389. })
  390. //List every public space (the discovery directory).
  391. router.HandleFunc("/system/sharedspace/listpublic", func(w http.ResponseWriter, r *http.Request) {
  392. userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
  393. if err != nil {
  394. utils.SendErrorResponse(w, "Not logged in")
  395. return
  396. }
  397. public := sharedSpaceManager.ListPublicSpaces()
  398. list := make([]map[string]interface{}, 0, len(public))
  399. for _, space := range public {
  400. list = append(list, ssDescribeSpace(space, userinfo.Username))
  401. }
  402. js := mrMarshalOrDrop(list)
  403. utils.SendJSONResponse(w, string(js))
  404. })
  405. //Self-join a public (or open) space.
  406. router.HandleFunc("/system/sharedspace/join", func(w http.ResponseWriter, r *http.Request) {
  407. username, space, ok := ssResolveSpaceRequest(w, r, true)
  408. if !ok {
  409. return
  410. }
  411. if err := space.JoinPublic(username); err != nil {
  412. utils.SendErrorResponse(w, err.Error())
  413. return
  414. }
  415. utils.SendOK(w)
  416. })
  417. //Leave a space.
  418. router.HandleFunc("/system/sharedspace/leave", func(w http.ResponseWriter, r *http.Request) {
  419. username, space, ok := ssResolveSpaceRequest(w, r, true)
  420. if !ok {
  421. return
  422. }
  423. if err := space.RemoveMember(username, username); err != nil {
  424. utils.SendErrorResponse(w, err.Error())
  425. return
  426. }
  427. utils.SendOK(w)
  428. })
  429. //Invite a user into the space; managers only.
  430. router.HandleFunc("/system/sharedspace/members/add", func(w http.ResponseWriter, r *http.Request) {
  431. username, space, ok := ssResolveSpaceRequest(w, r, true)
  432. if !ok {
  433. return
  434. }
  435. target, err := utils.PostPara(r, "username")
  436. if err != nil {
  437. utils.SendErrorResponse(w, "Missing username")
  438. return
  439. }
  440. role, _ := utils.PostPara(r, "role")
  441. if role == "" {
  442. role = sharedspace.RoleMember
  443. }
  444. //The invitee must be a real user on this host
  445. if _, err := userHandler.GetUserInfoFromUsername(target); err != nil {
  446. utils.SendErrorResponse(w, "User not found")
  447. return
  448. }
  449. if err := space.AddMember(username, target, role); err != nil {
  450. utils.SendErrorResponse(w, err.Error())
  451. return
  452. }
  453. utils.SendOK(w)
  454. })
  455. //Remove a member; managers (or the member themselves) only.
  456. router.HandleFunc("/system/sharedspace/members/remove", func(w http.ResponseWriter, r *http.Request) {
  457. username, space, ok := ssResolveSpaceRequest(w, r, true)
  458. if !ok {
  459. return
  460. }
  461. target, err := utils.PostPara(r, "username")
  462. if err != nil {
  463. utils.SendErrorResponse(w, "Missing username")
  464. return
  465. }
  466. if err := space.RemoveMember(username, target); err != nil {
  467. utils.SendErrorResponse(w, err.Error())
  468. return
  469. }
  470. utils.SendOK(w)
  471. })
  472. //Change a member's role; managers only.
  473. router.HandleFunc("/system/sharedspace/members/role", func(w http.ResponseWriter, r *http.Request) {
  474. username, space, ok := ssResolveSpaceRequest(w, r, true)
  475. if !ok {
  476. return
  477. }
  478. target, err := utils.PostPara(r, "username")
  479. if err != nil {
  480. utils.SendErrorResponse(w, "Missing username")
  481. return
  482. }
  483. role, err := utils.PostPara(r, "role")
  484. if err != nil {
  485. utils.SendErrorResponse(w, "Missing role")
  486. return
  487. }
  488. if err := space.SetMemberRole(username, target, role); err != nil {
  489. utils.SendErrorResponse(w, err.Error())
  490. return
  491. }
  492. utils.SendOK(w)
  493. })
  494. //Change the access mode; managers only.
  495. router.HandleFunc("/system/sharedspace/access", func(w http.ResponseWriter, r *http.Request) {
  496. username, space, ok := ssResolveSpaceRequest(w, r, true)
  497. if !ok {
  498. return
  499. }
  500. access, err := utils.PostPara(r, "access")
  501. if err != nil {
  502. utils.SendErrorResponse(w, "Missing access mode")
  503. return
  504. }
  505. if err := space.SetAccess(username, access); err != nil {
  506. utils.SendErrorResponse(w, err.Error())
  507. return
  508. }
  509. utils.SendOK(w)
  510. })
  511. //Set (or with an empty value delete) a metadata entry; managers only.
  512. router.HandleFunc("/system/sharedspace/meta", func(w http.ResponseWriter, r *http.Request) {
  513. username, space, ok := ssResolveSpaceRequest(w, r, true)
  514. if !ok {
  515. return
  516. }
  517. key, err := utils.PostPara(r, "key")
  518. if err != nil {
  519. utils.SendErrorResponse(w, "Missing key")
  520. return
  521. }
  522. value, _ := utils.PostPara(r, "value")
  523. if err := space.SetMeta(username, key, value); err != nil {
  524. utils.SendErrorResponse(w, err.Error())
  525. return
  526. }
  527. utils.SendOK(w)
  528. })
  529. //List the items in a space.
  530. router.HandleFunc("/system/sharedspace/items", func(w http.ResponseWriter, r *http.Request) {
  531. _, space, ok := ssResolveSpaceRequest(w, r, false)
  532. if !ok {
  533. return
  534. }
  535. items := space.Items()
  536. list := make([]map[string]interface{}, 0, len(items))
  537. for _, item := range items {
  538. list = append(list, ssDescribeItem(item))
  539. }
  540. js := mrMarshalOrDrop(list)
  541. utils.SendJSONResponse(w, string(js))
  542. })
  543. //Post a text snippet.
  544. router.HandleFunc("/system/sharedspace/addtext", func(w http.ResponseWriter, r *http.Request) {
  545. username, space, ok := ssResolveSpaceRequest(w, r, true)
  546. if !ok {
  547. return
  548. }
  549. text, err := utils.PostPara(r, "text")
  550. if err != nil {
  551. utils.SendErrorResponse(w, "Missing text")
  552. return
  553. }
  554. item, err := space.AddText(username, text, "http")
  555. if err != nil {
  556. utils.SendErrorResponse(w, err.Error())
  557. return
  558. }
  559. js := mrMarshalOrDrop(map[string]interface{}{"itemid": item.ID})
  560. utils.SendJSONResponse(w, string(js))
  561. })
  562. //Upload a file / image into the space.
  563. router.HandleFunc("/system/sharedspace/upload", func(w http.ResponseWriter, r *http.Request) {
  564. userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
  565. if err != nil {
  566. utils.SendErrorResponse(w, "Not logged in")
  567. return
  568. }
  569. if err := r.ParseMultipartForm(32 << 20); err != nil {
  570. utils.SendErrorResponse(w, "Invalid upload")
  571. return
  572. }
  573. space, exists := sharedSpaceManager.GetSpace(r.FormValue("spaceid"))
  574. if !exists || !space.CanPost(userinfo.Username) {
  575. utils.SendErrorResponse(w, "Space not found")
  576. return
  577. }
  578. file, header, err := r.FormFile("file")
  579. if err != nil {
  580. utils.SendErrorResponse(w, "Missing file")
  581. return
  582. }
  583. defer file.Close()
  584. itemType := sharedspace.ItemTypeFile
  585. if sharedspace.IsImageName(header.Filename) {
  586. itemType = sharedspace.ItemTypeImage
  587. }
  588. item, err := space.SaveBlob(itemType, header.Filename, userinfo.Username, "http", file, sharedSpaceManager.MaxUpload())
  589. if err != nil {
  590. utils.SendErrorResponse(w, err.Error())
  591. return
  592. }
  593. js := mrMarshalOrDrop(map[string]interface{}{
  594. "itemid": item.ID,
  595. "name": item.Name,
  596. "size": item.Size,
  597. })
  598. utils.SendJSONResponse(w, string(js))
  599. })
  600. //Download an item blob. inline=1 serves raster images for in-browser
  601. //display; everything else (notably SVG, which can carry scripts) keeps
  602. //the attachment disposition.
  603. router.HandleFunc("/system/sharedspace/download", func(w http.ResponseWriter, r *http.Request) {
  604. _, space, ok := ssResolveSpaceRequest(w, r, false)
  605. if !ok {
  606. return
  607. }
  608. itemID, err := utils.GetPara(r, "itemid")
  609. if err != nil {
  610. utils.SendErrorResponse(w, "Missing item ID")
  611. return
  612. }
  613. item, exists := space.GetItem(itemID)
  614. if !exists || item.DiskPath == "" {
  615. http.NotFound(w, r)
  616. return
  617. }
  618. f, err := os.Open(item.DiskPath)
  619. if err != nil {
  620. http.NotFound(w, r)
  621. return
  622. }
  623. defer f.Close()
  624. //Serve with the original name; the ASCII fallback strips anything
  625. //that could break the header, the RFC 5987 form keeps unicode names.
  626. fallback := strings.Map(func(c rune) rune {
  627. if c < 32 || c == '"' || c == '\\' || c > 126 {
  628. return '_'
  629. }
  630. return c
  631. }, item.Name)
  632. disposition := "attachment"
  633. if r.URL.Query().Get("inline") == "1" && sharedspace.IsImageName(item.Name) {
  634. disposition = "inline"
  635. }
  636. w.Header().Set("Content-Disposition", disposition+"; filename=\""+fallback+"\"; filename*=UTF-8''"+url.PathEscape(item.Name))
  637. w.Header().Set("X-Content-Type-Options", "nosniff")
  638. if ctype := mime.TypeByExtension(strings.ToLower(filepathExt(item.Name))); ctype != "" {
  639. w.Header().Set("Content-Type", ctype)
  640. } else {
  641. w.Header().Set("Content-Type", "application/octet-stream")
  642. }
  643. http.ServeContent(w, r, "", time.Now(), f)
  644. })
  645. //Remove an item; uploader, space managers or the system.
  646. router.HandleFunc("/system/sharedspace/removeitem", func(w http.ResponseWriter, r *http.Request) {
  647. username, space, ok := ssResolveSpaceRequest(w, r, true)
  648. if !ok {
  649. return
  650. }
  651. itemID, err := utils.PostPara(r, "itemid")
  652. if err != nil {
  653. utils.SendErrorResponse(w, "Missing item ID")
  654. return
  655. }
  656. if err := space.RemoveItem(itemID, username); err != nil {
  657. utils.SendErrorResponse(w, err.Error())
  658. return
  659. }
  660. utils.SendOK(w)
  661. })
  662. //Create a collaborative document.
  663. router.HandleFunc("/system/sharedspace/doc/create", func(w http.ResponseWriter, r *http.Request) {
  664. username, space, ok := ssResolveSpaceRequest(w, r, true)
  665. if !ok {
  666. return
  667. }
  668. name, _ := utils.PostPara(r, "name")
  669. doc, err := space.CreateDoc(username, name)
  670. if err != nil {
  671. utils.SendErrorResponse(w, err.Error())
  672. return
  673. }
  674. js := mrMarshalOrDrop(ssDescribeDoc(doc, true))
  675. utils.SendJSONResponse(w, string(js))
  676. })
  677. //List documents (without contents).
  678. router.HandleFunc("/system/sharedspace/doc/list", func(w http.ResponseWriter, r *http.Request) {
  679. _, space, ok := ssResolveSpaceRequest(w, r, false)
  680. if !ok {
  681. return
  682. }
  683. docs := space.ListDocs()
  684. list := make([]map[string]interface{}, 0, len(docs))
  685. for _, doc := range docs {
  686. list = append(list, ssDescribeDoc(doc, false))
  687. }
  688. js := mrMarshalOrDrop(list)
  689. utils.SendJSONResponse(w, string(js))
  690. })
  691. //Fetch one document with content (also the conflict recovery path).
  692. router.HandleFunc("/system/sharedspace/doc/get", func(w http.ResponseWriter, r *http.Request) {
  693. _, space, ok := ssResolveSpaceRequest(w, r, false)
  694. if !ok {
  695. return
  696. }
  697. docID, err := utils.GetPara(r, "docid")
  698. if err != nil {
  699. utils.SendErrorResponse(w, "Missing document ID")
  700. return
  701. }
  702. doc, exists := space.GetDoc(docID)
  703. if !exists {
  704. utils.SendErrorResponse(w, "Document not found")
  705. return
  706. }
  707. js := mrMarshalOrDrop(ssDescribeDoc(doc, true))
  708. utils.SendJSONResponse(w, string(js))
  709. })
  710. //Compare-and-swap document update over HTTP.
  711. router.HandleFunc("/system/sharedspace/doc/update", func(w http.ResponseWriter, r *http.Request) {
  712. username, space, ok := ssResolveSpaceRequest(w, r, true)
  713. if !ok {
  714. return
  715. }
  716. docID, err := utils.PostPara(r, "docid")
  717. if err != nil {
  718. utils.SendErrorResponse(w, "Missing document ID")
  719. return
  720. }
  721. baseRevStr, err := utils.PostPara(r, "baserev")
  722. if err != nil {
  723. utils.SendErrorResponse(w, "Missing base revision")
  724. return
  725. }
  726. baseRev, err := strconv.ParseInt(baseRevStr, 10, 64)
  727. if err != nil {
  728. utils.SendErrorResponse(w, "Invalid base revision")
  729. return
  730. }
  731. content, _ := utils.PostPara(r, "content")
  732. doc, err := space.UpdateDoc(username, docID, baseRev, content)
  733. if err == sharedspace.ErrRevisionConflict {
  734. //Report the current revision so the client can re-fetch + rebase
  735. current, _ := space.GetDoc(docID)
  736. revision := int64(0)
  737. if current != nil {
  738. revision = current.Revision
  739. }
  740. js := mrMarshalOrDrop(map[string]interface{}{
  741. "error": "revision conflict",
  742. "revision": revision,
  743. })
  744. utils.SendJSONResponse(w, string(js))
  745. return
  746. }
  747. if err != nil {
  748. utils.SendErrorResponse(w, err.Error())
  749. return
  750. }
  751. js := mrMarshalOrDrop(map[string]interface{}{
  752. "ok": true,
  753. "revision": doc.Revision,
  754. })
  755. utils.SendJSONResponse(w, string(js))
  756. })
  757. //Delete a document; creator or space managers.
  758. router.HandleFunc("/system/sharedspace/doc/delete", func(w http.ResponseWriter, r *http.Request) {
  759. username, space, ok := ssResolveSpaceRequest(w, r, true)
  760. if !ok {
  761. return
  762. }
  763. docID, err := utils.PostPara(r, "docid")
  764. if err != nil {
  765. utils.SendErrorResponse(w, "Missing document ID")
  766. return
  767. }
  768. if err := space.DeleteDoc(username, docID); err != nil {
  769. utils.SendErrorResponse(w, err.Error())
  770. return
  771. }
  772. utils.SendOK(w)
  773. })
  774. //WebSocket: realtime bidirectional exchange on a space.
  775. router.HandleFunc("/system/sharedspace/ws", ssHandleWebSocket)
  776. //Admin surface: System Settings > Shared Spaces
  777. adminRouter := prout.NewModuleRouter(prout.RouterOption{
  778. ModuleName: "System Settings",
  779. AdminOnly: true,
  780. UserHandler: userHandler,
  781. DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
  782. errorHandlePermissionDenied(w, r)
  783. },
  784. })
  785. //Enumerate every space with usage figures.
  786. adminRouter.HandleFunc("/system/sharedspace/admin/list", func(w http.ResponseWriter, r *http.Request) {
  787. all := sharedSpaceManager.ListSpaces()
  788. list := make([]map[string]interface{}, 0, len(all))
  789. for _, space := range all {
  790. desc := ssDescribeSpace(space, "")
  791. desc["subscribers"] = space.Channel().Count()
  792. desc["diskusage"] = sharedSpaceManager.SpaceDiskUsage(space.ID)
  793. list = append(list, desc)
  794. }
  795. js := mrMarshalOrDrop(list)
  796. utils.SendJSONResponse(w, string(js))
  797. })
  798. //Delete any space with system authority.
  799. adminRouter.HandleFunc("/system/sharedspace/admin/delete", func(w http.ResponseWriter, r *http.Request) {
  800. spaceID, err := utils.PostPara(r, "spaceid")
  801. if err != nil {
  802. utils.SendErrorResponse(w, "Missing space ID")
  803. return
  804. }
  805. if _, exists := sharedSpaceManager.GetSpace(spaceID); !exists {
  806. utils.SendErrorResponse(w, "Space not found")
  807. return
  808. }
  809. ssCloseSpace(spaceID)
  810. utils.SendOK(w)
  811. })
  812. //Current admin configuration.
  813. adminRouter.HandleFunc("/system/sharedspace/admin/config", func(w http.ResponseWriter, r *http.Request) {
  814. js := mrMarshalOrDrop(map[string]interface{}{
  815. "maxupload": sharedSpaceManager.MaxUpload(),
  816. "maxitems": sharedSpaceManager.DefaultItemLimit(),
  817. "allowpersistent": sharedSpaceManager.PersistenceAvailable(),
  818. "retentiondays": ssConfInt64(ssConfRetentionDays, 0),
  819. "spaces": sharedSpaceManager.SpaceCount(),
  820. })
  821. utils.SendJSONResponse(w, string(js))
  822. })
  823. //Update the admin configuration (persisted; applied live).
  824. adminRouter.HandleFunc("/system/sharedspace/admin/setconfig", func(w http.ResponseWriter, r *http.Request) {
  825. if maxUpload, err := utils.PostPara(r, "maxupload"); err == nil {
  826. if value, err := strconv.ParseInt(maxUpload, 10, 64); err == nil && value > 0 {
  827. sysdb.Write(ssDBTable, ssConfMaxUpload, value)
  828. sharedSpaceManager.SetMaxUpload(value)
  829. }
  830. }
  831. if maxItems, err := utils.PostPara(r, "maxitems"); err == nil {
  832. if value, err := strconv.Atoi(maxItems); err == nil && value > 0 {
  833. sysdb.Write(ssDBTable, ssConfMaxItems, int64(value))
  834. sharedSpaceManager.SetDefaultItemLimit(value)
  835. }
  836. }
  837. if allowPersistent, err := utils.PostPara(r, "allowpersistent"); err == nil {
  838. enabled := allowPersistent == "true"
  839. sysdb.Write(ssDBTable, ssConfAllowPersistent, enabled)
  840. sharedSpaceManager.SetAllowPersistent(enabled)
  841. }
  842. if retentionDays, err := utils.PostPara(r, "retentiondays"); err == nil {
  843. if value, err := strconv.ParseInt(retentionDays, 10, 64); err == nil && value >= 0 {
  844. sysdb.Write(ssDBTable, ssConfRetentionDays, value)
  845. }
  846. }
  847. utils.SendOK(w)
  848. })
  849. //Settings tile in System Settings
  850. registerSetting(settingModule{
  851. Name: "Shared Spaces",
  852. Desc: "Collaboration spaces: storage, limits and cleanup",
  853. IconPath: "SystemAO/system_setting/img/module.svg",
  854. Group: "Advance",
  855. StartDir: "SystemAO/sharedspace/spaces.html",
  856. RequireAdmin: true,
  857. })
  858. }
  859. // ssHandleWebSocket upgrades the request and joins the caller to the
  860. // space's realtime channel.
  861. func ssHandleWebSocket(w http.ResponseWriter, r *http.Request) {
  862. userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
  863. if err != nil {
  864. http.Error(w, "Not logged in", http.StatusUnauthorized)
  865. return
  866. }
  867. username := userinfo.Username
  868. space, exists := sharedSpaceManager.GetSpace(r.URL.Query().Get("spaceid"))
  869. if !exists || !space.CanRead(username) {
  870. http.Error(w, "Space not found", http.StatusForbidden)
  871. return
  872. }
  873. conn, err := ssUpgrader.Upgrade(w, r, nil)
  874. if err != nil {
  875. return
  876. }
  877. conn.SetReadLimit(ssMaxSocketFrame)
  878. //Liveness: a client that goes silent (no frames and no pong replies)
  879. //past ssReadTimeout is dropped so it does not linger as a ghost.
  880. conn.SetReadDeadline(time.Now().Add(ssReadTimeout))
  881. conn.SetPongHandler(func(string) error {
  882. conn.SetReadDeadline(time.Now().Add(ssReadTimeout))
  883. return nil
  884. })
  885. channel := space.Channel()
  886. sub, err := channel.Join(username)
  887. if err != nil {
  888. conn.Close()
  889. return
  890. }
  891. //Fan space mutations out to this channel as protocol frames. The fixed
  892. //key makes re-registration on every connection idempotent.
  893. space.SubscribeEvents("wstransport", ssBroadcastSpaceEvent(space))
  894. //Writer: drain the send buffer until it is closed, interleaving
  895. //keepalive pings, then hang up.
  896. go func() {
  897. pinger := time.NewTicker(ssPingInterval)
  898. defer pinger.Stop()
  899. defer conn.Close()
  900. for {
  901. select {
  902. case msg, ok := <-sub.Send:
  903. if !ok {
  904. return
  905. }
  906. conn.SetWriteDeadline(time.Now().Add(ssWriteTimeout))
  907. if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
  908. return
  909. }
  910. case <-pinger.C:
  911. conn.SetWriteDeadline(time.Now().Add(ssWriteTimeout))
  912. if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
  913. return
  914. }
  915. }
  916. }
  917. }()
  918. //Welcome frame: own identity, space descriptor and current peers.
  919. peers := []map[string]interface{}{}
  920. for _, peer := range channel.Subscribers() {
  921. if peer.ID == sub.ID {
  922. continue
  923. }
  924. peers = append(peers, map[string]interface{}{"subid": peer.ID, "username": peer.Username})
  925. }
  926. channel.SendTo(sub.ID, mrMarshalOrDrop(map[string]interface{}{
  927. "type": "welcome",
  928. "subid": sub.ID,
  929. "username": username,
  930. "space": ssDescribeSpace(space, username),
  931. "peers": peers,
  932. }))
  933. //Announce the newcomer to everyone else.
  934. channel.Broadcast(mrMarshalOrDrop(map[string]interface{}{
  935. "type": "peer-join",
  936. "peer": map[string]interface{}{"subid": sub.ID, "username": username},
  937. }), sub.ID)
  938. defer func() {
  939. channel.Leave(sub.ID)
  940. channel.Broadcast(mrMarshalOrDrop(map[string]interface{}{
  941. "type": "peer-leave",
  942. "subid": sub.ID,
  943. "username": username,
  944. }), -1)
  945. }()
  946. sendError := func(message string) {
  947. channel.SendTo(sub.ID, mrMarshalOrDrop(map[string]interface{}{
  948. "type": "error",
  949. "error": message,
  950. }))
  951. }
  952. for {
  953. _, raw, err := conn.ReadMessage()
  954. if err != nil {
  955. return
  956. }
  957. conn.SetReadDeadline(time.Now().Add(ssReadTimeout))
  958. var frame struct {
  959. Type string `json:"type"`
  960. To int `json:"to"`
  961. Data json.RawMessage `json:"data"`
  962. Text string `json:"text"`
  963. DocID string `json:"docid"`
  964. BaseRev int64 `json:"baserev"`
  965. Content string `json:"content"`
  966. }
  967. if json.Unmarshal(raw, &frame) != nil {
  968. continue
  969. }
  970. switch frame.Type {
  971. case "signal":
  972. //Ephemeral point-to-point relay (WebRTC SDP/ICE and friends)
  973. channel.SendTo(frame.To, mrMarshalOrDrop(map[string]interface{}{
  974. "type": "signal",
  975. "from": sub.ID,
  976. "data": frame.Data,
  977. }))
  978. case "broadcast":
  979. //Ephemeral application fan-out to every other subscriber
  980. channel.Broadcast(mrMarshalOrDrop(map[string]interface{}{
  981. "type": "broadcast",
  982. "from": sub.ID,
  983. "username": username,
  984. "data": frame.Data,
  985. }), sub.ID)
  986. case "chat":
  987. text := frame.Text
  988. if strings.TrimSpace(text) == "" {
  989. continue
  990. }
  991. if runes := []rune(text); len(runes) > ssMaxChatLength {
  992. text = string(runes[:ssMaxChatLength])
  993. }
  994. //Persisted: delivery to every subscriber happens through the
  995. //item-added event fan-out (no double send)
  996. if _, err := space.AddText(username, text, "ws"); err != nil {
  997. sendError(err.Error())
  998. }
  999. case "doc-update":
  1000. _, err := space.UpdateDoc(username, frame.DocID, frame.BaseRev, frame.Content)
  1001. if err == sharedspace.ErrRevisionConflict {
  1002. current, _ := space.GetDoc(frame.DocID)
  1003. revision := int64(0)
  1004. if current != nil {
  1005. revision = current.Revision
  1006. }
  1007. channel.SendTo(sub.ID, mrMarshalOrDrop(map[string]interface{}{
  1008. "type": "doc-conflict",
  1009. "docid": frame.DocID,
  1010. "revision": revision,
  1011. }))
  1012. } else if err != nil {
  1013. sendError(err.Error())
  1014. }
  1015. //Success needs no direct reply: the doc-updated event fan-out
  1016. //delivers the accepted patch (with the new revision) to
  1017. //everyone, the sender included.
  1018. case "ping":
  1019. channel.SendTo(sub.ID, []byte(`{"type":"pong"}`))
  1020. }
  1021. }
  1022. }