Skip to content

Commit

Permalink
🔀 Merge pull request #12 from yoavst/feature/multiuser
Browse files Browse the repository at this point in the history
Feature/multiuser
  • Loading branch information
yoavst authored May 12, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 5c019cd + aa84e78 commit a1eb190
Showing 35 changed files with 1,182 additions and 253 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -5,8 +5,6 @@ on:
branches:
- "master"
pull_request:
branches:
- "master"
workflow_dispatch:

permissions:
@@ -23,10 +21,11 @@ jobs:
- { name: jeb, type: pybunch }
- { name: intellij, type: java }
- { name: clion, type: java }
- { name: ida, type: pack }
- { name: ida, type: pybunch }
- { name: vscode, type: typescript }
- { name: opengrok_sourcegraph, type: typescript }
- { name: jadx, type: pack }
- { name: server, type: pybunch }
steps:
- name: Checkout repository
uses: actions/checkout@v4
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.SHELLFLAGS := -ec

# Load version
FILE := version.txt
ifeq ($(wildcard $(FILE)),)
@@ -24,7 +26,8 @@ frontends: web visio

ida:
@echo "Building Graffiti for IDA"
echo "# Graffiti for IDA, Version: $(VERSION)" | cat - backends/ida/graffiti.py > graffiti.py
echo "# Graffiti for IDA, Version: $(VERSION)" > graffiti.py
python3 -m pybunch -d backends/ida -e graffiti -so -o graffiti.py
zip -j out/graffiti_v$(VERSION)_for_ida.zip graffiti.py
rm graffiti.py

@@ -35,9 +38,9 @@ jadx:
jeb_packed_%:
@echo Packing $*
mkdir -p backends/jeb/packed && \
( (cat backends/jeb/$*.py | awk '/^#/ {print} !/^#/ {exit}') &&\
(cat backends/jeb/$*.py | awk '/^#/ {print} !/^#/ {exit}') &&\
echo && echo &&\
python3 -m pybunch -d backends/jeb -e $* -so ) | cat > backends/jeb/packed/$*.py
python3 -m pybunch -d backends/jeb -e $* -so -o backends/jeb/packed/$*.py

JEB_SCRIPTS := $(shell grep -rlP '^#\?' backends/jeb | sed 's/.*\///;s/\.[^.]*$$//')

@@ -120,4 +123,4 @@ visio:

server:
@echo "Building the graffiti Server"
cp server/main.py out/graffiti_v$(VERSION)_server.py
python3 -m pybunch -d server -e graffiti -so -o out/graffiti_v$(VERSION)_server.py
88 changes: 54 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -16,54 +16,40 @@ Create customized callgraph directly from your favorite editor.
- Multiple tabs
- Rename in the editor? the change will propagate to the graph.

## Architecture
## Setup

![Architecture](docs/images/architecture.svg)
Graffiti was built with the following assumptions:
The setup consits of three components: server, backend and frontend. See [Architecture section](#architecture) for more info.

- You might use more than a single editor for a project.
- You might want to run everything locally.
- It should be easy to use.
### Frontend

Graffiti consists of 3 separate components
you can use [graffiti.quest](https://graffiti.quest). It serves the latest version of the web frontend.
You can also self-host it by serving the frontend statically:

- **Backend** - the editor used to browse code. The editor might be native, therefore supporting TCP sockets. However, some editors are inside a browser (for example: OpenGrok). Chrome doesn't support TCP Sockets, so Backend should be able to communicate with WebSocket as well. Backend should implement the following functionality:
- Add to graph - Send the current focused symbol.
- Pull - Get a symbol's address from the socket and open it in the editor
- (Optional) Rename - detect rename in the editor and notify the socket.
- **Frontend** - Shows the call graph and allow you to interact with it. Should support:
- Layout the nodes
- Navigating the graph
- Import and export graph
- Undo, Redo
- etc...
- **Server** - A middleware between the backend and the frontend. Support multiple of them in the same time, by multiplexing all the requests.
Need to support: - TCP editor connection - WebSocket editor connection - Websocket frontend connection
```bash
cd frontend/Web
python3 -m http.server 80
```

As a user, you should run the server locally. It is a single python file which depends on `websockets` library.
The frontend is a website which you can serve using `python -m http.server`, or use [graffiti.quest](https://graffiti.quest) directly.
As for the editors, you should install an extension or the equivalance for your editor.
### Server

## Setup
The server can run it two modes:

In order to run Graffiti using the web frontend, you should:
- Single user mode - for local user.
- Multi user mode - for domains or multiple users on the same server.

1. Run the python server
To run the server, first install the single dependency - websockets library:

```python
pip3 install -r server/requirements.txt
python3 server/main.py
pip3 install websockets==10.3
```

2. If you want to self-host, serve the frontend:
Then, just run `python3 server.py`. If you want to use multi-user mode, add the `--multi-user-mode` flag.

```bash
cd frontend/Web
python3 -m http.server 80
```
Note: if you want to run the unpacked version from source, run `python3 -m server.graffiti`.

3. From your browser, Go to [graffiti.quest](https://graffiti.quest) or to the self hosted url. Press connect to connect to the python server. The button will be green if successfully connected.
4. Follow the usage instructions for the specific backend below.
### Backend

Graffiti supports multiple backends, as can be seen [here](#backends). Open the frontend on your browser and press "shift+?". It will show installation and usage instruction for each supported backend.

## Backends

@@ -89,6 +75,40 @@ You can build all the backends using `make`, or build specific backend by runnin

Check [here](docs/platforms) for instructions for using graffiti on each supported platform

## Architecture

![Architecture](docs/images/architecture.svg)
Graffiti was built with the following assumptions:

- You might use more than a single editor for a project.
- You might want to run everything locally.
- It should be easy to use.

Graffiti consists of 3 separate components

- **Backend** - the editor used to browse code. The editor might be native, therefore supporting TCP sockets. However, some editors are inside a browser (for example: OpenGrok). Chrome doesn't support TCP Sockets, so Backend should be able to communicate with WebSocket as well. Backend should implement the following functionality:
- Add to graph - Send the current focused symbol.
- Pull - Get a symbol's address from the socket and open it in the editor
- (Optional) Rename - detect rename in the editor and notify the socket.
- **Frontend** - Shows the call graph and allow you to interact with it. Should support:
- Layout the nodes
- Navigating the graph
- Import and export graph
- Undo, Redo
- etc...
- **Server** - A middleware between the backend and the frontend. Support multiple of them in the same time, by multiplexing all the requests.
Need to support: - TCP editor connection - WebSocket editor connection - Websocket frontend connection

### Multi user support

Multi user support works by requiring each connected frontend/backend to supply a token.
A token represent the namespace of a single user. Messages from the same user will only be delievered to its components.

On the first time you try to connect to a multi-user server with any backend, it will ask you for the token to use. You can retreive it from the web frontend,
by clicking the key button on the top right of the screen. The frontend token is saved to localstorage so you can count it to remain the same.

To make it more user friendly, the backends cache the token under `~/.graffiti/token` on MacOS/Linux, `%USERPROFILE%/.graffiti/token` on Windows.

## Patches

### Mermaid
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ class EnableGraffitiSyncAction : AnAction() {
val (address, port) = getAddressAndPort(e.project!!)

if (SocketHolder.connect(address, port)) {
e.project!!.notify("Connected to graffiti at $address:$port", NotificationType.INFORMATION)
e.project!!.notify("Connected to graffiti at $address:$port (might require authentication if enabled on server)", NotificationType.INFORMATION)
thread(start = true, isDaemon = true) {
threadCode(e.project!!)
}
@@ -49,6 +49,27 @@ class EnableGraffitiSyncAction : AnAction() {
if (rawData.isEmpty())
break
val data = JsonParser.parseString(rawData).asJsonObject

if (data.has("type") && data["type"].asString == "auth_req_v1") {
logger.info("Received auth request")
ApplicationManager.getApplication().invokeLater {
val token = getTokenOrElse {
val userToken = Messages.showInputDialog(
project, "Enter the UUID token from graffiti website",
"Graffiti Authentication", Messages.getQuestionIcon(), "", null
)
if (userToken.isNullOrEmpty()) null else userToken
}
if (token != null) {
SocketHolder.sendJson(project, mapOf("type" to "auth_resp_v1", "token" to token))
} else {
SocketHolder.socket?.close()
SocketHolder.socket = null
}
}
continue
}

if (data.has("project")) {
if (!data["project"].asString.startsWith("Clion:")) {
continue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.yoavst.graffiti.intellij

import com.intellij.openapi.diagnostic.Logger
import java.io.File
import java.util.*

private val logger = Logger.getInstance(EnableGraffitiSyncAction::class.java)

private fun getTokenBaseDir() = File(System.getProperty("user.home"), ".graffiti")

private fun getTokenPath(): File {
val baseDir = getTokenBaseDir()
baseDir.mkdirs()
return File(baseDir, "token")
}
private fun validateToken(token: String): Boolean = try {
UUID.fromString(token).version() == 4
} catch (ignored: IllegalArgumentException){
false
}

private fun getTokenFromFile(): String? {
val tokenFile = getTokenPath()
if (!tokenFile.exists()) return null
val token = tokenFile.readText().trim()
return when {
token.isEmpty() -> {
logger.info("token file is empty!")
null
}
!validateToken(token) -> {
logger.info("Token is not valid uuid v4: $token")
null
}
else -> {
token
}
}
}

fun saveTokenToFile(token: String) {
getTokenPath().writeText(token)
}

fun getTokenOrElse(askForToken: () -> String?): String? {
val fileToken = getTokenFromFile()
if (fileToken != null) return fileToken

val inputToken = askForToken()
return when {
inputToken == null -> {
logger.info("Authentication canceled")
null
}
!validateToken(inputToken) -> {
logger.info("Token is not valid uuid v4: $inputToken")
null
}
else -> {
saveTokenToFile(inputToken)
inputToken
}
}
}
1 change: 1 addition & 0 deletions backends/ida/authentication.py
23 changes: 23 additions & 0 deletions backends/ida/graffiti.py
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@
import ida_funcs
import ida_segment

from authentication import get_token_or_else


HAVE_WIN32_LIBS = False
if sys.platform == 'win32':
@@ -288,6 +290,24 @@ def sync_read_thread(db_filename):
while True:
length = struct.unpack('>i', readexactly(sock, 4))[0]
data = json.loads(readexactly(sock, length))

if 'type' in data and data['type'] == 'auth_req_v1':
token_storage = []
def ask_for_token():
def ui_ask_for_token():
token = ida_kernwin.ask_str('', 2, 'Enter the UUID token from graffiti website')
if token and token.strip():
token_storage.append(token)

idaapi.execute_sync(ui_ask_for_token, idaapi.MFF_FAST)
return token_storage[0] if token_storage else None

token = get_token_or_else(ask_for_token)
if token is not None:
lengthy_send(sock, to_bytes(json.dumps({'type': 'auth_resp_v1', 'token': token})))
continue
else:
break

if 'project' in data:
if data['project'] != 'IDA: {}'.format(db_filename):
@@ -300,6 +320,9 @@ def on_ui():
return False

ida_kernwin.execute_ui_requests([on_ui])

if sock is not None:
sock.close()
except socket.error:
print("Socket is closed")
sock = None
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ class EnableGraffitiSyncAction : AnAction() {
val (address, port) = getAddressAndPort(e.project!!)

if (SocketHolder.connect(address, port)) {
e.project!!.notify("Connected to graffiti at $address:$port", NotificationType.INFORMATION)
e.project!!.notify("Connected to graffiti at $address:$port (might require authentication if enabled on server)", NotificationType.INFORMATION)
thread(start = true, isDaemon = true) {
threadCode(e.project!!)
}
@@ -49,6 +49,27 @@ class EnableGraffitiSyncAction : AnAction() {
if (rawData.isEmpty())
break
val data = JsonParser.parseString(rawData).asJsonObject

if (data.has("type") && data["type"].asString == "auth_req_v1") {
logger.info("Received auth request")
ApplicationManager.getApplication().invokeLater {
val token = getTokenOrElse {
val userToken = Messages.showInputDialog(
project, "Enter the UUID token from graffiti website",
"Graffiti Authentication", Messages.getQuestionIcon(), "", null
)
if (userToken.isNullOrEmpty()) null else userToken
}
if (token != null) {
SocketHolder.sendJson(project, mapOf("type" to "auth_resp_v1", "token" to token))
} else {
SocketHolder.socket?.close()
SocketHolder.socket = null
}
}
continue
}

if (data.has("project")) {
if (!data["project"].asString.startsWith("Intellij:")) {
continue
Loading

0 comments on commit a1eb190

Please sign in to comment.