Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add plugin permissions and IO API #5231

Merged
merged 37 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bcdbcb8
Add basicest plugin permission parsing
Mm2PL Mar 1, 2024
a3ff7de
Add Plugin::hasFSPermissionFor utility function
Mm2PL Mar 3, 2024
4d1f827
Add wrappers for (almost all) IO functions
Mm2PL Mar 3, 2024
4313d0b
Disallow commas in file paths
Mm2PL Mar 3, 2024
4a01239
Unrelated: Ensure that lua::pop(lua_State*, T*) is always balanced,
Mm2PL Mar 3, 2024
923e3b6
Restrict plugins' io to only a data directory
Mm2PL Mar 4, 2024
6dde7bc
This is documented to be <algorithm> instead
Mm2PL Mar 4, 2024
c83ab5b
Add stubs for io.popen and io.tmpfile to ease porting of existing Lua…
Mm2PL Mar 4, 2024
2c5bbc7
Make open relative to data directory instead of plugin
Mm2PL Mar 4, 2024
b5d4e4e
Document relative file behavior
Mm2PL Mar 4, 2024
b77a93c
rewrite string - how did i write this
Mm2PL Mar 4, 2024
3983fc7
Rename PluginPermission::toHtmlEscaped to toHtml
Mm2PL Mar 4, 2024
a00382d
Reformat PluginPermission.hpp
Mm2PL Mar 4, 2024
891f3da
Actually use isValid
Mm2PL Mar 4, 2024
9e850c6
Document behavior of lua::pop()
Mm2PL Mar 5, 2024
47b3578
Reword error strings and validate the string in LuaFileMode constructor
Mm2PL Mar 5, 2024
0e526c5
Reformat IOWrapper.cpp
Mm2PL Mar 5, 2024
5241b80
Make errors conform better to how Lua does them
Mm2PL Mar 5, 2024
4e9ca37
i fucking hate prettier
Mm2PL Mar 5, 2024
98d9b7f
changelog
Mm2PL Mar 5, 2024
3c877d5
ShutUpOldGcc names an order.
Mm2PL Mar 5, 2024
e0c9be6
control DOES NOT REACH THE end of non-void function
Mm2PL Mar 5, 2024
2655251
ah old compilers what would we do without them
Mm2PL Mar 5, 2024
df7d2c8
Merge remote-tracking branch 'origin/master' into feature/plugin-perm…
Mm2PL Mar 5, 2024
708d273
Document permissions
Mm2PL Mar 7, 2024
5357be2
Document IO api
Mm2PL Mar 7, 2024
11c5bb3
Include header links inside of docs/wip-plugins.md
Mm2PL Mar 7, 2024
d6fa4d4
Unrelated: Make sure c2.Channel functions get a channel instead of any
Mm2PL Mar 7, 2024
c9ea3bc
shut up uglier
Mm2PL Mar 7, 2024
08ef872
Use "enum" instead of "examples"
Mm2PL Mar 7, 2024
e4bbedd
Unrelated: Clarify plugin schema license field.
Mm2PL Mar 7, 2024
115f23e
Add missing #pragma once and reformat includes
Mm2PL Mar 9, 2024
7b927f2
Only run error copy loop if there are errors
Mm2PL Mar 9, 2024
4ed9e77
Change permission example snippets to show the structure of info.json
Mm2PL Mar 9, 2024
555683f
Merge branch 'master' of github.com:Chatterino/chatterino2 into featu…
Mm2PL Mar 9, 2024
de45a05
s/err/perm
Mm2PL Mar 9, 2024
5a5e3ae
Do not load data as code :)
Mm2PL Mar 9, 2024
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
- Minor: Image links now reflect the scale of their image instead of an internal label. (#5201)
- Minor: IPC files are now stored in the Chatterino directory instead of system directories on Windows. (#5226)
- Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209)
- Minor: Add wrappers for Lua `io` library for experimental plugins feature. (#5231)
- Minor: Add permissions to experimental plugins feature. (#5231)
- Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840)
- Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848)
- Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834)
Expand Down
14 changes: 13 additions & 1 deletion docs/plugin-info.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,21 @@
},
"license": {
"type": "string",
"description": "A small description of your license.",
"description": "SPDX identifier for license of this plugin. See https://spdx.org/licenses/",
"examples": ["MIT", "GPL-2.0-or-later"]
},
"permissions": {
"type": "array",
"description": "The permissions the plugin needs to work.",
"items": {
"type": "object",
"properties": {
"type": {
"enum": ["FilesystemRead", "FilesystemWrite"]
}
}
}
},
"$schema": { "type": "string" }
},
"required": ["name", "description", "authors", "version", "license"]
Expand Down
168 changes: 163 additions & 5 deletions docs/wip-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ Each plugin should have its own directory.
Chatterino Plugins dir/
└── plugin_name/
├── init.lua
└── info.json
├── info.json
└── data/
└── This is where your data/configs can be dumped
```

`init.lua` will be the file loaded when the plugin is enabled. You may load other files using [`require` global function](#requiremodname).
Expand All @@ -35,12 +37,54 @@ Example file:
"homepage": "https://github.com/Chatterino/Chatterino2",
"tags": ["test"],
"version": "0.0.0",
"license": "MIT"
"license": "MIT",
"permissions": []
}
```

An example plugin is available at [https://github.com/Mm2PL/Chatterino-test-plugin](https://github.com/Mm2PL/Chatterino-test-plugin)

## Permissions

Plugins can have permissions associated to them. Unless otherwise noted functions don't require permissions.
These are the valid permissions:

### FilesystemRead

Allows the plugin to read from its data directory.

Example:

```json
{
...,
"permissions": [
{
"type": "FilesystemRead"
},
...
]
}
```

### FilesystemWrite

Allows the plugin to write to files and create files in its data directory.

Example:

```json
{
...,
"permissions": [
{
"type": "FilesystemWrite"
},
...
]
}
```

## Plugins with Typescript

If you prefer, you may use [TypescriptToLua](https://typescripttolua.github.io)
Expand All @@ -60,9 +104,10 @@ script](../scripts/make_luals_meta.py).
The following parts of the Lua standard library are loaded:

- `_G` (most globals)
- `table`
- `string`
- `io` - except `stdin`, `stdout`, `stderr`. Some functions require permissions.
- `math`
- `string`
- `table`
- `utf8`

The official manual for them is available [here](https://www.lua.org/manual/5.4/manual.html#6).
Expand Down Expand Up @@ -325,6 +370,117 @@ Returns `true` if the channel can be moderated by the current user.

Returns `true` if the current user is a VIP in the channel.

### Input/Output API

These functions are wrappers for Lua's I/O library. Functions on file pointer
objects (`FILE*`) are not modified or replaced. [You can read the documentation
for them here](https://www.lua.org/manual/5.4/manual.html#pdf-file:close).
Chatterino does _not_ give you stdin and stdout as default input and output
respectively. The following objects are missing from the `io` table exposed by
Chatterino compared to Lua's native library: `stdin`, `stdout`, `stderr`.

#### `close([file])`

Closes a file. If not given, `io.output()` is used instead.

See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.close)

#### `flush()`

Flushes `io.output()`.

See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.flush)

#### `input([file_or_name])`

When called with no arguments this function returns the default input file.
This variant requires no permissions.

When called with a file object, it will set the default input file to the one
given. This one also requires no permissions.

When called with a filename as a string, it will open that file for reading.
Equivalent to: `io.input(io.open(filename))`. This variant requires
the `FilesystemRead` permission and the given file to be within the plugin's
data directory.

See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.input)

#### `lines([filename, ...])`

With no arguments this function is equivalent to `io.input():lines("l")`. See
[Lua documentation for file:flush()](https://www.lua.org/manual/5.4/manual.html#pdf-file:flush).
This variant requires no permissions.

With `filename` given it is most like `io.open(filename):lines(...)`. This
variant requires the `FilesystemRead` permission and the given file to be
within the plugin's data directory.

See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.lines)

#### `open(filename [, mode])`

This functions opens the given file with a mode. It requires `filename` to be
within the plugin's data directory. A call with no mode given is equivalent to
one with `mode="r"`.
Depending on the mode this function has slightly different behavior:

| Mode | Permission | Read? | Write? | Truncate? | Create? |
| ----------- | ----------------- | ----- | ------ | --------- | ------- |
| `r` read | `FilesystemRead` | Yes | No | No | No |
| `w` write | `FilesystemWrite` | No | Yes | Yes | Yes |
| `a` append | `FilesystemWrite` | No | Append | No | Yes |
| `r+` update | `FilesystemWrite` | Yes | Yes | No | No |
| `w+` update | `FilesystemWrite` | Yes | Yes | Yes | Yes |
| `a+` update | `FilesystemWrite` | Yes | Append | No | Yes |

To open a file in binary mode add a `b` at the end of the mode.

See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.open)

#### `output([file_or_name])`

This is identical to [`io.input()`](#inputfile_or_name) but operates on the
default output and opens the file in write mode instead. Requires
`FilesystemWrite` instead of `FilesystemRead`.

See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.output)

#### `popen(exe [, mode])`

This function is unavailable in Chatterino. Calling it results in an error
message to let you know that it's not available, no permissions needed.

See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.popen)

#### `read(...)`

Equivalent to `io.input():read(...)`. See [`io.input()`](#inputfile_or_name)
and [`file:read()`](https://www.lua.org/manual/5.4/manual.html#pdf-file:read).

See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.read)

#### `tmpfile()`

This function is unavailable in Chatterino. Calling it results in an error
message to let you know that it's not available, no permissions needed.

See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.tmpfile)

#### `type(obj)`

This functions allows you to tell if the object is a `file`, a `closed file` or
a different bit of data.

See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.type)

#### `write(...)`

Equivalent to `io.output():write(...)`. See [`io.output()`](#outputfile_or_name)
and [`file:write()`](https://www.lua.org/manual/5.4/manual.html#pdf-file:write).

See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.write)

### Changed globals

#### `load(chunk [, chunkname [, mode [, env]]])`
Expand All @@ -344,7 +500,8 @@ However, the searcher and load configuration is notably different from the defau
- `package.path` is not used, in its place are two searchers,
- when `require()` is used, first a file relative to the currently executing
file will be checked, then a file relative to the plugin directory,
- binary chunks are never loaded
- binary chunks are never loaded,
- files inside of the plugin `data` directory are never loaded

As in normal Lua, dots are converted to the path separators (`'/'` on Linux and Mac, `'\'` on Windows).

Expand All @@ -354,6 +511,7 @@ Example:
require("stuff") -- executes Plugins/name/stuff.lua or $(dirname $CURR_FILE)/stuff.lua
require("dir.name") -- executes Plugins/name/dir/name.lua or $(dirname $CURR_FILE)/dir/name.lua
require("binary") -- tried to load Plugins/name/binary.lua and errors because binary is not a text file
require("data.file") -- tried to load Plugins/name/data/file.lua and errors because that is not allowed
```

#### `print(Args...)`
Expand Down
4 changes: 4 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,12 @@ set(SOURCE_FILES

controllers/plugins/api/ChannelRef.cpp
controllers/plugins/api/ChannelRef.hpp
controllers/plugins/api/IOWrapper.cpp
controllers/plugins/api/IOWrapper.hpp
controllers/plugins/LuaAPI.cpp
controllers/plugins/LuaAPI.hpp
controllers/plugins/PluginPermission.cpp
controllers/plugins/PluginPermission.hpp
controllers/plugins/Plugin.cpp
controllers/plugins/Plugin.hpp
controllers/plugins/PluginController.hpp
Expand Down
8 changes: 8 additions & 0 deletions src/controllers/plugins/LuaAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ int loadfile(lua_State *L, const QString &str)
L, QString("requested module is outside of the plugin directory"));
return 1;
}
auto datadir = QUrl(pl->dataDirectory().canonicalPath() + "/");
if (datadir.isParentOf(str))
{
lua::push(L, QString("requested file is data, not code, see Chatterino "
"documentation"));
return 1;
}

QFileInfo info(str);
if (!info.exists())
{
Expand Down
10 changes: 4 additions & 6 deletions src/controllers/plugins/LuaUtilities.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ StackIdx push(lua_State *L, T inp)

/**
* @brief Converts a Lua object into c++ and removes it from the stack.
* If peek fails, the object is still removed from the stack.
*
* Relies on bool peek(lua_State*, T*, StackIdx) existing.
*/
Expand All @@ -291,14 +292,11 @@ bool pop(lua_State *L, T *out, StackIdx idx = -1)
{
StackGuard guard(L, -1);
auto ok = peek(L, out, idx);
if (ok)
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
if (idx < 0)
{
if (idx < 0)
{
idx = lua_gettop(L) + idx + 1;
}
lua_remove(L, idx);
idx = lua_gettop(L) + idx + 1;
}
lua_remove(L, idx);
return ok;
}

Expand Down
66 changes: 66 additions & 0 deletions src/controllers/plugins/Plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# include <QJsonArray>
# include <QJsonObject>

# include <algorithm>
# include <unordered_map>
# include <unordered_set>

Expand Down Expand Up @@ -111,6 +112,48 @@ PluginMeta::PluginMeta(const QJsonObject &obj)
QString("version is not a string (its type is %1)").arg(type));
this->version = semver::version(0, 0, 0);
}
auto permsObj = obj.value("permissions");
if (!permsObj.isUndefined())
{
if (!permsObj.isArray())
{
QString type = magic_enum::enum_name(permsObj.type()).data();
this->errors.emplace_back(
QString("permissions is not an array (its type is %1)")
.arg(type));
return;
}

auto permsArr = permsObj.toArray();
for (int i = 0; i < permsArr.size(); i++)
{
const auto &t = permsArr.at(i);
if (!t.isObject())
{
QString type = magic_enum::enum_name(t.type()).data();
this->errors.push_back(QString("permissions element #%1 is not "
"an object (its type is %2)")
.arg(i)
.arg(type));
return;
}
auto parsed = PluginPermission(t.toObject());
if (parsed.isValid())
{
// ensure no invalid permissions slip through this
this->permissions.push_back(parsed);
}
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
else
{
for (const auto &err : parsed.errors)
{
this->errors.push_back(
QString("permissions element #%1: %2").arg(i).arg(err));
}
}
}
}

auto tagsObj = obj.value("tags");
if (!tagsObj.isUndefined())
{
Expand Down Expand Up @@ -201,5 +244,28 @@ void Plugin::removeTimeout(QTimer *timer)
}
}

bool Plugin::hasFSPermissionFor(bool write, const QString &path)
{
auto canon = QUrl(this->dataDirectory().absolutePath() + "/");
if (!canon.isParentOf(path))
{
return false;
}

using PType = PluginPermission::Type;
auto typ = write ? PType::FilesystemWrite : PType::FilesystemRead;

// XXX: Older compilers don't have support for std::ranges
// NOLINTNEXTLINE(readability-use-anyofallof)
for (const auto &p : this->meta.permissions)
{
if (p.type == typ)
{
return true;
}
}
return false;
}

} // namespace chatterino
#endif
Loading
Loading