Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "ui/extra/simplerenderer/librm_lines"]
path = ui/extra/simplerenderer/librm_lines
url = https://github.com/RedTTGMoss/librm_lines
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ See the [documentation](https://ddvk.github.io/rmfakecloud/remarkable/setup/) fo

run `./dev.sh` which should start the UI and backend

### `.rm` page decoder (lines format)

To inspect raw handwriting pages (`*.rm`), see **[internal/rmdecode/README.md](internal/rmdecode/README.md)**. It documents **v3/v5** (Go) vs **v6** (Python + [rmscene](https://github.com/ricklupton/rmscene)), and points to **[rmc](https://github.com/ricklupton/rmc)** for production-grade v6 SVG/PDF export from the ecosystem.

The **rmscene** library is included as a git submodule at **`third_party/rmscene`**. After a fresh clone run: `git submodule update --init --recursive`.

```bash
go run ./cmd/rmdecode /path/to/page.rm
go run ./cmd/rmdecode -o strokes.svg -format svg /path/to/page.rm
go run ./cmd/rmdecode -o strokes.pdf /path/to/page.rm
```

v3/v5 SVG/PDF are rendered in Go; v6 SVG/PDF use the `rmc` CLI if installed.

**`.rmdoc` → PNG (all pages, paged filenames):** `go run ./cmd/rmdoc2png -o outdir doc.rmdoc` — writes `001_name.png`, `002_…` for each `*.rm` (v3/v5 in Go; v6 needs Python + Pillow + vendored [rmscene](https://github.com/ricklupton/rmscene)). See `internal/rmdecode/README.md`.

### Caveats/ WARNING

- (applies when you don't have security, version <= 0.0.3) connecting to the api will delete all your files, unless you mark them as not synced `synced:false` prior to syncing (advisable just to disconnect, reconnect the cloud)
Expand Down
2 changes: 2 additions & 0 deletions docs/install/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ The configuration is made through environment variables.
| `RM_HTTPS_COOKIE` | For the UI, force cookies to be available only via https |
| `RM_TRUST_PROXY` | Trust the proxy for client ip addresses (X-Forwarded-For/X-Real-IP) default false |
| `HASH_SCHEMA_VERSION` | Hash tree schema version: "3" or "4" (default: 3) |
| `RMFAKECLOUD_ALLOW_SU` | Enable admin **su** (impersonate another user) in the web UI (default: `false`). Env-only — not editable from the web UI. When disabled, `POST /su` is forbidden; an active su session can still leave via `POST /su/leave`. |
| `RMFAKECLOUD_RMC_SRC` | Path to the [rmc](https://github.com/ricklupton/rmc) source `src` directory (e.g. `/home/aaron/Documents/rmc-main/src`) for v6 `.rm` conversion. Also editable by admins under **Admin → Rendering** (saved to `DATADIR/server_settings.json`, which overrides this env value). |

## Handwriting recognition

Expand Down
16 changes: 14 additions & 2 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/ddvk/rmfakecloud/internal/ui"

"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
)

const (
Expand Down Expand Up @@ -116,7 +117,6 @@ func (app *App) Stop() {
}
}


// NewApp constructs an app
func NewApp(cfg *config.Config) App {
debugMode := log.GetLevel() >= log.DebugLevel
Expand Down Expand Up @@ -154,6 +154,7 @@ func NewApp(cfg *config.Config) App {

// Register the middleware
// router.Use(cors.New(corsConfig))
router.Use(userAgentLoggerMiddleware())

if debugMode {
router.Use(requestLoggerMiddleware())
Expand All @@ -180,7 +181,18 @@ func NewApp(cfg *config.Config) App {

app.registerRoutes(router)

uiApp := ui.New(cfg, fsStorage, codeConnector, ntfHub, pcStore, fsStorage, fsStorage, roomMgr, app.mqttBroker)
issueDeviceToken := func(uid, deviceID, deviceDesc string) (string, error) {
claims := &DeviceClaims{
UserID: uid,
DeviceID: deviceID,
DeviceDesc: deviceDesc,
StandardClaims: jwt.StandardClaims{
Audience: APIUsage,
},
}
return common.SignClaims(claims, cfg.JWTSecretKey)
}
uiApp := ui.New(cfg, fsStorage, codeConnector, ntfHub, pcStore, fsStorage, fsStorage, issueDeviceToken, roomMgr, app.mqttBroker)
uiApp.RegisterRoutes(router)

storageapp := fs.NewApp(cfg, fsStorage)
Expand Down
44 changes: 32 additions & 12 deletions internal/app/codeconnector.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,50 +10,55 @@ import (
log "github.com/sirupsen/logrus"
)

type codeEntry struct {
uid string
expiresAt time.Time
}

type inMemoryCodeConnector struct {
dict map[string]string
dict map[string]codeEntry
uids map[string]string
lock sync.Mutex
codeValidity time.Duration
}

// CodeConnector matches a code to users
type CodeConnector interface {
//NewCode generates one time code for a user
// NewCode generates one time code for a user
NewCode(uid string) (code string, err error)

//ConsumeCode a code and returns the uid if ofound
// ConsumeCode consumes a code and returns the uid if found
ConsumeCode(code string) (uid string, err error)
// CodeStatus returns expiration and whether the user's code is still valid
CodeStatus(uid string) (expiresAt time.Time, valid bool)
}

// NewCodeConnector constructor
func NewCodeConnector() CodeConnector {
return &inMemoryCodeConnector{
dict: make(map[string]string),
dict: make(map[string]codeEntry),
uids: make(map[string]string),
codeValidity: time.Minute * 5,
}

}

func (conn *inMemoryCodeConnector) NewCode(uid string) (string, error) {
code, err := newUserCode()
if err != nil {
return "", err
}
expiresAt := time.Now().Add(conn.codeValidity)
conn.lock.Lock()
conn.dict[code] = uid
if oldcode, ok := conn.uids[uid]; ok {
delete(conn.dict, oldcode)
}
conn.dict[code] = codeEntry{uid: uid, expiresAt: expiresAt}
conn.uids[uid] = code
conn.lock.Unlock()
go func() {
<-time.After(conn.codeValidity)
if _, err := conn.ConsumeCode(code); err == nil {
log.Infof("removed unused code: %s for uid: %s ", code, uid)
}

}()
return code, nil
}
Expand Down Expand Up @@ -84,14 +89,29 @@ func newUserCode() (code string, err error) {
// return code, nil
}

// ConsumeCode return the userId matching the
// ConsumeCode returns the userId matching the code
func (conn *inMemoryCodeConnector) ConsumeCode(code string) (string, error) {
conn.lock.Lock()
defer conn.lock.Unlock()
if uid, ok := conn.dict[code]; ok {
if ent, ok := conn.dict[code]; ok {
delete(conn.dict, code)
delete(conn.uids, uid)
return uid, nil
delete(conn.uids, ent.uid)
return ent.uid, nil
}
return "", errors.New("code not found")
}

// CodeStatus returns the expiration time and whether the user's code is still valid
func (conn *inMemoryCodeConnector) CodeStatus(uid string) (expiresAt time.Time, valid bool) {
conn.lock.Lock()
defer conn.lock.Unlock()
code, ok := conn.uids[uid]
if !ok {
return time.Time{}, false
}
ent, ok := conn.dict[code]
if !ok {
return time.Time{}, false
}
return ent.expiresAt, true
}
45 changes: 43 additions & 2 deletions internal/app/handlers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app

import (
"bufio"
"bytes"
"crypto/rand"
"encoding/base64"
Expand Down Expand Up @@ -112,6 +113,15 @@ func (app *App) newDevice(c *gin.Context) {
return
}

if user, err := app.userStorer.GetUser(uid); err == nil && user != nil {
user.UpsertRegisteredDevice(tokenRequest.DeviceID, tokenRequest.DeviceDesc, tokenRequest.DeviceLink)
if err := app.userStorer.UpdateUser(user); err != nil {
log.Warn("could not persist registered device: ", err)
}
} else if err != nil {
log.Warn("could not load user for device registration: ", err)
}

c.String(http.StatusOK, tokenString)
}

Expand All @@ -123,6 +133,14 @@ func (app *App) deleteDevice(c *gin.Context) {
return
}
log.Info("Logging out: ", deviceToken.UserID)
if user, err := app.userStorer.GetUser(deviceToken.UserID); err == nil && user != nil {
user.RemoveRegisteredDevice(deviceToken.DeviceID)
if err := app.userStorer.UpdateUser(user); err != nil {
log.Warn("could not update user after device logout: ", err)
}
} else if err != nil {
log.Warn("could not load user on device logout: ", err)
}
c.Status(http.StatusNoContent)
}

Expand Down Expand Up @@ -899,7 +917,13 @@ func (app *App) blobStorageRead(c *gin.Context) {
defer reader.Close()
common.AddHashHeader(c, hash)

c.DataFromReader(http.StatusOK, size, "application/octet-stream", reader, nil)
br := bufio.NewReader(reader)
ct := "application/octet-stream"
if b, err := br.Peek(5); err == nil && string(b) == "%PDF-" {
ct = "application/pdf"
}
c.Header("X-Content-Type-Options", "nosniff")
c.DataFromReader(http.StatusOK, size, ct, br, nil)
}

func (app *App) blobStorageWrite(c *gin.Context) {
Expand Down Expand Up @@ -1032,6 +1056,17 @@ func (app *App) integrationsUpload(c *gin.Context) {
name := common.QueryS("name", c)
fileType := common.QueryS("fileType", c)

ro, err := integrations.IsReadOnly(app.userStorer, uid, integrationID)
if err != nil {
log.Error(fmt.Errorf("can't check integration mode, %v", err))
c.AbortWithStatus(http.StatusInternalServerError)
return
}
if ro {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "integration folder is read-only"})
return
}

integrationProvider, err := integrations.GetStorageIntegrationProvider(app.userStorer, uid, integrationID)

if err != nil {
Expand Down Expand Up @@ -1072,7 +1107,13 @@ func (app *App) integrationsGetFile(c *gin.Context) {

defer reader.Close()

c.DataFromReader(http.StatusOK, size, "application/octet-stream", reader, nil)
br := bufio.NewReader(reader)
ct := "application/octet-stream"
if b, err := br.Peek(5); err == nil && string(b) == "%PDF-" {
ct = "application/pdf"
}
c.Header("X-Content-Type-Options", "nosniff")
c.DataFromReader(http.StatusOK, size, ct, br, nil)
}

func (app *App) integrationsList(c *gin.Context) {
Expand Down
12 changes: 12 additions & 0 deletions internal/app/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
const (
authLog = "[auth-middleware]"
requestLog = "[requestlogging-middleware]"
userAgentLog = "[user-agent-middleware]"
syncDefault = "sync:default"
syncNew = "sync:tortoise"
syncNewLimited = "sync:fox" // Display cloud limit messages
Expand Down Expand Up @@ -95,3 +96,14 @@ func requestLoggerMiddleware() gin.HandlerFunc {
c.Next()
}
}

func userAgentLoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
ua := c.Request.UserAgent()
if ua == "" {
ua = "-"
}
c.Next()
log.Infof("%s %s %s %d ua=%q", userAgentLog, c.Request.Method, c.Request.URL.Path, c.Writer.Status(), ua)
}
}
Loading