Compare commits
145 Commits
v2.14.0
...
wip/ci-tes
Author | SHA1 | Date | |
---|---|---|---|
915901975b | |||
867d831e9a | |||
a96b5cc8f6 | |||
91439f04c0 | |||
c144d0468b | |||
482efae89a | |||
cbb0927a7a | |||
25440a07c3 | |||
869a8d7ab3 | |||
c8536ed50c | |||
cfb43bf550 | |||
816769af5b | |||
c5e0b22c55 | |||
c9145a1460 | |||
199c03c8c6 | |||
cdcdeacd63 | |||
28a4726ddc | |||
6b7d110ced | |||
d5b4577315 | |||
55e4f1c42e | |||
08e13a3ac5 | |||
f5926fbd23 | |||
623d93c6f1 | |||
1f608e600b | |||
747a52aae8 | |||
1f5c95d9e9 | |||
09e9d1f749 | |||
333a02d015 | |||
734d888210 | |||
4fc22a978a | |||
7f8b0a19cf | |||
cdfc3b9ea9 | |||
076b2c1c73 | |||
7121bb6e82 | |||
da26097aab | |||
e03fab07ed | |||
0a85d79dff | |||
d3545f37cd | |||
ad20708766 | |||
37118a4d2b | |||
6199635e7f | |||
c64dda4dea | |||
5310f451f2 | |||
65930492ca | |||
04acbdc221 | |||
e2ec2c9ab7 | |||
939ec7a16e | |||
29e78d3851 | |||
c06f6f2565 | |||
e4fd69e3d4 | |||
f0554b27df | |||
65edc9ad9a | |||
a25f238168 | |||
90c91d6c9a | |||
090fd29acf | |||
cc04916137 | |||
964ae72fa8 | |||
87eb728147 | |||
078af20e8b | |||
bd3f3fa5f7 | |||
df818ad7d9 | |||
c7844c775a | |||
4758d3705d | |||
bbbc2aad1b | |||
7a275812c0 | |||
453cb7ca79 | |||
163608d7fd | |||
71eb79fee4 | |||
aec72593f2 | |||
c5a798beec | |||
2f376953f3 | |||
53952feddd | |||
f9adf88eca | |||
82a424fc8a | |||
c2cdf0d2a1 | |||
83daed8706 | |||
5d5838e712 | |||
082f2f8ceb | |||
7b950eb021 | |||
37192a9136 | |||
3871fbaacb | |||
5deb695919 | |||
bcff9a2ad8 | |||
9c44d7baf4 | |||
c361bdca6a | |||
c522ccce7f | |||
58cdff728d | |||
bfd6eea98f | |||
eeada79a64 | |||
202393a77c | |||
d9809f2787 | |||
ea2f298a1a | |||
7d9f3acfc9 | |||
ad5be08a07 | |||
308838da32 | |||
92014628d1 | |||
586f089df6 | |||
a67eafc796 | |||
5382401893 | |||
8bb768ef93 | |||
ed1d5061a4 | |||
468ce821fe | |||
87470f30a9 | |||
ba72cc7b6d | |||
c1091c38b8 | |||
804f959a1d | |||
7abeb10cf1 | |||
a5a727122b | |||
f7713a6a64 | |||
3ebfa83fdd | |||
ed55330153 | |||
a2ff661d40 | |||
706f9bca82 | |||
6432694455 | |||
cf140f3ab0 | |||
c7322f406c | |||
18eae24acf | |||
c092af89a2 | |||
2a8ab8bb7f | |||
8665501c77 | |||
7659caada1 | |||
fd47adf595 | |||
cadc51ede9 | |||
57478b6575 | |||
5c5aacd9da | |||
93cc105a40 | |||
33300630a3 | |||
fd2167d856 | |||
08fb808ea4 | |||
c70c1e1896 | |||
5cd70622aa | |||
5ca767f7f8 | |||
111441302c | |||
ed6f544572 | |||
ee85129a9b | |||
93f926bf12 | |||
da56297c5a | |||
5d8b4719a8 | |||
8a875afad0 | |||
dc483b2342 | |||
28a3d42ad1 | |||
eb942fc274 | |||
27acca0f5b | |||
ececf2f640 | |||
d72249d91f |
17
.github/workflows/flatpak-build.yml
vendored
Normal file
17
.github/workflows/flatpak-build.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
name: Flatpak Build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: bilelmoussaoui/flatpak-github-actions:gnome-40
|
||||
options: --privileged
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v3
|
||||
with:
|
||||
bundle: hexchat.flatpak
|
||||
manifest-path: flatpak/io.github.Hexchat.json
|
40
.github/workflows/msys-build.yml
vendored
Normal file
40
.github/workflows/msys-build.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
name: MSYS2 Build
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
install: >-
|
||||
mingw-w64-x86_64-gcc
|
||||
mingw-w64-x86_64-pkg-config
|
||||
mingw-w64-x86_64-python3-cffi
|
||||
mingw-w64-x86_64-meson
|
||||
mingw-w64-x86_64-gtk2
|
||||
mingw-w64-x86_64-luajit
|
||||
mingw-w64-x86_64-desktop-file-utils
|
||||
|
||||
- name: Configure
|
||||
run: >-
|
||||
meson build
|
||||
-Dtext-frontend=true
|
||||
-Ddbus=disabled
|
||||
-Dwith-upd=false
|
||||
-Dwith-perl=false
|
||||
|
||||
- name: Build
|
||||
run: ninja -C build
|
||||
|
||||
- name: Test
|
||||
run: ninja -C build test
|
||||
|
||||
- name: Install
|
||||
run: ninja -C build install
|
25
.github/workflows/ubuntu-build.yml
vendored
Normal file
25
.github/workflows/ubuntu-build.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: Ubuntu Build
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y meson libcanberra-dev libdbus-glib-1-dev libglib2.0-dev libgtk2.0-dev libluajit-5.1-dev libpci-dev libperl-dev libproxy-dev libssl-dev python3-dev python3-cffi mono-devel desktop-file-utils
|
||||
|
||||
- name: Configure
|
||||
run: meson build -Dtext=true -Dtheme-manager=true -Dauto_features=enabled
|
||||
|
||||
- name: Build
|
||||
run: ninja -C build
|
||||
|
||||
- name: Test
|
||||
run: ninja -C build test
|
||||
|
||||
- name: Install
|
||||
run: sudo ninja -C build install
|
105
.github/workflows/windows-build.yml
vendored
Normal file
105
.github/workflows/windows-build.yml
vendored
Normal file
@ -0,0 +1,105 @@
|
||||
name: Windows Build
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build-gtk:
|
||||
runs-on: windows-2019
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [x64, win32]
|
||||
arch: [x64, x86]
|
||||
exclude:
|
||||
- platform: x64
|
||||
arch: x86
|
||||
- platform: win32
|
||||
arch: x64
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
repository: wingtk/gvsbuild
|
||||
ref: 9b10978a8c5aa539f4280feeaa69bc5cc8bf9fbf
|
||||
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: C:\gtk-build
|
||||
key: 9b10978a8c5aa539f4280feeaa69bc5cc8bf9fbf
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
C:/hostedtoolcache/windows/Python/3.6.8/${{ matrix.arch }}/python.exe .\build.py build --python-dir="C:/hostedtoolcache/windows/Python/3.6.8/${{ matrix.arch }}" -p ${{ matrix.arch }} --vs-ver=16 gtk lgi openssl
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Build Files ${{ matrix.arch }}
|
||||
path: C:\gtk-build\gtk\${{ matrix.platform }}\release
|
||||
|
||||
build:
|
||||
runs-on: windows-2019
|
||||
needs: build-gtk
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [x64, win32]
|
||||
arch: [x64, x86]
|
||||
exclude:
|
||||
- platform: x64
|
||||
arch: x86
|
||||
- platform: win32
|
||||
arch: x64
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
New-Item -Name "deps" -ItemType "Directory"
|
||||
|
||||
Invoke-WebRequest http://files.jrsoftware.org/is/5/innosetup-5.5.9-unicode.exe -OutFile deps\innosetup-unicode.exe
|
||||
& deps\innosetup-unicode.exe /VERYSILENT | Out-Null
|
||||
|
||||
Invoke-WebRequest https://dl.hexchat.net/misc/idpsetup-1.5.1.exe -OutFile deps\idpsetup.exe
|
||||
& deps\idpsetup.exe /VERYSILENT
|
||||
|
||||
Invoke-WebRequest https://dl.hexchat.net/gtk/gtk-${{ matrix.platform }}-2018-08-29.7z -OutFile deps\gtk-${{ matrix.arch }}.7z
|
||||
& 7z.exe x deps\gtk-${{ matrix.arch }}.7z -oC:\gtk-build\gtk
|
||||
|
||||
Invoke-WebRequest https://dl.hexchat.net/gtk-win32/gendef-20111031.7z -OutFile deps\gendef.7z
|
||||
& 7z.exe x deps\gendef.7z -oC:\gtk-build
|
||||
|
||||
Invoke-WebRequest https://dl.hexchat.net/gtk-win32/WinSparkle-20151011.7z -OutFile deps\WinSparkle.7z
|
||||
& 7z.exe x deps\WinSparkle.7z -oC:\gtk-build\WinSparkle
|
||||
|
||||
Invoke-WebRequest https://dl.hexchat.net/misc/perl/perl-5.20.0-${{ matrix.arch }}.7z -OutFile deps\perl-${{ matrix.arch }}.7z
|
||||
& 7z.exe x deps\perl-${{ matrix.arch }}.7z -oC:\gtk-build\perl-5.20\${{ matrix.platform }}
|
||||
|
||||
New-Item -Path "c:\gtk-build" -Name "python-2.7" -ItemType "Directory"
|
||||
New-Item -Path "c:\gtk-build" -Name "python-3.6" -ItemType "Directory"
|
||||
New-Item -Path "c:\gtk-build\python-2.7" -Name "${{ matrix.platform }}" -ItemType "SymbolicLink" -Value "C:/hostedtoolcache/windows/Python/2.7.18/${{ matrix.arch }}"
|
||||
New-Item -Path "c:\gtk-build\python-3.6" -Name "${{ matrix.platform }}" -ItemType "SymbolicLink" -Value "C:/hostedtoolcache/windows/Python/3.6.8/${{ matrix.arch }}"
|
||||
|
||||
C:/hostedtoolcache/windows/Python/3.6.8/${{ matrix.arch }}/python.exe -m pip install cffi
|
||||
C:/hostedtoolcache/windows/Python/2.7.18/${{ matrix.arch }}/python.exe -m pip install -qq cffi
|
||||
shell: powershell
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\VsDevCmd.bat"
|
||||
msbuild win32\hexchat.sln /m /verbosity:minimal /p:Configuration=Release /p:Platform=${{ matrix.platform }}
|
||||
shell: cmd
|
||||
|
||||
- name: Preparing Artifacts
|
||||
run: |
|
||||
move ..\hexchat-build\${{ matrix.platform }}\HexChat*.exe .\
|
||||
move ..\hexchat-build .\
|
||||
shell: cmd
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Installer ${{ matrix.arch }}
|
||||
path: HexChat*.exe
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Build Files ${{ matrix.arch }}
|
||||
path: hexchat-build
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "flatpak/shared-modules"]
|
||||
path = flatpak/shared-modules
|
||||
url = https://github.com/flathub/shared-modules.git
|
20
.travis.yml
20
.travis.yml
@ -1,20 +0,0 @@
|
||||
sudo: required
|
||||
services: docker
|
||||
before_install:
|
||||
- docker pull ubuntu:16.04
|
||||
- docker run --privileged --cidfile=/tmp/cid ubuntu:16.04 /bin/sh -c 'apt-get update && apt-get install -y meson/xenial-backports libcanberra-dev libdbus-glib-1-dev libglib2.0-dev libgtk2.0-dev libluajit-5.1-dev libnotify-dev libpci-dev libperl-dev libproxy-dev libssl-dev python3-dev mono-devel desktop-file-utils'
|
||||
- docker commit `cat /tmp/cid` hexchat/ubuntu-ci
|
||||
- rm -f /tmp/cid
|
||||
install:
|
||||
- docker run -d --privileged --cidfile=/tmp/cid --volume=${PWD}:/opt/hexchat hexchat/ubuntu-ci /bin/systemd --system
|
||||
script:
|
||||
- docker exec `cat /tmp/cid` /bin/sh -c 'meson /opt/hexchat /opt/hexchat-build -Dwith-text=true -Dwith-theme-manager=true && ninja -C /opt/hexchat-build install'
|
||||
after_script:
|
||||
- docker kill `cat /tmp/cid`
|
||||
notifications:
|
||||
irc:
|
||||
channels: "chat.freenode.net#hexchat-devel"
|
||||
template: "Build %{build_url} (%{commit} in %{branch}) by %{author}: %{message}"
|
||||
on_success: change
|
||||
matrix:
|
||||
fast_finish: true
|
@ -1,11 +1,11 @@
|
||||
if get_option('with-plugin')
|
||||
if get_option('plugin')
|
||||
subdir('pkgconfig')
|
||||
endif
|
||||
|
||||
if get_option('with-gtk')
|
||||
if get_option('gtk-frontend')
|
||||
subdir('icons')
|
||||
subdir('misc')
|
||||
subdir('man')
|
||||
elif get_option('with-theme-manager')
|
||||
elif get_option('theme-manager')
|
||||
subdir('misc')
|
||||
endif
|
||||
|
11
data/misc/io.github.Hexchat.Plugin.metainfo.xml.in
Normal file
11
data/misc/io.github.Hexchat.Plugin.metainfo.xml.in
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="addon">
|
||||
<id>io.github.Hexchat.Plugin.@NAME@</id>
|
||||
<extends>io.github.Hexchat.desktop</extends>
|
||||
<name>@NAME@ Plugin</name>
|
||||
<summary>@SUMMARY@</summary>
|
||||
<url type="homepage">https://hexchat.github.io/</url>
|
||||
<project_license>@LICENSE@</project_license>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<update_contact>tingping_AT_fedoraproject.org</update_contact>
|
||||
</component>
|
@ -13,7 +13,6 @@
|
||||
</description>
|
||||
<url type="homepage">http://hexchat.github.io</url>
|
||||
<url type="bugtracker">https://github.com/hexchat/hexchat</url>
|
||||
<url type="translate">https://www.transifex.com/hexchat/hexchat</url>
|
||||
<url type="donation">https://goo.gl/jESZvU</url>
|
||||
<url type="help">https://hexchat.readthedocs.io/en/latest/</url>
|
||||
<screenshots>
|
||||
@ -27,6 +26,38 @@
|
||||
<id>hexchat.desktop</id>
|
||||
</provides>
|
||||
<releases>
|
||||
<release date="2019-12-20" version="2.14.3">
|
||||
<description>
|
||||
<p>This is a bug-fix release:</p>
|
||||
<ul>
|
||||
<li>Fix various incorrect parsing of IRC messages relating to trailing parameters</li>
|
||||
<li>Fix SASL negotiation combined with multi-line cap</li>
|
||||
<li>Fix input box theming with Yaru theme</li>
|
||||
<li>python: Work around Python 3.7 regression causing crash on unload</li>
|
||||
<li>sysinfo: Add support for /etc/os-release</li>
|
||||
<li>sysinfo: Ignore irrelevant mounts when calculating storage size</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-08-29" version="2.14.2">
|
||||
<description>
|
||||
<p>This is a minor release:</p>
|
||||
<ul>
|
||||
<li>Remove shift+click to close tab binding</li>
|
||||
<li>Always unminimize when opening from tray</li>
|
||||
<li>Fix some translations containing invalid text events</li>
|
||||
<li>Fix sending server passwords starting with ":"</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-03-13" version="2.14.1">
|
||||
<description>
|
||||
<p>This is a very minor bug-fix release:</p>
|
||||
<ul>
|
||||
<li>Fix performance regression</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release date="2018-03-10" version="2.14.0">
|
||||
<description>
|
||||
<p>This is largely a bug fix release though it has some large behind the scenes changes:</p>
|
||||
@ -83,5 +114,8 @@
|
||||
<kudo>HiDpiIcon</kudo>
|
||||
<kudo>Notifications</kudo>
|
||||
</kudos>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="social-chat">intense</content_attribute>
|
||||
</content_rating>
|
||||
<update_contact>tingping_at_fedoraproject.org</update_contact>
|
||||
</component>
|
||||
|
@ -1,24 +1,27 @@
|
||||
appdir = join_paths(get_option('datadir'), 'applications')
|
||||
metainfodir = join_paths(get_option('datadir'), 'metainfo')
|
||||
desktop_utils = find_program('desktop-file-validate', required: false)
|
||||
|
||||
if get_option('with-gtk')
|
||||
hexchat_appdata = i18n.merge_file(
|
||||
input: 'io.github.Hexchat.appdata.xml.in',
|
||||
output: 'io.github.Hexchat.appdata.xml',
|
||||
po_dir: '../../po',
|
||||
install: true,
|
||||
install_dir: join_paths(get_option('datadir'), 'metainfo')
|
||||
)
|
||||
|
||||
appstream_util = find_program('appstream-util', required: false)
|
||||
if appstream_util.found()
|
||||
test('Validate io.github.Hexchat.appdata.xml', appstream_util,
|
||||
args: ['validate-relax', hexchat_appdata]
|
||||
if get_option('gtk-frontend')
|
||||
if get_option('install-appdata')
|
||||
hexchat_appdata = i18n.merge_file(
|
||||
input: 'io.github.Hexchat.appdata.xml.in',
|
||||
output: 'io.github.Hexchat.appdata.xml',
|
||||
po_dir: '../../po',
|
||||
install: true,
|
||||
install_dir: metainfodir
|
||||
)
|
||||
|
||||
appstream_util = find_program('appstream-util', required: false)
|
||||
if appstream_util.found()
|
||||
test('Validate io.github.Hexchat.appdata.xml', appstream_util,
|
||||
args: ['validate-relax', hexchat_appdata]
|
||||
)
|
||||
endif
|
||||
endif
|
||||
|
||||
desktop_conf = configuration_data()
|
||||
if get_option('with-dbus')
|
||||
if dbus_glib_dep.found()
|
||||
desktop_conf.set('exec_command', 'hexchat --existing %U')
|
||||
else
|
||||
desktop_conf.set('exec_command', 'hexchat %U')
|
||||
@ -46,7 +49,7 @@ if get_option('with-gtk')
|
||||
endif
|
||||
endif
|
||||
|
||||
if get_option('with-theme-manager')
|
||||
if get_option('theme-manager')
|
||||
htm_desktop = i18n.merge_file(
|
||||
input: 'io.github.Hexchat.ThemeManager.desktop.in',
|
||||
output: 'io.github.Hexchat.ThemeManager.desktop',
|
||||
@ -66,3 +69,59 @@ if get_option('with-theme-manager')
|
||||
install_dir: join_paths(get_option('datadir'), 'mime/packages')
|
||||
)
|
||||
endif
|
||||
|
||||
if get_option('plugin')
|
||||
plugin_metainfo = []
|
||||
|
||||
# FIXME: These should all get translated somewhere
|
||||
if get_option('with-checksum')
|
||||
plugin_metainfo += [
|
||||
['Checksum', 'Calculates a checksum for all sent and recieved DCC files', 'MIT']
|
||||
]
|
||||
endif
|
||||
|
||||
if get_option('with-fishlim')
|
||||
plugin_metainfo += [
|
||||
['Fishlim', 'Allows setting a key for encrypted conversations', 'MIT AND GPL-2.0+']
|
||||
]
|
||||
endif
|
||||
|
||||
if get_option('with-lua') != 'false'
|
||||
plugin_metainfo += [
|
||||
['Lua', 'Provides a scripting interface in Lua', 'MIT']
|
||||
]
|
||||
endif
|
||||
|
||||
if get_option('with-perl') != 'false'
|
||||
plugin_metainfo += [
|
||||
['Perl', 'Provides a scripting interface in Perl', 'GPL-2.0+']
|
||||
]
|
||||
endif
|
||||
|
||||
if get_option('with-python') != 'false'
|
||||
plugin_metainfo += [
|
||||
['Python', 'Provides a scripting interface in Python', 'GPL-2.0+']
|
||||
]
|
||||
endif
|
||||
|
||||
if get_option('with-sysinfo')
|
||||
plugin_metainfo += [
|
||||
['Sysinfo', 'Adds command to display system information', 'GPL-2.0+']
|
||||
]
|
||||
endif
|
||||
|
||||
foreach metainfo : plugin_metainfo
|
||||
name = metainfo[0]
|
||||
conf = configuration_data()
|
||||
conf.set('NAME', name)
|
||||
conf.set('SUMMARY', metainfo[1])
|
||||
conf.set('LICENSE', metainfo[2])
|
||||
|
||||
configure_file(
|
||||
input: 'io.github.Hexchat.Plugin.metainfo.xml.in',
|
||||
output: 'io.github.Hexchat.Plugin.@0@.metainfo.xml'.format(name),
|
||||
configuration: conf,
|
||||
install_dir: get_option('install-plugin-metainfo') ? metainfodir : '',
|
||||
)
|
||||
endforeach
|
||||
endif
|
||||
|
@ -2,8 +2,8 @@ pkg_conf = configuration_data()
|
||||
prefix = get_option('prefix')
|
||||
pkg_conf.set('prefix', prefix)
|
||||
pkg_conf.set('VERSION', meson.project_version())
|
||||
pkg_conf.set('hexchatlibdir', join_paths(prefix, plugindir))
|
||||
pkg_conf.set('includedir', join_paths(prefix, get_option('includedir')))
|
||||
pkg_conf.set('hexchatlibdir', join_paths('${prefix}', plugindir))
|
||||
pkg_conf.set('includedir', join_paths('${prefix}', get_option('includedir')))
|
||||
|
||||
configure_file(
|
||||
input: 'hexchat-plugin.pc.in',
|
||||
|
25
flatpak/Load-plugins-from-Flatpak-extensions.patch
Normal file
25
flatpak/Load-plugins-from-Flatpak-extensions.patch
Normal file
@ -0,0 +1,25 @@
|
||||
From 918503d57c6740d20be68a6717158673a2a8b25f Mon Sep 17 00:00:00 2001
|
||||
From: Patrick Griffis <tingping@tingping.se>
|
||||
Date: Sat, 17 Mar 2018 05:57:49 -0400
|
||||
Subject: [PATCH] Support loading Flatpak extensions
|
||||
|
||||
---
|
||||
src/common/plugin.c | 2 ++
|
||||
1 file changed, 2 insertions(+)
|
||||
|
||||
diff --git a/src/common/plugin.c b/src/common/plugin.c
|
||||
index 3ad3c558..6addf962 100644
|
||||
--- a/src/common/plugin.c
|
||||
+++ b/src/common/plugin.c
|
||||
@@ -450,6 +450,8 @@ plugin_auto_load (session *sess)
|
||||
lib_dir = plugin_get_libdir ();
|
||||
sub_dir = g_build_filename (get_xdir (), "addons", NULL);
|
||||
|
||||
+ for_files ("/app/extensions/lib/hexchat/plugins", "*.so", plugin_auto_load_cb);
|
||||
+
|
||||
#ifdef WIN32
|
||||
/* a long list of bundled plugins that should be loaded automatically,
|
||||
* user plugins should go to <config>, leave Program Files alone! */
|
||||
--
|
||||
2.14.3
|
||||
|
79
flatpak/io.github.Hexchat.json
Normal file
79
flatpak/io.github.Hexchat.json
Normal file
@ -0,0 +1,79 @@
|
||||
{
|
||||
"app-id": "io.github.Hexchat",
|
||||
"branch": "stable",
|
||||
"runtime": "org.gnome.Platform",
|
||||
"runtime-version": "40",
|
||||
"sdk": "org.gnome.Sdk",
|
||||
"command": "hexchat",
|
||||
"rename-icon": "hexchat",
|
||||
"finish-args": [
|
||||
"--share=ipc",
|
||||
"--socket=x11",
|
||||
"--share=network",
|
||||
"--socket=pulseaudio",
|
||||
"--filesystem=xdg-download",
|
||||
|
||||
"--talk-name=org.freedesktop.Notifications",
|
||||
|
||||
"--talk-name=org.mpris.MediaPlayer2.*"
|
||||
],
|
||||
"add-extensions": {
|
||||
"io.github.Hexchat.Plugin": {
|
||||
"version": "20.08",
|
||||
"directory": "extensions",
|
||||
"add-ld-path": "lib",
|
||||
"merge-dirs": "lib/hexchat/plugins",
|
||||
"subdirectories": true,
|
||||
"no-autodownload": true,
|
||||
"autodelete": true
|
||||
}
|
||||
},
|
||||
"modules": [
|
||||
"shared-modules/gtk2/gtk2.json",
|
||||
"shared-modules/gtk2/gtk2-common-themes.json",
|
||||
"shared-modules/dbus-glib/dbus-glib-0.110.json",
|
||||
"shared-modules/lua5.3/lua-5.3.5.json",
|
||||
"shared-modules/libcanberra/libcanberra.json",
|
||||
"python3-cffi.json",
|
||||
{
|
||||
"name": "lgi",
|
||||
"buildsystem": "meson",
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/pavouk/lgi.git",
|
||||
"commit": "95418635aa8151a516d43166227ea2b9d4c4403f"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hexchat",
|
||||
"buildsystem": "meson",
|
||||
"config-opts": [
|
||||
"--buildtype=release",
|
||||
"-Ddbus-service-use-appid=true",
|
||||
"-Dwith-perl=false",
|
||||
"-Dwith-lua=lua"
|
||||
],
|
||||
"build-options": {
|
||||
"cflags": "-Wno-error=missing-include-dirs"
|
||||
},
|
||||
"cleanup": [
|
||||
"/share/man"
|
||||
],
|
||||
"post-install": [
|
||||
"install -d /app/extensions"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "dir",
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"type": "patch",
|
||||
"path": "Load-plugins-from-Flatpak-extensions.patch"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
19
flatpak/python3-cffi.json
Normal file
19
flatpak/python3-cffi.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "python3-cffi",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"cffi\" --no-build-isolation"
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/0f/86/e19659527668d70be91d0369aeaa055b4eb396b0f387a4f92293a20035bd/pycparser-2.20.tar.gz",
|
||||
"sha256": "2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/a8/20/025f59f929bbcaa579704f443a438135918484fffaacfaddba776b374563/cffi-1.14.5.tar.gz",
|
||||
"sha256": "fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"
|
||||
}
|
||||
]
|
||||
}
|
1
flatpak/shared-modules
Submodule
1
flatpak/shared-modules
Submodule
Submodule flatpak/shared-modules added at 45cc381cdb
69
meson.build
69
meson.build
@ -1,6 +1,6 @@
|
||||
project('hexchat', 'c',
|
||||
version: '2.14.0',
|
||||
meson_version: '>= 0.38.0',
|
||||
version: '2.14.3',
|
||||
meson_version: '>= 0.47.0',
|
||||
default_options: [
|
||||
'c_std=gnu89',
|
||||
'buildtype=debugoptimized',
|
||||
@ -15,12 +15,17 @@ cc = meson.get_compiler('c')
|
||||
|
||||
libgio_dep = dependency('gio-2.0', version: '>= 2.34.0')
|
||||
libgmodule_dep = dependency('gmodule-2.0')
|
||||
|
||||
libcanberra_dep = dependency('libcanberra', version: '>= 0.22',
|
||||
required: get_option('libcanberra'))
|
||||
dbus_glib_dep = dependency('dbus-glib-1', required: get_option('dbus'))
|
||||
|
||||
global_deps = []
|
||||
if cc.get_id() == 'msvc'
|
||||
libssl_dep = cc.find_library('libeay32')
|
||||
else
|
||||
libssl_dep = dependency('openssl', version: '>= 0.9.8',
|
||||
required: get_option('with-ssl'))
|
||||
required: get_option('tls'))
|
||||
endif
|
||||
|
||||
config_h = configuration_data()
|
||||
@ -32,11 +37,10 @@ config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'),
|
||||
config_h.set10('ENABLE_NLS', true)
|
||||
|
||||
# Optional features
|
||||
config_h.set('USE_OPENSSL', get_option('with-ssl'))
|
||||
config_h.set('USE_LIBPROXY', get_option('with-libproxy'))
|
||||
config_h.set('USE_LIBCANBERRA', get_option('with-libcanberra'))
|
||||
config_h.set('USE_DBUS', get_option('with-dbus'))
|
||||
config_h.set('USE_PLUGIN', get_option('with-plugin'))
|
||||
config_h.set('USE_OPENSSL', libssl_dep.found())
|
||||
config_h.set('USE_LIBCANBERRA', libcanberra_dep.found())
|
||||
config_h.set('USE_DBUS', dbus_glib_dep.found())
|
||||
config_h.set('USE_PLUGIN', get_option('plugin'))
|
||||
|
||||
config_h.set('G_DISABLE_SINGLE_INCLUDES', true)
|
||||
config_h.set('GTK_DISABLE_DEPRECATED', true)
|
||||
@ -49,6 +53,10 @@ config_h.set('GLIB_VERSION_MIN_REQUIRED', 'GLIB_VERSION_2_34')
|
||||
config_h.set('HAVE_MEMRCHR', cc.has_function('memrchr'))
|
||||
config_h.set('HAVE_STRINGS_H', cc.has_header('strings.h'))
|
||||
|
||||
config_h.set_quoted('HEXCHATLIBDIR',
|
||||
join_paths(get_option('prefix'), get_option('libdir'), 'hexchat/plugins')
|
||||
)
|
||||
|
||||
if libssl_dep.found()
|
||||
config_h.set('HAVE_X509_GET_SIGNATURE_NID',
|
||||
cc.has_function('X509_get_signature_nid', dependencies: libssl_dep)
|
||||
@ -87,8 +95,6 @@ endif
|
||||
|
||||
global_cflags = []
|
||||
test_cflags = [
|
||||
'-pipe',
|
||||
'-fPIE',
|
||||
'-funsigned-char',
|
||||
'-Wno-conversion',
|
||||
'-Wno-pointer-sign',
|
||||
@ -131,20 +137,27 @@ global_ldflags = []
|
||||
test_ldflags = [
|
||||
'-Wl,-z,relro',
|
||||
'-Wl,-z,now',
|
||||
'-Wl,-pie',
|
||||
# mingw
|
||||
'-Wl,--dynamicbase',
|
||||
'-Wl,--nxcompat',
|
||||
]
|
||||
if not (host_machine.system() == 'windows' and get_option('debug'))
|
||||
test_ldflags += '-Wl,--dynamicbase'
|
||||
endif
|
||||
foreach ldflag : test_ldflags
|
||||
if cc.has_argument(ldflag) and cc.links('int main (void) { return 0; }', args: ldflag)
|
||||
if meson.version().version_compare('>= 0.46.0')
|
||||
has_arg = cc.has_link_argument(ldflag)
|
||||
else
|
||||
has_arg = cc.has_argument(ldflag)
|
||||
endif
|
||||
|
||||
if has_arg and cc.links('int main (void) { return 0; }', args: ldflag)
|
||||
global_ldflags += ldflag
|
||||
endif
|
||||
endforeach
|
||||
add_project_link_arguments(global_ldflags, language: 'c')
|
||||
|
||||
subdir('src')
|
||||
if get_option('with-plugin')
|
||||
if get_option('plugin')
|
||||
subdir('plugins')
|
||||
endif
|
||||
if cc.get_id() != 'msvc'
|
||||
@ -152,6 +165,32 @@ if cc.get_id() != 'msvc'
|
||||
subdir('po') # FIXME: build xgettext
|
||||
|
||||
meson.add_install_script('meson_post_install.py',
|
||||
'@0@'.format(get_option('with-theme-manager'))
|
||||
'@0@'.format(get_option('theme-manager'))
|
||||
)
|
||||
endif
|
||||
|
||||
if meson.version().version_compare('>= 0.53.0')
|
||||
summary({
|
||||
'prefix': get_option('prefix'),
|
||||
'bindir': get_option('bindir'),
|
||||
'libdir': get_option('libdir'),
|
||||
'datadir': get_option('datadir'),
|
||||
}, section: 'Directories')
|
||||
|
||||
summary({
|
||||
'TLS (openssl)': libssl_dep.found(),
|
||||
'Plugin Support': get_option('plugin'),
|
||||
'DBus Support': dbus_glib_dep.found(),
|
||||
'libcanberra': libcanberra_dep.found(),
|
||||
}, section: 'Features')
|
||||
|
||||
summary({
|
||||
'Lua': get_option('with-lua'),
|
||||
'Python': get_option('with-python'),
|
||||
'Perl': get_option('with-perl'),
|
||||
'Perl Legacy API': get_option('with-perl-legacy-api'),
|
||||
'FiSH': get_option('with-fishlim'),
|
||||
'Sysinfo': get_option('with-sysinfo'),
|
||||
'DCC Checksum': get_option('with-checksum'),
|
||||
}, section: 'Plugins')
|
||||
endif
|
||||
|
@ -1,33 +1,38 @@
|
||||
option('with-gtk', type: 'boolean',
|
||||
# Applications
|
||||
option('gtk-frontend', type: 'boolean',
|
||||
description: 'Main graphical interface'
|
||||
)
|
||||
option('with-text', type: 'boolean', value: false,
|
||||
option('text-frontend', type: 'boolean', value: false,
|
||||
description: 'Text interface (not generally useful)'
|
||||
)
|
||||
option('with-ssl', type: 'boolean',
|
||||
description: 'Support for TLS connections, requires openssl'
|
||||
)
|
||||
option('with-plugin', type: 'boolean',
|
||||
description: 'Support for loadable plugins'
|
||||
)
|
||||
option('with-dbus', type: 'boolean',
|
||||
description: 'Support used for single-instance and scripting interface, Unix only'
|
||||
)
|
||||
option('with-libproxy', type: 'boolean',
|
||||
description: 'Support for getting proxy information, Unix only'
|
||||
)
|
||||
option('with-libnotify', type: 'boolean',
|
||||
description: 'Support for freedesktop notifications, Unix only'
|
||||
)
|
||||
option('with-libcanberra', type: 'boolean',
|
||||
description: 'Support for sound alerts, Unix only'
|
||||
)
|
||||
option('with-theme-manager', type: 'boolean', value: false,
|
||||
option('theme-manager', type: 'boolean', value: false,
|
||||
description: 'Utility to help manage themes, requires mono/.net'
|
||||
)
|
||||
|
||||
# Features
|
||||
option('tls', type: 'feature', value: 'enabled',
|
||||
description: 'Support for TLS connections, requires openssl'
|
||||
)
|
||||
option('plugin', type: 'boolean',
|
||||
description: 'Support for loadable plugins'
|
||||
)
|
||||
option('dbus', type: 'feature', value: 'auto',
|
||||
description: 'Support used for single-instance and scripting interface, Unix only'
|
||||
)
|
||||
option('libcanberra', type: 'feature', value: 'auto',
|
||||
description: 'Support for sound alerts, Unix only'
|
||||
)
|
||||
|
||||
# Install options
|
||||
option('dbus-service-use-appid', type: 'boolean', value: false,
|
||||
description: 'Rename dbus service to match app-id, required for Flatpak'
|
||||
)
|
||||
option('install-appdata', type: 'boolean',
|
||||
description: 'Install appdata files for app stores'
|
||||
)
|
||||
option('install-plugin-metainfo', type: 'boolean', value: false,
|
||||
description: 'Installs metainfo files for enabled plugins, useful when distros create split packages'
|
||||
)
|
||||
|
||||
# Plugins
|
||||
option('with-checksum', type: 'boolean',
|
||||
@ -42,8 +47,8 @@ option('with-fishlim', type: 'boolean',
|
||||
option('with-lua', type: 'string', value: 'luajit',
|
||||
description: 'Lua scripting plugin, value is pkg-config name to use or "false"'
|
||||
)
|
||||
option('with-perl', type: 'boolean',
|
||||
description: 'Perl scripting plugin'
|
||||
option('with-perl', type: 'string', value: 'perl',
|
||||
description: 'Perl scripting plugin, value is path to perl executable or "false"'
|
||||
)
|
||||
option('with-python', type: 'string', value: 'python3',
|
||||
description: 'Python scripting plugin. value is pkg-config name to use or "false"'
|
||||
@ -57,3 +62,6 @@ option('with-upd', type: 'boolean',
|
||||
option('with-winamp', type: 'boolean',
|
||||
description: 'Winamp plugin, Windows only'
|
||||
)
|
||||
option('with-perl-legacy-api', type: 'boolean', value: false,
|
||||
description: 'Enables the legacy IRC perl module for compatibility with old scripts'
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
|
@ -1,6 +1,7 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2010 Samuel Lidén Borell <samuel@kodafritt.se>
|
||||
Copyright (c) 2019-2020 <bakasura@protonmail.ch>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -27,15 +28,22 @@
|
||||
#define DEPRECATED_IN_MAC_OS_X_VERSION_10_7_AND_LATER
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/blowfish.h>
|
||||
#include <openssl/rand.h>
|
||||
|
||||
#include "keystore.h"
|
||||
#include "fish.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define IB 64
|
||||
static const char fish_base64[64] = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
/* rfc 2812; 512 - CR-LF = 510 */
|
||||
static const int MAX_COMMAND_LENGTH = 510;
|
||||
static const char fish_base64[] = "./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
static const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
static const signed char fish_unbase64[256] = {
|
||||
IB,IB,IB,IB,IB,IB,IB,IB, IB,IB,IB,IB,IB,IB,IB,IB,
|
||||
IB,IB,IB,IB,IB,IB,IB,IB, IB,IB,IB,IB,IB,IB,IB,IB,
|
||||
@ -53,6 +61,12 @@ static const signed char fish_unbase64[256] = {
|
||||
27,28,29,30,31,32,33,34, 35,36,37,IB,IB,IB,IB,IB,
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert Int to 4 Bytes (Big-endian)
|
||||
*
|
||||
* @param int source
|
||||
* @param char* dest
|
||||
*/
|
||||
#define GET_BYTES(dest, source) do { \
|
||||
*((dest)++) = ((source) >> 24) & 0xFF; \
|
||||
*((dest)++) = ((source) >> 16) & 0xFF; \
|
||||
@ -60,135 +74,430 @@ static const signed char fish_unbase64[256] = {
|
||||
*((dest)++) = (source) & 0xFF; \
|
||||
} while (0);
|
||||
|
||||
/**
|
||||
* Convert 4 Bytes to Int (Big-endian)
|
||||
*
|
||||
* @param char* source
|
||||
* @param int dest
|
||||
*/
|
||||
#define GET_LONG(dest, source) do { \
|
||||
dest = ((uint8_t)*((source)++) << 24); \
|
||||
dest |= ((uint8_t)*((source)++) << 16); \
|
||||
dest |= ((uint8_t)*((source)++) << 8); \
|
||||
dest |= (uint8_t)*((source)++); \
|
||||
} while (0);
|
||||
|
||||
char *fish_encrypt(const char *key, size_t keylen, const char *message) {
|
||||
BF_KEY bfkey;
|
||||
size_t messagelen;
|
||||
size_t i;
|
||||
int j;
|
||||
char *encrypted;
|
||||
char *end;
|
||||
unsigned char bit;
|
||||
unsigned char word;
|
||||
unsigned char d;
|
||||
BF_set_key(&bfkey, keylen, (const unsigned char*)key);
|
||||
|
||||
messagelen = strlen(message);
|
||||
if (messagelen == 0) return NULL;
|
||||
encrypted = g_malloc(((messagelen - 1) / 8) * 12 + 12 + 1); /* each 8-byte block becomes 12 bytes */
|
||||
end = encrypted;
|
||||
|
||||
while (*message) {
|
||||
/* Read 8 bytes (a Blowfish block) */
|
||||
BF_LONG binary[2] = { 0, 0 };
|
||||
unsigned char c;
|
||||
for (i = 0; i < 8; i++) {
|
||||
c = message[i];
|
||||
binary[i >> 2] |= c << 8*(3 - (i&3));
|
||||
if (c == '\0') break;
|
||||
|
||||
/**
|
||||
* Encode ECB FiSH Base64
|
||||
*
|
||||
* @param [in] message Bytes to encode
|
||||
* @param [in] message_len Size of bytes to encode
|
||||
* @return Array of char with encoded string
|
||||
*/
|
||||
char *fish_base64_encode(const char *message, size_t message_len) {
|
||||
BF_LONG left = 0, right = 0;
|
||||
int i, j;
|
||||
char *encoded = NULL;
|
||||
char *end = NULL;
|
||||
char *msg = NULL;
|
||||
|
||||
if (message_len == 0)
|
||||
return NULL;
|
||||
|
||||
/* Each 8-byte block becomes 12 bytes (fish base64 format) and add 1 byte for \0 */
|
||||
encoded = g_malloc(((message_len - 1) / 8) * 12 + 12 + 1);
|
||||
end = encoded;
|
||||
|
||||
/* Iterate over each 8-byte block (Blowfish block size) */
|
||||
for (j = 0; j < message_len; j += 8) {
|
||||
msg = (char *) message;
|
||||
|
||||
/* Set left and right longs */
|
||||
GET_LONG(left, msg);
|
||||
GET_LONG(right, msg);
|
||||
|
||||
for (i = 0; i < 6; ++i) {
|
||||
*end++ = fish_base64[right & 0x3fu];
|
||||
right = (right >> 6u);
|
||||
}
|
||||
|
||||
for (i = 0; i < 6; ++i) {
|
||||
*end++ = fish_base64[left & 0x3fu];
|
||||
left = (left >> 6u);
|
||||
}
|
||||
|
||||
/* The previous for'd ensure fill all bytes of encoded, we don't need will with zeros */
|
||||
message += 8;
|
||||
|
||||
/* Encrypt block */
|
||||
BF_encrypt(binary, &bfkey);
|
||||
|
||||
/* Emit FiSH-BASE64 */
|
||||
bit = 0;
|
||||
word = 1;
|
||||
for (j = 0; j < 12; j++) {
|
||||
d = fish_base64[(binary[word] >> bit) & 63];
|
||||
*(end++) = d;
|
||||
bit += 6;
|
||||
if (j == 5) {
|
||||
bit = 0;
|
||||
word = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Stop if a null terminator was found */
|
||||
if (c == '\0') break;
|
||||
}
|
||||
*end = '\0';
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
|
||||
char *fish_decrypt(const char *key, size_t keylen, const char *data) {
|
||||
BF_KEY bfkey;
|
||||
size_t i;
|
||||
char *decrypted;
|
||||
char *end;
|
||||
unsigned char bit;
|
||||
unsigned char word;
|
||||
unsigned char d;
|
||||
BF_set_key(&bfkey, keylen, (const unsigned char*)key);
|
||||
|
||||
decrypted = g_malloc(strlen(data) + 1);
|
||||
end = decrypted;
|
||||
|
||||
while (*data) {
|
||||
/* Convert from FiSH-BASE64 */
|
||||
BF_LONG binary[2] = { 0, 0 };
|
||||
bit = 0;
|
||||
word = 1;
|
||||
for (i = 0; i < 12; i++) {
|
||||
d = fish_unbase64[(const unsigned char)*(data++)];
|
||||
if (d == IB) goto decrypt_end;
|
||||
binary[word] |= (unsigned long)d << bit;
|
||||
bit += 6;
|
||||
if (i == 5) {
|
||||
bit = 0;
|
||||
word = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Decrypt block */
|
||||
BF_decrypt(binary, &bfkey);
|
||||
|
||||
/* Copy to buffer */
|
||||
GET_BYTES(end, binary[0]);
|
||||
GET_BYTES(end, binary[1]);
|
||||
}
|
||||
|
||||
decrypt_end:
|
||||
*end = '\0';
|
||||
return decrypted;
|
||||
return encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a message (see fish_decrypt). The key is searched for in the
|
||||
* key store.
|
||||
* Decode ECB FiSH Base64
|
||||
*
|
||||
* @param [in] message Base64 encoded string
|
||||
* @param [out] final_len Real length of message
|
||||
* @return Array of char with decoded message
|
||||
*/
|
||||
char *fish_encrypt_for_nick(const char *nick, const char *data) {
|
||||
char *key;
|
||||
char *encrypted;
|
||||
char *fish_base64_decode(const char *message, size_t *final_len) {
|
||||
BF_LONG left, right;
|
||||
int i;
|
||||
char *bytes = NULL;
|
||||
char *msg = NULL;
|
||||
char *byt = NULL;
|
||||
size_t message_len;
|
||||
|
||||
/* Look for key */
|
||||
key = keystore_get_key(nick);
|
||||
if (!key) return NULL;
|
||||
|
||||
/* Encrypt */
|
||||
encrypted = fish_encrypt(key, strlen(key), data);
|
||||
|
||||
g_free(key);
|
||||
return encrypted;
|
||||
message_len = strlen(message);
|
||||
|
||||
/* Ensure blocks of 12 bytes each one and valid characters */
|
||||
if (message_len == 0 || message_len % 12 != 0 || strspn(message, fish_base64) != message_len)
|
||||
return NULL;
|
||||
|
||||
/* Each 12 bytes becomes 8-byte block and add 1 byte for \0 */
|
||||
*final_len = ((message_len - 1) / 12) * 8 + 8 + 1;
|
||||
(*final_len)--; /* We support binary data */
|
||||
bytes = (char *) g_malloc0(*final_len);
|
||||
byt = bytes;
|
||||
|
||||
msg = (char *) message;
|
||||
|
||||
while (*msg) {
|
||||
right = 0;
|
||||
left = 0;
|
||||
for (i = 0; i < 6; i++) right |= (uint8_t) fish_unbase64[(int)*msg++] << (i * 6u);
|
||||
for (i = 0; i < 6; i++) left |= (uint8_t) fish_unbase64[(int)*msg++] << (i * 6u);
|
||||
GET_BYTES(byt, left);
|
||||
GET_BYTES(byt, right);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a message (see fish_decrypt). The key is searched for in the
|
||||
* key store.
|
||||
* Encrypt or decrypt data with Blowfish cipher, support binary data.
|
||||
*
|
||||
* Good documentation for EVP:
|
||||
*
|
||||
* - https://wiki.openssl.org/index.php/EVP_Symmetric_Encryption_and_Decryption
|
||||
*
|
||||
* - https://stackoverflow.com/questions/5727646/what-is-the-length-parameter-of-aes-evp-decrypt
|
||||
*
|
||||
* - https://stackoverflow.com/questions/26345175/correct-way-to-free-allocate-the-context-in-the-openssl
|
||||
*
|
||||
* - https://stackoverflow.com/questions/29874150/working-with-evp-and-openssl-coding-in-c
|
||||
*
|
||||
* @param [in] plaintext Bytes to encrypt or decrypt
|
||||
* @param [in] plaintext_len Size of plaintext
|
||||
* @param [in] key Bytes of key
|
||||
* @param [in] keylen Size of key
|
||||
* @param [in] encode 1 or encrypt 0 for decrypt
|
||||
* @param [in] mode EVP_CIPH_ECB_MODE or EVP_CIPH_CBC_MODE
|
||||
* @param [out] ciphertext_len The bytes written
|
||||
* @return Array of char with data encrypted or decrypted
|
||||
*/
|
||||
char *fish_decrypt_from_nick(const char *nick, const char *data) {
|
||||
char *fish_cipher(const char *plaintext, size_t plaintext_len, const char *key, size_t keylen, int encode, int mode, size_t *ciphertext_len) {
|
||||
EVP_CIPHER_CTX *ctx;
|
||||
EVP_CIPHER *cipher = NULL;
|
||||
int bytes_written = 0;
|
||||
unsigned char *ciphertext = NULL;
|
||||
unsigned char *iv_ciphertext = NULL;
|
||||
unsigned char *iv = NULL;
|
||||
size_t block_size = 0;
|
||||
|
||||
*ciphertext_len = 0;
|
||||
|
||||
if (plaintext_len == 0 || keylen == 0 || encode < 0 || encode > 1)
|
||||
return NULL;
|
||||
|
||||
block_size = plaintext_len;
|
||||
|
||||
if (mode == EVP_CIPH_CBC_MODE) {
|
||||
if (encode == 1) {
|
||||
iv = (unsigned char *) g_malloc0(8);
|
||||
RAND_bytes(iv, 8);
|
||||
} else {
|
||||
if (plaintext_len <= 8) /* IV + DATA */
|
||||
return NULL;
|
||||
|
||||
iv = (unsigned char *) plaintext;
|
||||
block_size -= 8;
|
||||
plaintext += 8;
|
||||
plaintext_len -= 8;
|
||||
}
|
||||
|
||||
cipher = (EVP_CIPHER *) EVP_bf_cbc();
|
||||
} else if (mode == EVP_CIPH_ECB_MODE) {
|
||||
cipher = (EVP_CIPHER *) EVP_bf_ecb();
|
||||
}
|
||||
|
||||
/* Zero Padding */
|
||||
if (block_size % 8 != 0) {
|
||||
block_size = block_size + 8 - (block_size % 8);
|
||||
}
|
||||
|
||||
ciphertext = (unsigned char *) g_malloc0(block_size);
|
||||
memcpy(ciphertext, plaintext, plaintext_len);
|
||||
|
||||
/* Create and initialise the context */
|
||||
if (!(ctx = EVP_CIPHER_CTX_new()))
|
||||
return NULL;
|
||||
|
||||
/* Initialise the cipher operation only with mode */
|
||||
if (!EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, encode))
|
||||
return NULL;
|
||||
|
||||
/* Set custom key length */
|
||||
if (!EVP_CIPHER_CTX_set_key_length(ctx, keylen))
|
||||
return NULL;
|
||||
|
||||
/* Finish the initiation the cipher operation */
|
||||
if (1 != EVP_CipherInit_ex(ctx, NULL, NULL, (const unsigned char *) key, iv, encode))
|
||||
return NULL;
|
||||
|
||||
/* We will manage this */
|
||||
EVP_CIPHER_CTX_set_padding(ctx, 0);
|
||||
|
||||
/* Do cipher operation */
|
||||
if (1 != EVP_CipherUpdate(ctx, ciphertext, &bytes_written, ciphertext, block_size))
|
||||
return NULL;
|
||||
|
||||
*ciphertext_len = bytes_written;
|
||||
|
||||
/* Finalise the cipher. Further ciphertext bytes may be written at this stage */
|
||||
if (1 != EVP_CipherFinal_ex(ctx, ciphertext + bytes_written, &bytes_written))
|
||||
return NULL;
|
||||
|
||||
*ciphertext_len += bytes_written;
|
||||
|
||||
/* Clean up */
|
||||
EVP_CIPHER_CTX_free(ctx);
|
||||
|
||||
|
||||
if (mode == EVP_CIPH_CBC_MODE && encode == 1) {
|
||||
/* Join IV + DATA */
|
||||
iv_ciphertext = g_malloc0(8 + *ciphertext_len);
|
||||
|
||||
memcpy(iv_ciphertext, iv, 8);
|
||||
memcpy(&iv_ciphertext[8], ciphertext, *ciphertext_len);
|
||||
*ciphertext_len += 8;
|
||||
|
||||
g_free(ciphertext);
|
||||
g_free(iv);
|
||||
|
||||
return (char *) iv_ciphertext;
|
||||
} else {
|
||||
return (char *) ciphertext;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a fish or standard Base64 encoded string with data encrypted
|
||||
* is binary safe
|
||||
*
|
||||
* @param [in] key Bytes of key
|
||||
* @param [in] keylen Size of key
|
||||
* @param [in] message Bytes to encrypt
|
||||
* @param [in] message_len Size of message
|
||||
* @param [in] mode Chiper mode
|
||||
* @return Array of char with data encrypted
|
||||
*/
|
||||
char *fish_encrypt(const char *key, size_t keylen, const char *message, size_t message_len, enum fish_mode mode) {
|
||||
size_t ciphertext_len = 0;
|
||||
char *ciphertext = NULL;
|
||||
char *b64 = NULL;
|
||||
|
||||
if (keylen == 0 || message_len == 0)
|
||||
return NULL;
|
||||
|
||||
ciphertext = fish_cipher(message, message_len, key, keylen, 1, mode, &ciphertext_len);
|
||||
|
||||
if (ciphertext == NULL || ciphertext_len == 0)
|
||||
return NULL;
|
||||
|
||||
switch (mode) {
|
||||
case FISH_CBC_MODE:
|
||||
b64 = g_base64_encode((const unsigned char *) ciphertext, ciphertext_len);
|
||||
break;
|
||||
|
||||
case FISH_ECB_MODE:
|
||||
b64 = fish_base64_encode((const char *) ciphertext, ciphertext_len);
|
||||
}
|
||||
|
||||
g_free(ciphertext);
|
||||
|
||||
if (b64 == NULL)
|
||||
return NULL;
|
||||
|
||||
return b64;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of bytes with data decrypted
|
||||
* is binary safe
|
||||
*
|
||||
* @param [in] key Bytes of key
|
||||
* @param [in] keylen Size of key
|
||||
* @param [in] data Fish or standard Base64 encoded string
|
||||
* @param [in] mode Chiper mode
|
||||
* @param [out] final_len Length of returned array
|
||||
* @return Array of char with data decrypted
|
||||
*/
|
||||
char *fish_decrypt(const char *key, size_t keylen, const char *data, enum fish_mode mode, size_t *final_len) {
|
||||
size_t ciphertext_len = 0;
|
||||
char *ciphertext = NULL;
|
||||
char *plaintext = NULL;
|
||||
|
||||
*final_len = 0;
|
||||
|
||||
if (keylen == 0 || strlen(data) == 0)
|
||||
return NULL;
|
||||
|
||||
switch (mode) {
|
||||
case FISH_CBC_MODE:
|
||||
if (strspn(data, base64_chars) != strlen(data))
|
||||
return NULL;
|
||||
ciphertext = (char *) g_base64_decode(data, &ciphertext_len);
|
||||
break;
|
||||
|
||||
case FISH_ECB_MODE:
|
||||
ciphertext = fish_base64_decode(data, &ciphertext_len);
|
||||
}
|
||||
|
||||
if (ciphertext == NULL || ciphertext_len == 0)
|
||||
return NULL;
|
||||
|
||||
plaintext = fish_cipher(ciphertext, ciphertext_len, key, keylen, 0, mode, final_len);
|
||||
g_free(ciphertext);
|
||||
|
||||
if (*final_len == 0)
|
||||
return NULL;
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to fish_decrypt, but pad with zeros any after the first zero in the decrypted data
|
||||
*
|
||||
* @param [in] key Bytes of key
|
||||
* @param [in] keylen Size of key
|
||||
* @param [in] data Fish or standard Base64 encoded string
|
||||
* @param [in] mode Chiper mode
|
||||
* @return Array of char with data decrypted
|
||||
*/
|
||||
char *fish_decrypt_str(const char *key, size_t keylen, const char *data, enum fish_mode mode) {
|
||||
char *decrypted = NULL;
|
||||
char *plaintext_str = NULL;
|
||||
size_t decrypted_len = 0;
|
||||
|
||||
decrypted = fish_decrypt(key, strlen(key), data, mode, &decrypted_len);
|
||||
|
||||
if (decrypted == NULL || decrypted_len == 0)
|
||||
return NULL;
|
||||
|
||||
plaintext_str = g_strndup(decrypted, decrypted_len);
|
||||
g_free(decrypted);
|
||||
|
||||
return plaintext_str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a nick have a key
|
||||
*
|
||||
* @param [in] nick Nickname
|
||||
* @return TRUE if have a key or FALSE if not
|
||||
*/
|
||||
gboolean fish_nick_has_key(const char *nick) {
|
||||
gboolean has_key = FALSE;
|
||||
char *key;
|
||||
enum fish_mode mode;
|
||||
|
||||
key = keystore_get_key(nick, &mode);
|
||||
if (key) {
|
||||
has_key = TRUE;
|
||||
g_free(key);
|
||||
};
|
||||
|
||||
return has_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a message (see fish_encrypt). The key is searched for in the key store
|
||||
*
|
||||
* @param [in] nick Nickname
|
||||
* @param [in] data Plaintext to encrypt
|
||||
* @param [out] omode Mode of encryption
|
||||
* @param [in] command_len Length of command to use without the message part
|
||||
* @return A list of encoded strings with the message encrypted or NULL if any error occurred
|
||||
*/
|
||||
GSList *fish_encrypt_for_nick(const char *nick, const char *data, enum fish_mode *omode, size_t command_len) {
|
||||
char *key;
|
||||
GSList *encrypted_list = NULL;
|
||||
char *encrypted = NULL;
|
||||
enum fish_mode mode;
|
||||
int max_len, max_chunks_len, chunks_len;
|
||||
|
||||
/* Look for key */
|
||||
key = keystore_get_key(nick, &mode);
|
||||
if (!key) return NULL;
|
||||
|
||||
*omode = mode;
|
||||
|
||||
/* Calculate max length of each line */
|
||||
max_len = MAX_COMMAND_LENGTH - command_len;
|
||||
/* Add '*' */
|
||||
if (mode == FISH_CBC_MODE) max_len--;
|
||||
|
||||
max_chunks_len = max_text_command_len(max_len, mode);
|
||||
|
||||
const char *data_chunk = data;
|
||||
|
||||
while(foreach_utf8_data_chunks(data_chunk, max_chunks_len, &chunks_len)) {
|
||||
encrypted = fish_encrypt(key, strlen(key), data_chunk, chunks_len, mode);
|
||||
|
||||
if (mode == FISH_CBC_MODE) {
|
||||
/* Add '*' for CBC */
|
||||
encrypted_list = g_slist_append(encrypted_list, g_strdup_printf("*%s", encrypted));
|
||||
g_free(encrypted);
|
||||
} else {
|
||||
encrypted_list = g_slist_append(encrypted_list, encrypted);
|
||||
}
|
||||
|
||||
/* Next chunk */
|
||||
data_chunk += chunks_len;
|
||||
}
|
||||
|
||||
return encrypted_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a message (see fish_decrypt). The key is searched for in the key store
|
||||
*
|
||||
* @param [in] nick Nickname
|
||||
* @param [in] data Plaintext to encrypt
|
||||
* @param [out] omode Mode of encryption
|
||||
* @return Plaintext message or NULL if any error occurred
|
||||
*/
|
||||
char *fish_decrypt_from_nick(const char *nick, const char *data, enum fish_mode *omode) {
|
||||
char *key;
|
||||
char *decrypted;
|
||||
enum fish_mode mode;
|
||||
|
||||
/* Look for key */
|
||||
key = keystore_get_key(nick);
|
||||
key = keystore_get_key(nick, &mode);
|
||||
if (!key) return NULL;
|
||||
|
||||
|
||||
*omode = mode;
|
||||
|
||||
if (mode == FISH_CBC_MODE)
|
||||
++data;
|
||||
|
||||
/* Decrypt */
|
||||
decrypted = fish_decrypt(key, strlen(key), data);
|
||||
|
||||
decrypted = fish_decrypt_str(key, strlen(key), data, mode);
|
||||
g_free(key);
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2010 Samuel Lidén Borell <samuel@kodafritt.se>
|
||||
Copyright (c) 2019-2020 <bakasura@protonmail.ch>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -29,10 +30,19 @@
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
char *fish_encrypt(const char *key, size_t keylen, const char *message);
|
||||
char *fish_decrypt(const char *key, size_t keylen, const char *data);
|
||||
char *fish_encrypt_for_nick(const char *nick, const char *data);
|
||||
char *fish_decrypt_from_nick(const char *nick, const char *data);
|
||||
enum fish_mode {
|
||||
FISH_ECB_MODE = 0x1,
|
||||
FISH_CBC_MODE = 0x2
|
||||
};
|
||||
|
||||
char *fish_base64_encode(const char *message, size_t message_len);
|
||||
char *fish_base64_decode(const char *message, size_t *final_len);
|
||||
char *fish_encrypt(const char *key, size_t keylen, const char *message, size_t message_len, enum fish_mode mode);
|
||||
char *fish_decrypt(const char *key, size_t keylen, const char *data, enum fish_mode mode, size_t *final_len);
|
||||
char *fish_decrypt_str(const char *key, size_t keylen, const char *data, enum fish_mode mode);
|
||||
gboolean fish_nick_has_key(const char *nick);
|
||||
GSList *fish_encrypt_for_nick(const char *nick, const char *data, enum fish_mode *omode, size_t command_len);
|
||||
char *fish_decrypt_from_nick(const char *nick, const char *data, enum fish_mode *omode);
|
||||
|
||||
#endif
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
@ -55,6 +55,7 @@
|
||||
<ItemGroup>
|
||||
<ClInclude Include="dh1080.h" />
|
||||
<ClInclude Include="fish.h" />
|
||||
<ClInclude Include="utils.h" />
|
||||
<ClInclude Include="irc.h" />
|
||||
<ClInclude Include="keystore.h" />
|
||||
<ClInclude Include="plugin_hexchat.h" />
|
||||
@ -62,6 +63,7 @@
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dh1080.c" />
|
||||
<ClCompile Include="fish.c" />
|
||||
<ClCompile Include="utils.c" />
|
||||
<ClCompile Include="irc.c" />
|
||||
<ClCompile Include="keystore.c" />
|
||||
<ClCompile Include="plugin_hexchat.c" />
|
||||
|
@ -23,9 +23,15 @@
|
||||
<ClInclude Include="bool.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="dh1080.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="fish.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="utils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="irc.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
@ -37,9 +43,15 @@
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dh1080.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="fish.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="utils.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="irc.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
|
@ -103,27 +103,55 @@ static gchar *get_nick_value(GKeyFile *keyfile, const char *nick, const char *it
|
||||
/**
|
||||
* Extracts a key from the key store file.
|
||||
*/
|
||||
char *keystore_get_key(const char *nick) {
|
||||
char *keystore_get_key(const char *nick, enum fish_mode *mode) {
|
||||
GKeyFile *keyfile;
|
||||
char *escaped_nick;
|
||||
gchar *value, *key_mode;
|
||||
int encrypted_mode;
|
||||
char *password;
|
||||
char *encrypted;
|
||||
char *decrypted;
|
||||
|
||||
/* Get the key */
|
||||
GKeyFile *keyfile = getConfigFile();
|
||||
char *escaped_nick = escape_nickname(nick);
|
||||
gchar *value = get_nick_value(keyfile, escaped_nick, "key");
|
||||
keyfile = getConfigFile();
|
||||
escaped_nick = escape_nickname(nick);
|
||||
value = get_nick_value(keyfile, escaped_nick, "key");
|
||||
key_mode = get_nick_value(keyfile, escaped_nick, "mode");
|
||||
g_key_file_free(keyfile);
|
||||
g_free(escaped_nick);
|
||||
|
||||
/* Determine cipher mode */
|
||||
*mode = FISH_ECB_MODE;
|
||||
if (key_mode) {
|
||||
if (*key_mode == '1')
|
||||
*mode = FISH_ECB_MODE;
|
||||
else if (*key_mode == '2')
|
||||
*mode = FISH_CBC_MODE;
|
||||
g_free(key_mode);
|
||||
}
|
||||
|
||||
if (!value)
|
||||
return NULL;
|
||||
|
||||
if (strncmp(value, "+OK ", 4) != 0) {
|
||||
/* Key is stored in plaintext */
|
||||
return value;
|
||||
} else {
|
||||
|
||||
if (strncmp(value, "+OK ", 4) == 0) {
|
||||
/* Key is encrypted */
|
||||
const char *encrypted = value+4;
|
||||
const char *password = get_keystore_password();
|
||||
char *decrypted = fish_decrypt(password, strlen(password), encrypted);
|
||||
encrypted = (char *) value;
|
||||
encrypted += 4;
|
||||
|
||||
encrypted_mode = FISH_ECB_MODE;
|
||||
|
||||
if (*encrypted == '*') {
|
||||
++encrypted;
|
||||
encrypted_mode = FISH_CBC_MODE;
|
||||
}
|
||||
|
||||
password = (char *) get_keystore_password();
|
||||
decrypted = fish_decrypt_str((const char *) password, strlen(password), (const char *) encrypted, encrypted_mode);
|
||||
g_free(value);
|
||||
return decrypted;
|
||||
} else {
|
||||
/* Key is stored in plaintext */
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,7 +217,7 @@ G_GNUC_END_IGNORE_DEPRECATIONS
|
||||
/**
|
||||
* Sets a key in the key store file.
|
||||
*/
|
||||
gboolean keystore_store_key(const char *nick, const char *key) {
|
||||
gboolean keystore_store_key(const char *nick, const char *key, enum fish_mode mode) {
|
||||
const char *password;
|
||||
char *encrypted;
|
||||
char *wrapped;
|
||||
@ -204,11 +232,11 @@ gboolean keystore_store_key(const char *nick, const char *key) {
|
||||
password = get_keystore_password();
|
||||
if (password) {
|
||||
/* Encrypt the password */
|
||||
encrypted = fish_encrypt(password, strlen(password), key);
|
||||
encrypted = fish_encrypt(password, strlen(password), key, strlen(key), FISH_CBC_MODE);
|
||||
if (!encrypted) goto end;
|
||||
|
||||
/* Prepend "+OK " */
|
||||
wrapped = g_strconcat("+OK ", encrypted, NULL);
|
||||
wrapped = g_strconcat("+OK *", encrypted, NULL);
|
||||
g_free(encrypted);
|
||||
|
||||
/* Store encrypted in file */
|
||||
@ -218,6 +246,9 @@ gboolean keystore_store_key(const char *nick, const char *key) {
|
||||
/* Store unencrypted in file */
|
||||
g_key_file_set_string(keyfile, escaped_nick, "key", key);
|
||||
}
|
||||
|
||||
/* Store cipher mode */
|
||||
g_key_file_set_integer(keyfile, escaped_nick, "mode", mode);
|
||||
|
||||
/* Save key store file */
|
||||
ok = save_keystore(keyfile);
|
||||
|
@ -28,9 +28,10 @@
|
||||
#include <stddef.h>
|
||||
|
||||
#include <glib.h>
|
||||
#include "fish.h"
|
||||
|
||||
char *keystore_get_key(const char *nick);
|
||||
gboolean keystore_store_key(const char *nick, const char *key);
|
||||
char *keystore_get_key(const char *nick, enum fish_mode *mode);
|
||||
gboolean keystore_store_key(const char *nick, const char *key, enum fish_mode mode);
|
||||
gboolean keystore_delete_nick(const char *nick);
|
||||
|
||||
#endif
|
||||
|
@ -2,9 +2,13 @@ if not libssl_dep.found()
|
||||
error('fish plugin requires openssl')
|
||||
endif
|
||||
|
||||
# Run tests
|
||||
subdir('tests')
|
||||
|
||||
fishlim_sources = [
|
||||
'dh1080.c',
|
||||
'fish.c',
|
||||
'utils.c',
|
||||
'irc.c',
|
||||
'keystore.c',
|
||||
'plugin_hexchat.c'
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
Copyright (c) 2010-2011 Samuel Lidén Borell <samuel@kodafritt.se>
|
||||
Copyright (c) 2015 <the.cypher@gmail.com>
|
||||
Copyright (c) 2019-2020 <bakasura@protonmail.ch>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -28,21 +29,21 @@
|
||||
#include <glib.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "hexchat-plugin.h"
|
||||
#define HEXCHAT_MAX_WORDS 32
|
||||
|
||||
#include "fish.h"
|
||||
#include "dh1080.h"
|
||||
#include "keystore.h"
|
||||
#include "irc.h"
|
||||
|
||||
static const char *fish_modes[] = {"", "ECB", "CBC", NULL};
|
||||
|
||||
static const char plugin_name[] = "FiSHLiM";
|
||||
static const char plugin_desc[] = "Encryption plugin for the FiSH protocol. Less is More!";
|
||||
static const char plugin_version[] = "0.1.0";
|
||||
static const char plugin_version[] = "1.0.0";
|
||||
|
||||
static const char usage_setkey[] = "Usage: SETKEY [<nick or #channel>] <password>, sets the key for a channel or nick";
|
||||
static const char usage_delkey[] = "Usage: DELKEY <nick or #channel>, deletes the key for a channel or nick";
|
||||
static const char usage_setkey[] = "Usage: SETKEY [<nick or #channel>] [<mode>:]<password>, sets the key for a channel or nick. Modes: ECB, CBC";
|
||||
static const char usage_delkey[] = "Usage: DELKEY [<nick or #channel>], deletes the key for a channel or nick";
|
||||
static const char usage_keyx[] = "Usage: KEYX [<nick>], performs DH1080 key-exchange with <nick>";
|
||||
static const char usage_topic[] = "Usage: TOPIC+ <topic>, sets a new encrypted topic for the current channel";
|
||||
static const char usage_notice[] = "Usage: NOTICE+ <nick or #channel> <notice>";
|
||||
@ -53,6 +54,13 @@ static hexchat_plugin *ph;
|
||||
static GHashTable *pending_exchanges;
|
||||
|
||||
|
||||
/**
|
||||
* Compare two nicks using the current plugin
|
||||
*/
|
||||
int irc_nick_cmp(const char *a, const char *b) {
|
||||
return hexchat_nickcmp (ph, a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the key store file.
|
||||
*/
|
||||
@ -88,7 +96,7 @@ static hexchat_context *find_context_on_network (const char *name) {
|
||||
int chan_id = hexchat_list_int(ph, channels, "id");
|
||||
const char *chan_name = hexchat_list_str(ph, channels, "channel");
|
||||
|
||||
if (chan_id == id && chan_name && hexchat_nickcmp (ph, chan_name, name) == 0) {
|
||||
if (chan_id == id && chan_name && irc_nick_cmp (chan_name, name) == 0) {
|
||||
ret = (hexchat_context*)hexchat_list_str(ph, channels, "context");
|
||||
break;
|
||||
}
|
||||
@ -98,8 +106,175 @@ static hexchat_context *find_context_on_network (const char *name) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
int irc_nick_cmp(const char *a, const char *b) {
|
||||
return hexchat_nickcmp (ph, a, b);
|
||||
/**
|
||||
* Retrive the field for own user in current context
|
||||
* @return the field value
|
||||
*/
|
||||
char *get_my_info(const char *field, gboolean find_in_other_context) {
|
||||
char *result = NULL;
|
||||
const char *own_nick;
|
||||
hexchat_list *list;
|
||||
hexchat_context *ctx_current, *ctx_channel;
|
||||
|
||||
/* Display message */
|
||||
own_nick = hexchat_get_info(ph, "nick");
|
||||
|
||||
if (!own_nick)
|
||||
return NULL;
|
||||
|
||||
/* Get field for own nick if any */
|
||||
list = hexchat_list_get(ph, "users");
|
||||
if (list) {
|
||||
while (hexchat_list_next(ph, list)) {
|
||||
if (irc_nick_cmp(own_nick, hexchat_list_str(ph, list, "nick")) == 0)
|
||||
result = g_strdup(hexchat_list_str(ph, list, field));
|
||||
}
|
||||
hexchat_list_free(ph, list);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Try to get from a channel (we are outside a channel) */
|
||||
if (!find_in_other_context) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
list = hexchat_list_get(ph, "channels");
|
||||
if (list) {
|
||||
ctx_current = hexchat_get_context(ph);
|
||||
while (hexchat_list_next(ph, list)) {
|
||||
ctx_channel = (hexchat_context *) hexchat_list_str(ph, list, "context");
|
||||
|
||||
hexchat_set_context(ph, ctx_channel);
|
||||
result = get_my_info(field, FALSE);
|
||||
hexchat_set_context(ph, ctx_current);
|
||||
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
hexchat_list_free(ph, list);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive the prefix character for own nick in current context
|
||||
* @return @ or + or NULL
|
||||
*/
|
||||
char *get_my_own_prefix(void) {
|
||||
return get_my_info("prefix", FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive the mask for own nick in current context
|
||||
* @return Host name in the form: user@host (or NULL if not known)
|
||||
*/
|
||||
char *get_my_own_host(void) {
|
||||
return get_my_info("host", TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of prefix for current user in current context
|
||||
*
|
||||
* @return Length of prefix
|
||||
*/
|
||||
int get_prefix_length(void) {
|
||||
char *own_host;
|
||||
int prefix_len = 0;
|
||||
|
||||
/* ':! ' + 'nick' + 'ident@host', e.g. ':user!~name@mynet.com ' */
|
||||
prefix_len = 3 + strlen(hexchat_get_info(ph, "nick"));
|
||||
own_host = get_my_own_host();
|
||||
if (own_host) {
|
||||
prefix_len += strlen(own_host);
|
||||
} else {
|
||||
/* https://stackoverflow.com/questions/8724954/what-is-the-maximum-number-of-characters-for-a-host-name-in-unix */
|
||||
prefix_len += 64;
|
||||
}
|
||||
g_free(own_host);
|
||||
|
||||
return prefix_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to decrypt the first occurrence of fish message
|
||||
*
|
||||
* @param message Message to decrypt
|
||||
* @param key Key of message
|
||||
* @return Array of char with decrypted message or NULL. The returned string
|
||||
* should be freed with g_free() when no longer needed.
|
||||
*/
|
||||
char *decrypt_raw_message(const char *message, const char *key) {
|
||||
const char *prefixes[] = {"+OK ", "mcps ", NULL};
|
||||
char *start = NULL, *end = NULL;
|
||||
char *left = NULL, *right = NULL;
|
||||
char *encrypted = NULL, *decrypted = NULL;
|
||||
int length = 0;
|
||||
int index_prefix;
|
||||
enum fish_mode mode;
|
||||
GString *message_decrypted;
|
||||
char *result = NULL;
|
||||
|
||||
if (message == NULL || key == NULL)
|
||||
return NULL;
|
||||
|
||||
for (index_prefix = 0; index_prefix < 2; index_prefix++) {
|
||||
start = g_strstr_len(message, strlen(message), prefixes[index_prefix]);
|
||||
if (start) {
|
||||
/* Length ALWAYS will be less that original message
|
||||
* add '[CBC] ' length */
|
||||
message_decrypted = g_string_sized_new(strlen(message) + 6);
|
||||
|
||||
/* Left part of message */
|
||||
left = g_strndup(message, start - message);
|
||||
g_string_append(message_decrypted, left);
|
||||
g_free(left);
|
||||
|
||||
/* Encrypted part */
|
||||
start += strlen(prefixes[index_prefix]);
|
||||
end = g_strstr_len(start, strlen(message), " ");
|
||||
if (end) {
|
||||
length = end - start;
|
||||
right = end;
|
||||
}
|
||||
|
||||
if (length > 0) {
|
||||
encrypted = g_strndup(start, length);
|
||||
} else {
|
||||
encrypted = g_strdup(start);
|
||||
}
|
||||
decrypted = fish_decrypt_from_nick(key, encrypted, &mode);
|
||||
g_free(encrypted);
|
||||
|
||||
if (decrypted == NULL) {
|
||||
g_string_free(message_decrypted, TRUE);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Add encrypted flag */
|
||||
g_string_append(message_decrypted, "[");
|
||||
g_string_append(message_decrypted, fish_modes[mode]);
|
||||
g_string_append(message_decrypted, "] ");
|
||||
/* Decrypted message */
|
||||
g_string_append(message_decrypted, decrypted);
|
||||
g_free(decrypted);
|
||||
|
||||
/* Right part of message */
|
||||
if (right) {
|
||||
g_string_append(message_decrypted, right);
|
||||
}
|
||||
|
||||
result = message_decrypted->str;
|
||||
g_string_free(message_decrypted, FALSE);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*static int handle_debug(char *word[], char *word_eol[], void *userdata) {
|
||||
@ -115,20 +290,49 @@ int irc_nick_cmp(const char *a, const char *b) {
|
||||
* Called when a message is to be sent.
|
||||
*/
|
||||
static int handle_outgoing(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *own_nick;
|
||||
/* Encrypt the message if possible */
|
||||
char *prefix;
|
||||
enum fish_mode mode;
|
||||
char *message;
|
||||
GString *command;
|
||||
GSList *encrypted_list, *encrypted_item;
|
||||
|
||||
const char *channel = hexchat_get_info(ph, "channel");
|
||||
char *encrypted = fish_encrypt_for_nick(channel, word_eol[1]);
|
||||
if (!encrypted) return HEXCHAT_EAT_NONE;
|
||||
|
||||
|
||||
/* Check if we can encrypt */
|
||||
if (!fish_nick_has_key(channel)) return HEXCHAT_EAT_NONE;
|
||||
|
||||
command = g_string_new("");
|
||||
g_string_printf(command, "PRIVMSG %s :+OK ", channel);
|
||||
|
||||
encrypted_list = fish_encrypt_for_nick(channel, word_eol[1], &mode, get_prefix_length() + command->len);
|
||||
if (!encrypted_list) {
|
||||
g_string_free(command, TRUE);
|
||||
return HEXCHAT_EAT_NONE;
|
||||
}
|
||||
|
||||
/* Get prefix for own nick if any */
|
||||
prefix = get_my_own_prefix();
|
||||
|
||||
/* Add encrypted flag */
|
||||
message = g_strconcat("[", fish_modes[mode], "] ", word_eol[1], NULL);
|
||||
|
||||
/* Display message */
|
||||
own_nick = hexchat_get_info(ph, "nick");
|
||||
hexchat_emit_print(ph, "Your Message", own_nick, word_eol[1], NULL);
|
||||
|
||||
/* Send message */
|
||||
hexchat_commandf(ph, "PRIVMSG %s :+OK %s", channel, encrypted);
|
||||
|
||||
g_free(encrypted);
|
||||
hexchat_emit_print(ph, "Your Message", hexchat_get_info(ph, "nick"), message, prefix, NULL);
|
||||
g_free(message);
|
||||
|
||||
/* Send encrypted messages */
|
||||
encrypted_item = encrypted_list;
|
||||
while (encrypted_item)
|
||||
{
|
||||
hexchat_commandf(ph, "%s%s", command->str, (char *)encrypted_item->data);
|
||||
|
||||
encrypted_item = encrypted_item->next;
|
||||
}
|
||||
|
||||
g_free(prefix);
|
||||
g_string_free(command, TRUE);
|
||||
g_slist_free_full(encrypted_list, g_free);
|
||||
|
||||
return HEXCHAT_EAT_HEXCHAT;
|
||||
}
|
||||
|
||||
@ -139,96 +343,59 @@ static int handle_incoming(char *word[], char *word_eol[], hexchat_event_attrs *
|
||||
const char *prefix;
|
||||
const char *command;
|
||||
const char *recipient;
|
||||
const char *encrypted;
|
||||
const char *peice;
|
||||
const char *raw_message = word_eol[1];
|
||||
char *sender_nick;
|
||||
char *decrypted;
|
||||
size_t w;
|
||||
size_t ew;
|
||||
size_t uw;
|
||||
char prefix_char = 0;
|
||||
size_t parameters_offset;
|
||||
GString *message;
|
||||
|
||||
if (!irc_parse_message((const char **)word, &prefix, &command, &w))
|
||||
if (!irc_parse_message((const char **)word, &prefix, &command, ¶meters_offset))
|
||||
return HEXCHAT_EAT_NONE;
|
||||
|
||||
|
||||
/* Topic (command 332) has an extra parameter */
|
||||
if (!strcmp(command, "332")) w++;
|
||||
|
||||
/* Look for encrypted data */
|
||||
for (ew = w+1; ew < HEXCHAT_MAX_WORDS-1; ew++) {
|
||||
const char *s = (ew == w+1 ? word[ew]+1 : word[ew]);
|
||||
if (*s && (s[1] == '+' || s[1] == 'm')) { prefix_char = *(s++); }
|
||||
else { prefix_char = 0; }
|
||||
if (strcmp(s, "+OK") == 0 || strcmp(s, "mcps") == 0) goto has_encrypted_data;
|
||||
if (!strcmp(command, "332"))
|
||||
parameters_offset++;
|
||||
|
||||
/* Extract sender nick and recipient nick/channel and try to decrypt */
|
||||
recipient = word[parameters_offset];
|
||||
decrypted = decrypt_raw_message(raw_message, recipient);
|
||||
if (decrypted == NULL) {
|
||||
sender_nick = irc_prefix_get_nick(prefix);
|
||||
decrypted = decrypt_raw_message(raw_message, sender_nick);
|
||||
g_free(sender_nick);
|
||||
}
|
||||
return HEXCHAT_EAT_NONE;
|
||||
has_encrypted_data: ;
|
||||
/* Extract sender nick and recipient nick/channel */
|
||||
sender_nick = irc_prefix_get_nick(prefix);
|
||||
recipient = word[w];
|
||||
|
||||
/* Try to decrypt with these (the keys are searched for in the key store) */
|
||||
encrypted = word[ew+1];
|
||||
decrypted = fish_decrypt_from_nick(recipient, encrypted);
|
||||
if (!decrypted) decrypted = fish_decrypt_from_nick(sender_nick, encrypted);
|
||||
|
||||
/* Check for error */
|
||||
if (!decrypted) goto decrypt_error;
|
||||
|
||||
/* Build unecrypted message */
|
||||
message = g_string_sized_new (100); /* TODO: more accurate estimation of size */
|
||||
g_string_append (message, "RECV");
|
||||
|
||||
/* Nothing to decrypt */
|
||||
if (decrypted == NULL)
|
||||
return HEXCHAT_EAT_NONE;
|
||||
|
||||
/* Build decrypted message */
|
||||
|
||||
/* decrypted + 'RECV ' + '@time=YYYY-MM-DDTHH:MM:SS.fffffZ ' */
|
||||
message = g_string_sized_new (strlen(decrypted) + 5 + 33);
|
||||
g_string_append (message, "RECV ");
|
||||
|
||||
if (attrs->server_time_utc)
|
||||
{
|
||||
GTimeVal tv = { (glong)attrs->server_time_utc, 0 };
|
||||
char *timestamp = g_time_val_to_iso8601 (&tv);
|
||||
|
||||
g_string_append (message, " @time=");
|
||||
g_string_append (message, "@time=");
|
||||
g_string_append (message, timestamp);
|
||||
g_string_append (message, " ");
|
||||
g_free (timestamp);
|
||||
}
|
||||
|
||||
for (uw = 1; uw < HEXCHAT_MAX_WORDS; uw++) {
|
||||
if (word[uw][0] != '\0')
|
||||
g_string_append_c (message, ' ');
|
||||
|
||||
if (uw == ew) {
|
||||
/* Add the encrypted data */
|
||||
peice = decrypted;
|
||||
uw++; /* Skip "OK+" */
|
||||
|
||||
if (ew == w+1) {
|
||||
/* Prefix with colon, which gets stripped out otherwise */
|
||||
g_string_append_c (message, ':');
|
||||
}
|
||||
|
||||
if (prefix_char) {
|
||||
g_string_append_c (message, prefix_char);
|
||||
}
|
||||
|
||||
} else {
|
||||
/* Add unencrypted data (for example, a prefix from a bouncer or bot) */
|
||||
peice = word[uw];
|
||||
}
|
||||
|
||||
g_string_append (message, peice);
|
||||
}
|
||||
g_string_append (message, decrypted);
|
||||
g_free(decrypted);
|
||||
|
||||
/* Simulate unencrypted message */
|
||||
/* hexchat_printf(ph, "simulating: %s\n", message->str); */
|
||||
/* Fake server message
|
||||
* RECV command will throw this function again, if message have multiple
|
||||
* encrypted data, we will decrypt all */
|
||||
hexchat_command(ph, message->str);
|
||||
|
||||
g_string_free (message, TRUE);
|
||||
g_free(sender_nick);
|
||||
return HEXCHAT_EAT_HEXCHAT;
|
||||
|
||||
decrypt_error:
|
||||
g_free(decrypted);
|
||||
g_free(sender_nick);
|
||||
return HEXCHAT_EAT_NONE;
|
||||
return HEXCHAT_EAT_HEXCHAT;
|
||||
}
|
||||
|
||||
static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
@ -236,8 +403,8 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *dh_pubkey = word[5];
|
||||
hexchat_context *query_ctx;
|
||||
const char *prefix;
|
||||
gboolean cbc;
|
||||
char *sender, *secret_key, *priv_key = NULL;
|
||||
enum fish_mode mode = FISH_ECB_MODE;
|
||||
|
||||
if (!*dh_message || !*dh_pubkey || strlen(dh_pubkey) != 181)
|
||||
return HEXCHAT_EAT_NONE;
|
||||
@ -248,25 +415,19 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
sender = irc_prefix_get_nick(prefix);
|
||||
query_ctx = find_context_on_network(sender);
|
||||
if (query_ctx)
|
||||
hexchat_set_context(ph, query_ctx);
|
||||
g_assert(hexchat_set_context(ph, query_ctx) == 1);
|
||||
|
||||
dh_message++; /* : prefix */
|
||||
if (*dh_message == '+' || *dh_message == '-')
|
||||
dh_message++; /* identify-msg */
|
||||
|
||||
cbc = g_strcmp0 (word[6], "CBC") == 0;
|
||||
if (g_strcmp0 (word[6], "CBC") == 0)
|
||||
mode = FISH_CBC_MODE;
|
||||
|
||||
if (!strcmp(dh_message, "DH1080_INIT")) {
|
||||
char *pub_key;
|
||||
|
||||
if (cbc) {
|
||||
hexchat_print(ph, "Received key exchange for CBC mode which is not supported.");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
hexchat_printf(ph, "Received public key from %s, sending mine...", sender);
|
||||
hexchat_printf(ph, "Received public key from %s (%s), sending mine...", sender, fish_modes[mode]);
|
||||
if (dh1080_generate_key(&priv_key, &pub_key)) {
|
||||
hexchat_commandf(ph, "quote NOTICE %s :DH1080_FINISH %s", sender, pub_key);
|
||||
hexchat_commandf(ph, "quote NOTICE %s :DH1080_FINISH %s%s", sender, pub_key, (mode == FISH_CBC_MODE) ? " CBC" : "");
|
||||
g_free(pub_key);
|
||||
} else {
|
||||
hexchat_print(ph, "Failed to generate keys");
|
||||
@ -279,11 +440,6 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
g_hash_table_steal(pending_exchanges, sender_lower);
|
||||
g_free(sender_lower);
|
||||
|
||||
if (cbc) {
|
||||
hexchat_print(ph, "Received key exchange for CBC mode which is not supported.");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (!priv_key) {
|
||||
hexchat_printf(ph, "Received a key exchange response for unknown user: %s", sender);
|
||||
goto cleanup;
|
||||
@ -295,8 +451,8 @@ static int handle_keyx_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
}
|
||||
|
||||
if (dh1080_compute_key(priv_key, dh_pubkey, &secret_key)) {
|
||||
keystore_store_key(sender, secret_key);
|
||||
hexchat_printf(ph, "Stored new key for %s", sender);
|
||||
keystore_store_key(sender, secret_key, mode);
|
||||
hexchat_printf(ph, "Stored new key for %s (%s)", sender, fish_modes[mode]);
|
||||
g_free(secret_key);
|
||||
} else {
|
||||
hexchat_print(ph, "Failed to create secret key!");
|
||||
@ -314,6 +470,7 @@ cleanup:
|
||||
static int handle_setkey(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *nick;
|
||||
const char *key;
|
||||
enum fish_mode mode;
|
||||
|
||||
/* Check syntax */
|
||||
if (*word[2] == '\0') {
|
||||
@ -331,9 +488,17 @@ static int handle_setkey(char *word[], char *word_eol[], void *userdata) {
|
||||
key = word_eol[3];
|
||||
}
|
||||
|
||||
mode = FISH_ECB_MODE;
|
||||
if (g_ascii_strncasecmp("cbc:", key, 4) == 0) {
|
||||
key = key+4;
|
||||
mode = FISH_CBC_MODE;
|
||||
} else if (g_ascii_strncasecmp("ecb:", key, 4) == 0) {
|
||||
key = key+4;
|
||||
}
|
||||
|
||||
/* Set password */
|
||||
if (keystore_store_key(nick, key)) {
|
||||
hexchat_printf(ph, "Stored key for %s\n", nick);
|
||||
if (keystore_store_key(nick, key, mode)) {
|
||||
hexchat_printf(ph, "Stored key for %s (%s)\n", nick, fish_modes[mode]);
|
||||
} else {
|
||||
hexchat_printf(ph, "\00305Failed to store key in addon_fishlim.conf\n");
|
||||
}
|
||||
@ -345,22 +510,30 @@ static int handle_setkey(char *word[], char *word_eol[], void *userdata) {
|
||||
* Command handler for /delkey
|
||||
*/
|
||||
static int handle_delkey(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *nick;
|
||||
char *nick = NULL;
|
||||
int ctx_type = 0;
|
||||
|
||||
/* Check syntax */
|
||||
if (*word[2] == '\0' || *word[3] != '\0') {
|
||||
hexchat_printf(ph, "%s\n", usage_delkey);
|
||||
return HEXCHAT_EAT_HEXCHAT;
|
||||
/* Delete key from input */
|
||||
if (*word[2] != '\0') {
|
||||
nick = g_strstrip(g_strdup(word_eol[2]));
|
||||
} else { /* Delete key from current context */
|
||||
nick = g_strdup(hexchat_get_info(ph, "channel"));
|
||||
ctx_type = hexchat_list_int(ph, NULL, "type");
|
||||
|
||||
/* Only allow channel or dialog */
|
||||
if (ctx_type < 2 || ctx_type > 3) {
|
||||
hexchat_printf(ph, "%s\n", usage_delkey);
|
||||
return HEXCHAT_EAT_HEXCHAT;
|
||||
}
|
||||
}
|
||||
|
||||
nick = g_strstrip (word_eol[2]);
|
||||
|
||||
/* Delete the given nick from the key store */
|
||||
if (keystore_delete_nick(nick)) {
|
||||
hexchat_printf(ph, "Deleted key for %s\n", nick);
|
||||
} else {
|
||||
hexchat_printf(ph, "\00305Failed to delete key in addon_fishlim.conf!\n");
|
||||
}
|
||||
g_free(nick);
|
||||
|
||||
return HEXCHAT_EAT_HEXCHAT;
|
||||
}
|
||||
@ -379,7 +552,7 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) {
|
||||
}
|
||||
|
||||
if (query_ctx) {
|
||||
hexchat_set_context(ph, query_ctx);
|
||||
g_assert(hexchat_set_context(ph, query_ctx) == 1);
|
||||
ctx_type = hexchat_list_int(ph, NULL, "type");
|
||||
}
|
||||
|
||||
@ -391,8 +564,8 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) {
|
||||
if (dh1080_generate_key(&priv_key, &pub_key)) {
|
||||
g_hash_table_replace (pending_exchanges, g_ascii_strdown(target, -1), priv_key);
|
||||
|
||||
hexchat_commandf(ph, "quote NOTICE %s :DH1080_INIT %s", target, pub_key);
|
||||
hexchat_printf(ph, "Sent public key to %s, waiting for reply...", target);
|
||||
hexchat_commandf(ph, "quote NOTICE %s :DH1080_INIT %s CBC", target, pub_key);
|
||||
hexchat_printf(ph, "Sent public key to %s (CBC), waiting for reply...", target);
|
||||
|
||||
g_free(pub_key);
|
||||
} else {
|
||||
@ -408,7 +581,9 @@ static int handle_keyx(char *word[], char *word_eol[], void *userdata) {
|
||||
static int handle_crypt_topic(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *target;
|
||||
const char *topic = word_eol[2];
|
||||
char *buf;
|
||||
enum fish_mode mode;
|
||||
GString *command;
|
||||
GSList *encrypted_list;
|
||||
|
||||
if (!*topic) {
|
||||
hexchat_print(ph, usage_topic);
|
||||
@ -421,40 +596,77 @@ static int handle_crypt_topic(char *word[], char *word_eol[], void *userdata) {
|
||||
}
|
||||
|
||||
target = hexchat_get_info(ph, "channel");
|
||||
buf = fish_encrypt_for_nick(target, topic);
|
||||
if (buf == NULL) {
|
||||
|
||||
/* Check if we can encrypt */
|
||||
if (!fish_nick_has_key(target)) {
|
||||
hexchat_printf(ph, "/topic+ error, no key found for %s", target);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
hexchat_commandf(ph, "TOPIC %s +OK %s", target, buf);
|
||||
g_free(buf);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
command = g_string_new("");
|
||||
g_string_printf(command, "TOPIC %s +OK ", target);
|
||||
|
||||
encrypted_list = fish_encrypt_for_nick(target, topic, &mode, get_prefix_length() + command->len);
|
||||
if (!encrypted_list) {
|
||||
g_string_free(command, TRUE);
|
||||
hexchat_printf(ph, "/topic+ error, can't encrypt %s", target);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
hexchat_commandf(ph, "%s%s", command->str, (char *) encrypted_list->data);
|
||||
|
||||
g_string_free(command, TRUE);
|
||||
g_slist_free_full(encrypted_list, g_free);
|
||||
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Command handler for /notice+
|
||||
*/
|
||||
static int handle_crypt_notice(char *word[], char *word_eol[], void *userdata)
|
||||
{
|
||||
static int handle_crypt_notice(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *target = word[2];
|
||||
const char *notice = word_eol[3];
|
||||
char *buf;
|
||||
char *notice_flag;
|
||||
enum fish_mode mode;
|
||||
GString *command;
|
||||
GSList *encrypted_list, *encrypted_item;
|
||||
|
||||
if (!*target || !*notice) {
|
||||
hexchat_print(ph, usage_notice);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
buf = fish_encrypt_for_nick(target, notice);
|
||||
if (buf == NULL) {
|
||||
/* Check if we can encrypt */
|
||||
if (!fish_nick_has_key(target)) {
|
||||
hexchat_printf(ph, "/notice+ error, no key found for %s.", target);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
hexchat_commandf(ph, "quote NOTICE %s :+OK %s", target, buf);
|
||||
hexchat_emit_print(ph, "Notice Sent", target, notice);
|
||||
g_free(buf);
|
||||
command = g_string_new("");
|
||||
g_string_printf(command, "quote NOTICE %s :+OK ", target);
|
||||
|
||||
encrypted_list = fish_encrypt_for_nick(target, notice, &mode, get_prefix_length() + command->len);
|
||||
if (!encrypted_list) {
|
||||
g_string_free(command, TRUE);
|
||||
hexchat_printf(ph, "/notice+ error, can't encrypt %s", target);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
notice_flag = g_strconcat("[", fish_modes[mode], "] ", notice, NULL);
|
||||
hexchat_emit_print(ph, "Notice Send", target, notice_flag);
|
||||
|
||||
/* Send encrypted messages */
|
||||
encrypted_item = encrypted_list;
|
||||
while (encrypted_item) {
|
||||
hexchat_commandf(ph, "%s%s", command->str, (char *) encrypted_item->data);
|
||||
|
||||
encrypted_item = encrypted_item->next;
|
||||
}
|
||||
|
||||
g_free(notice_flag);
|
||||
g_string_free(command, TRUE);
|
||||
g_slist_free_full(encrypted_list, g_free);
|
||||
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
@ -462,56 +674,102 @@ static int handle_crypt_notice(char *word[], char *word_eol[], void *userdata)
|
||||
/**
|
||||
* Command handler for /msg+
|
||||
*/
|
||||
static int handle_crypt_msg(char *word[], char *word_eol[], void *userdata)
|
||||
{
|
||||
static int handle_crypt_msg(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *target = word[2];
|
||||
const char *message = word_eol[3];
|
||||
char *message_flag;
|
||||
char *prefix;
|
||||
hexchat_context *query_ctx;
|
||||
char *buf;
|
||||
enum fish_mode mode;
|
||||
GString *command;
|
||||
GSList *encrypted_list, *encrypted_item;
|
||||
|
||||
if (!*target || !*message) {
|
||||
hexchat_print(ph, usage_msg);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
buf = fish_encrypt_for_nick(target, message);
|
||||
if (buf == NULL) {
|
||||
/* Check if we can encrypt */
|
||||
if (!fish_nick_has_key(target)) {
|
||||
hexchat_printf(ph, "/msg+ error, no key found for %s", target);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
hexchat_commandf(ph, "PRIVMSG %s :+OK %s", target, buf);
|
||||
command = g_string_new("");
|
||||
g_string_printf(command, "PRIVMSG %s :+OK ", target);
|
||||
|
||||
encrypted_list = fish_encrypt_for_nick(target, message, &mode, get_prefix_length() + command->len);
|
||||
if (!encrypted_list) {
|
||||
g_string_free(command, TRUE);
|
||||
hexchat_printf(ph, "/msg+ error, can't encrypt %s", target);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
/* Send encrypted messages */
|
||||
encrypted_item = encrypted_list;
|
||||
while (encrypted_item) {
|
||||
hexchat_commandf(ph, "%s%s", command->str, (char *) encrypted_item->data);
|
||||
|
||||
encrypted_item = encrypted_item->next;
|
||||
}
|
||||
|
||||
g_string_free(command, TRUE);
|
||||
g_slist_free_full(encrypted_list, g_free);
|
||||
|
||||
query_ctx = find_context_on_network(target);
|
||||
if (query_ctx) {
|
||||
hexchat_set_context(ph, query_ctx);
|
||||
g_assert(hexchat_set_context(ph, query_ctx) == 1);
|
||||
|
||||
/* FIXME: Mode char */
|
||||
hexchat_emit_print(ph, "Your Message", hexchat_get_info(ph, "nick"),
|
||||
message, "", "");
|
||||
}
|
||||
else {
|
||||
prefix = get_my_own_prefix();
|
||||
|
||||
/* Add encrypted flag */
|
||||
message_flag = g_strconcat("[", fish_modes[mode], "] ", message, NULL);
|
||||
hexchat_emit_print(ph, "Your Message", hexchat_get_info(ph, "nick"), message_flag, prefix, NULL);
|
||||
g_free(prefix);
|
||||
g_free(message_flag);
|
||||
} else {
|
||||
hexchat_emit_print(ph, "Message Send", target, message);
|
||||
}
|
||||
|
||||
g_free(buf);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
static int handle_crypt_me(char *word[], char *word_eol[], void *userdata) {
|
||||
const char *channel = hexchat_get_info(ph, "channel");
|
||||
char *buf;
|
||||
const char *channel = hexchat_get_info(ph, "channel");
|
||||
enum fish_mode mode;
|
||||
GString *command;
|
||||
GSList *encrypted_list, *encrypted_item;
|
||||
|
||||
buf = fish_encrypt_for_nick(channel, word_eol[2]);
|
||||
if (!buf)
|
||||
/* Check if we can encrypt */
|
||||
if (!fish_nick_has_key(channel)) {
|
||||
return HEXCHAT_EAT_NONE;
|
||||
}
|
||||
|
||||
hexchat_commandf(ph, "PRIVMSG %s :\001ACTION +OK %s \001", channel, buf);
|
||||
hexchat_emit_print(ph, "Your Action", hexchat_get_info(ph, "nick"),
|
||||
word_eol[2], NULL);
|
||||
command = g_string_new("");
|
||||
g_string_printf(command, "PRIVMSG %s :\001ACTION +OK ", channel);
|
||||
|
||||
g_free(buf);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
/* 2 = ' \001' */
|
||||
encrypted_list = fish_encrypt_for_nick(channel, word_eol[2], &mode, get_prefix_length() + command->len + 2);
|
||||
if (!encrypted_list) {
|
||||
g_string_free(command, TRUE);
|
||||
hexchat_printf(ph, "/me error, can't encrypt %s", channel);
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
hexchat_emit_print(ph, "Your Action", hexchat_get_info(ph, "nick"), word_eol[2], NULL);
|
||||
|
||||
/* Send encrypted messages */
|
||||
encrypted_item = encrypted_list;
|
||||
while (encrypted_item) {
|
||||
hexchat_commandf(ph, "%s%s \001", command->str, (char *) encrypted_item->data);
|
||||
|
||||
encrypted_item = encrypted_item->next;
|
||||
}
|
||||
|
||||
g_string_free(command, TRUE);
|
||||
g_slist_free_full(encrypted_list, g_free);
|
||||
|
||||
return HEXCHAT_EAT_ALL;
|
||||
}
|
||||
|
||||
/**
|
||||
|
15
plugins/fishlim/tests/meson.build
Normal file
15
plugins/fishlim/tests/meson.build
Normal file
@ -0,0 +1,15 @@
|
||||
fishlim_test_sources = [
|
||||
'tests.c',
|
||||
'mock-keystore.c',
|
||||
'../fish.c',
|
||||
'../utils.c',
|
||||
]
|
||||
|
||||
fishlim_tests = executable('fishlim_tests', fishlim_test_sources,
|
||||
dependencies: [libgio_dep, libssl_dep, hexchat_plugin_dep],
|
||||
include_directories: include_directories('..'),
|
||||
)
|
||||
|
||||
test('Fishlim Tests', fishlim_tests,
|
||||
protocol: 'tap',
|
||||
)
|
51
plugins/fishlim/tests/mock-keystore.c
Normal file
51
plugins/fishlim/tests/mock-keystore.c
Normal file
@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright (c) 2010 Samuel Lidén Borell <samuel@kodafritt.se>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
#include "fish.h"
|
||||
|
||||
/**
|
||||
* Extracts a key from the key store file.
|
||||
*/
|
||||
char *
|
||||
keystore_get_key(const char *nick, enum fish_mode *mode)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a key in the key store file.
|
||||
*/
|
||||
gboolean
|
||||
keystore_store_key(const char *nick, const char *key, enum fish_mode mode)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a nick from the key store.
|
||||
*/
|
||||
gboolean
|
||||
keystore_delete_nick(const char *nick)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
286
plugins/fishlim/tests/tests.c
Normal file
286
plugins/fishlim/tests/tests.c
Normal file
@ -0,0 +1,286 @@
|
||||
/*
|
||||
Copyright (c) 2020 <bakasura@protonmail.ch>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
#include "fish.h"
|
||||
#include "utils.h"
|
||||
|
||||
/**
|
||||
* Auxiliary function: Generate a random string
|
||||
* @param out Preallocated string to fill
|
||||
* @param len Size of bytes to fill
|
||||
*/
|
||||
static void
|
||||
random_string(char *out, size_t len)
|
||||
{
|
||||
GRand *rand = NULL;
|
||||
int i = 0;
|
||||
|
||||
rand = g_rand_new();
|
||||
for (i = 0; i < len; ++i) {
|
||||
out[i] = g_rand_int_range(rand, 1, 256);
|
||||
}
|
||||
|
||||
out[len] = 0;
|
||||
|
||||
g_rand_free(rand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check encrypt and decrypt in ECB mode
|
||||
*/
|
||||
static void
|
||||
test_ecb(void)
|
||||
{
|
||||
char *b64 = NULL;
|
||||
char *de = NULL;
|
||||
int key_len, message_len = 0;
|
||||
char key[57];
|
||||
char message[1000];
|
||||
|
||||
/* Generate key 32–448 bits (Yes, I start with 8 bits) */
|
||||
for (key_len = 1; key_len < 57; ++key_len) {
|
||||
|
||||
random_string(key, key_len);
|
||||
|
||||
for (message_len = 1; message_len < 1000; ++message_len) {
|
||||
random_string(message, message_len);
|
||||
|
||||
/* Encrypt */
|
||||
b64 = fish_encrypt(key, key_len, message, message_len, FISH_ECB_MODE);
|
||||
g_assert_nonnull(b64);
|
||||
|
||||
/* Decrypt */
|
||||
/* Linear */
|
||||
de = fish_decrypt_str(key, key_len, b64, FISH_ECB_MODE);
|
||||
g_assert_cmpstr (de, ==, message);
|
||||
g_free(de);
|
||||
|
||||
/* Mixed */
|
||||
de = fish_decrypt_str(key, key_len, b64, FISH_ECB_MODE);
|
||||
g_assert_cmpstr (de, ==, message);
|
||||
g_free(de);
|
||||
|
||||
g_free(b64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check encrypt and decrypt in CBC mode
|
||||
*/
|
||||
static void
|
||||
test_cbc(void)
|
||||
{
|
||||
char *b64 = NULL;
|
||||
char *de = NULL;
|
||||
int key_len, message_len = 0;
|
||||
char key[57];
|
||||
char message[1000];
|
||||
|
||||
/* Generate key 32–448 bits (Yes, I start with 8 bits) */
|
||||
for (key_len = 1; key_len < 57; ++key_len) {
|
||||
|
||||
random_string(key, key_len);
|
||||
|
||||
for (message_len = 1; message_len < 1000; ++message_len) {
|
||||
random_string(message, message_len);
|
||||
|
||||
/* Encrypt */
|
||||
b64 = fish_encrypt(key, key_len, message, message_len, FISH_CBC_MODE);
|
||||
g_assert_nonnull(b64);
|
||||
|
||||
/* Decrypt */
|
||||
/* Linear */
|
||||
de = fish_decrypt_str(key, key_len, b64, FISH_CBC_MODE);
|
||||
g_assert_cmpstr (de, ==, message);
|
||||
g_free(de);
|
||||
|
||||
g_free(b64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the calculation of final length from an encoded string in Base64
|
||||
*/
|
||||
static void
|
||||
test_base64_len (void)
|
||||
{
|
||||
char *b64 = NULL;
|
||||
int i, message_len = 0;
|
||||
char message[1000];
|
||||
|
||||
for (i = 0; i < 10; ++i) {
|
||||
for (message_len = 1; message_len < 1000; ++message_len) {
|
||||
random_string(message, message_len);
|
||||
b64 = g_base64_encode((const unsigned char *) message, message_len);
|
||||
g_assert_nonnull(b64);
|
||||
g_assert_cmpuint(strlen(b64), == , base64_len(message_len));
|
||||
g_free(b64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the calculation of final length from an encoded string in BlowcryptBase64
|
||||
*/
|
||||
static void
|
||||
test_base64_fish_len (void)
|
||||
{
|
||||
char *b64 = NULL;
|
||||
int i, message_len = 0;
|
||||
char message[1000];
|
||||
|
||||
for (i = 0; i < 10; ++i) {
|
||||
|
||||
for (message_len = 1; message_len < 1000; ++message_len) {
|
||||
random_string(message, message_len);
|
||||
b64 = fish_base64_encode(message, message_len);
|
||||
g_assert_nonnull(b64);
|
||||
g_assert_cmpuint(strlen(b64), == , base64_fish_len(message_len));
|
||||
g_free(b64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the calculation of final length from an encrypted string in ECB mode
|
||||
*/
|
||||
static void
|
||||
test_base64_ecb_len(void)
|
||||
{
|
||||
char *b64 = NULL;
|
||||
int key_len, message_len = 0;
|
||||
char key[57];
|
||||
char message[1000];
|
||||
|
||||
/* Generate key 32–448 bits (Yes, I start with 8 bits) */
|
||||
for (key_len = 1; key_len < 57; ++key_len) {
|
||||
|
||||
random_string(key, key_len);
|
||||
|
||||
for (message_len = 1; message_len < 1000; ++message_len) {
|
||||
random_string(message, message_len);
|
||||
b64 = fish_encrypt(key, key_len, message, message_len, FISH_ECB_MODE);
|
||||
g_assert_nonnull(b64);
|
||||
g_assert_cmpuint(strlen(b64), == , ecb_len(message_len));
|
||||
g_free(b64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the calculation of final length from an encrypted string in CBC mode
|
||||
*/
|
||||
static void
|
||||
test_base64_cbc_len(void)
|
||||
{
|
||||
char *b64 = NULL;
|
||||
int key_len, message_len = 0;
|
||||
char key[57];
|
||||
char message[1000];
|
||||
|
||||
/* Generate key 32–448 bits (Yes, I start with 8 bits) */
|
||||
for (key_len = 1; key_len < 57; ++key_len) {
|
||||
|
||||
random_string(key, key_len);
|
||||
|
||||
for (message_len = 1; message_len < 1000; ++message_len) {
|
||||
random_string(message, message_len);
|
||||
b64 = fish_encrypt(key, key_len, message, message_len, FISH_CBC_MODE);
|
||||
g_assert_nonnull(b64);
|
||||
g_assert_cmpuint(strlen(b64), == , cbc_len(message_len));
|
||||
g_free(b64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the calculation of length limit for a plaintext in each encryption mode
|
||||
*/
|
||||
static void
|
||||
test_max_text_command_len(void)
|
||||
{
|
||||
int max_encoded_len, plaintext_len;
|
||||
enum fish_mode mode;
|
||||
|
||||
for (max_encoded_len = 0; max_encoded_len < 10000; ++max_encoded_len) {
|
||||
for (mode = FISH_ECB_MODE; mode <= FISH_CBC_MODE; ++mode) {
|
||||
plaintext_len = max_text_command_len(max_encoded_len, mode);
|
||||
g_assert_cmpuint(encoded_len(plaintext_len, mode), <= , max_encoded_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the calculation of length limit for a plaintext in each encryption mode
|
||||
*/
|
||||
static void
|
||||
test_foreach_utf8_data_chunks(void)
|
||||
{
|
||||
GRand *rand = NULL;
|
||||
GString *chunks = NULL;
|
||||
int tests, max_chunks_len, chunks_len;
|
||||
char ascii_message[1001];
|
||||
char *data_chunk = NULL;
|
||||
|
||||
rand = g_rand_new();
|
||||
|
||||
for (tests = 0; tests < 1000; ++tests) {
|
||||
|
||||
max_chunks_len = g_rand_int_range(rand, 2, 301);
|
||||
random_string(ascii_message, 1000);
|
||||
|
||||
data_chunk = ascii_message;
|
||||
|
||||
chunks = g_string_new(NULL);
|
||||
|
||||
while (foreach_utf8_data_chunks(data_chunk, max_chunks_len, &chunks_len)) {
|
||||
g_string_append(chunks, g_strndup(data_chunk, chunks_len));
|
||||
/* Next chunk */
|
||||
data_chunk += chunks_len;
|
||||
}
|
||||
/* Check data loss */
|
||||
g_assert_cmpstr(chunks->str, == , ascii_message);
|
||||
g_string_free(chunks, TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[]) {
|
||||
|
||||
g_test_init(&argc, &argv, NULL);
|
||||
|
||||
g_test_add_func("/fishlim/ecb", test_ecb);
|
||||
g_test_add_func("/fishlim/cbc", test_cbc);
|
||||
g_test_add_func("/fishlim/base64_len", test_base64_len);
|
||||
g_test_add_func("/fishlim/base64_fish_len", test_base64_fish_len);
|
||||
g_test_add_func("/fishlim/base64_ecb_len", test_base64_ecb_len);
|
||||
g_test_add_func("/fishlim/base64_cbc_len", test_base64_cbc_len);
|
||||
g_test_add_func("/fishlim/max_text_command_len", test_max_text_command_len);
|
||||
g_test_add_func("/fishlim/foreach_utf8_data_chunks", test_foreach_utf8_data_chunks);
|
||||
|
||||
return g_test_run();
|
||||
}
|
149
plugins/fishlim/utils.c
Normal file
149
plugins/fishlim/utils.c
Normal file
@ -0,0 +1,149 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2020 <bakasura@protonmail.ch>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
#include "utils.h"
|
||||
#include "fish.h"
|
||||
|
||||
/**
|
||||
* Calculate the length of Base64-encoded string
|
||||
*
|
||||
* @param plaintext_len Size of clear text to encode
|
||||
* @return Size of encoded string
|
||||
*/
|
||||
unsigned long base64_len(size_t plaintext_len) {
|
||||
int length_unpadded = (4 * plaintext_len) / 3;
|
||||
/* Add padding */
|
||||
return length_unpadded % 4 != 0 ? length_unpadded + (4 - length_unpadded % 4) : length_unpadded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of BlowcryptBase64-encoded string
|
||||
*
|
||||
* @param plaintext_len Size of clear text to encode
|
||||
* @return Size of encoded string
|
||||
*/
|
||||
unsigned long base64_fish_len(size_t plaintext_len) {
|
||||
int length_unpadded = (12 * plaintext_len) / 8;
|
||||
/* Add padding */
|
||||
return length_unpadded % 12 != 0 ? length_unpadded + (12 - length_unpadded % 12) : length_unpadded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of fish-encrypted string in CBC mode
|
||||
*
|
||||
* @param plaintext_len Size of clear text to encode
|
||||
* @return Size of encoded string
|
||||
*/
|
||||
unsigned long cbc_len(size_t plaintext_len) {
|
||||
/*IV + DATA + Zero Padding */
|
||||
return base64_len(8 + (plaintext_len % 8 != 0 ? plaintext_len + 8 - (plaintext_len % 8) : plaintext_len));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of fish-encrypted string in ECB mode
|
||||
*
|
||||
* @param plaintext_len Size of clear text to encode
|
||||
* @return Size of encoded string
|
||||
*/
|
||||
unsigned long ecb_len(size_t plaintext_len) {
|
||||
return base64_fish_len(plaintext_len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the length of encrypted string in 'mode' mode
|
||||
*
|
||||
* @param plaintext_len Length of plaintext
|
||||
* @param mode Encryption mode
|
||||
* @return Size of encoded string
|
||||
*/
|
||||
unsigned long encoded_len(size_t plaintext_len, enum fish_mode mode) {
|
||||
switch (mode) {
|
||||
|
||||
case FISH_CBC_MODE:
|
||||
return cbc_len(plaintext_len);
|
||||
break;
|
||||
|
||||
case FISH_ECB_MODE:
|
||||
return ecb_len(plaintext_len);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the maximum length of plaintext for a 'max_len' limit taking care the overload of encryption
|
||||
*
|
||||
* @param max_len Limit for plaintext
|
||||
* @param mode Encryption mode
|
||||
* @return Maximum allowed plaintext length
|
||||
*/
|
||||
int max_text_command_len(size_t max_len, enum fish_mode mode) {
|
||||
int len;
|
||||
|
||||
for (len = max_len; encoded_len(len, mode) > max_len; --len);
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over 'data' in chunks of 'max_chunk_len' taking care the UTF-8 characters
|
||||
*
|
||||
* @param data Data to iterate
|
||||
* @param max_chunk_len Size of biggest chunk
|
||||
* @param [out] chunk_len Current chunk length
|
||||
* @return Pointer to current chunk position or NULL if not have more chunks
|
||||
*/
|
||||
const char *foreach_utf8_data_chunks(const char *data, int max_chunk_len, int *chunk_len) {
|
||||
int data_len, last_chunk_len = 0;
|
||||
|
||||
if (!*data) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Last chunk of data */
|
||||
data_len = strlen(data);
|
||||
if (data_len <= max_chunk_len) {
|
||||
*chunk_len = data_len;
|
||||
return data;
|
||||
}
|
||||
|
||||
*chunk_len = 0;
|
||||
const char *utf8_character = data;
|
||||
|
||||
/* Not valid UTF-8, but maybe valid text, just split into max length */
|
||||
if (!g_utf8_validate(data, -1, NULL)) {
|
||||
*chunk_len = max_chunk_len;
|
||||
return utf8_character;
|
||||
}
|
||||
|
||||
while (*utf8_character && *chunk_len <= max_chunk_len) {
|
||||
last_chunk_len = *chunk_len;
|
||||
*chunk_len = (g_utf8_next_char(utf8_character) - data) * sizeof(*utf8_character);
|
||||
utf8_character = g_utf8_next_char(utf8_character);
|
||||
}
|
||||
|
||||
/* We need the previous length before overflow the limit */
|
||||
*chunk_len = last_chunk_len;
|
||||
|
||||
return utf8_character;
|
||||
}
|
39
plugins/fishlim/utils.h
Normal file
39
plugins/fishlim/utils.h
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2020 <bakasura@protonmail.ch>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
#ifndef PLUGIN_HEXCHAT_FISHLIM_UTILS_H
|
||||
#define PLUGIN_HEXCHAT_FISHLIM_UTILS_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include "fish.h"
|
||||
|
||||
unsigned long base64_len(size_t plaintext_len);
|
||||
unsigned long base64_fish_len(size_t plaintext_len);
|
||||
unsigned long cbc_len(size_t plaintext_len);
|
||||
unsigned long ecb_len(size_t plaintext_len);
|
||||
unsigned long encoded_len(size_t plaintext_len, enum fish_mode mode);
|
||||
int max_text_command_len(size_t max_len, enum fish_mode mode);
|
||||
const char *foreach_utf8_data_chunks(const char *data, int max_chunk_len, int *chunk_len);
|
||||
|
||||
#endif
|
@ -35,6 +35,8 @@
|
||||
|
||||
#include <hexchat-plugin.h>
|
||||
|
||||
#define WORD_ARRAY_LEN 32
|
||||
|
||||
static char plugin_name[] = "Lua";
|
||||
static char plugin_description[] = "Lua scripting interface";
|
||||
static char plugin_version[16] = "1.3";
|
||||
@ -275,13 +277,13 @@ static int api_command_closure(char *word[], char *word_eol[], void *udata)
|
||||
base = lua_gettop(L);
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
|
||||
lua_newtable(L);
|
||||
for(i = 1; i < 32 && *word_eol[i]; i++)
|
||||
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
|
||||
{
|
||||
lua_pushstring(L, word[i]);
|
||||
lua_rawseti(L, -2, i);
|
||||
}
|
||||
lua_newtable(L);
|
||||
for(i = 1; i < 32 && *word_eol[i]; i++)
|
||||
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
|
||||
{
|
||||
lua_pushstring(L, word_eol[i]);
|
||||
lua_rawseti(L, -2, i);
|
||||
@ -462,13 +464,13 @@ static int api_server_closure(char *word[], char *word_eol[], void *udata)
|
||||
base = lua_gettop(L);
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
|
||||
lua_newtable(L);
|
||||
for(i = 1; i < 32 && *word_eol[i]; i++)
|
||||
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
|
||||
{
|
||||
lua_pushstring(L, word[i]);
|
||||
lua_rawseti(L, -2, i);
|
||||
}
|
||||
lua_newtable(L);
|
||||
for(i = 1; i < 32 && *word_eol[i]; i++)
|
||||
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
|
||||
{
|
||||
lua_pushstring(L, word_eol[i]);
|
||||
lua_rawseti(L, -2, i);
|
||||
@ -521,13 +523,13 @@ static int api_server_attrs_closure(char *word[], char *word_eol[], hexchat_even
|
||||
base = lua_gettop(L);
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, info->ref);
|
||||
lua_newtable(L);
|
||||
for(i = 1; i < 32 && *word_eol[i]; i++)
|
||||
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
|
||||
{
|
||||
lua_pushstring(L, word[i]);
|
||||
lua_rawseti(L, -2, i);
|
||||
}
|
||||
lua_newtable(L);
|
||||
for(i = 1; i < 32 && *word_eol[i]; i++)
|
||||
for(i = 1; i < WORD_ARRAY_LEN && *word_eol[i]; i++)
|
||||
{
|
||||
lua_pushstring(L, word_eol[i]);
|
||||
lua_rawseti(L, -2, i);
|
||||
@ -1187,11 +1189,11 @@ static void patch_clibs(lua_State *L)
|
||||
if(lua_type(L, -2) == LUA_TLIGHTUSERDATA && lua_type(L, -1) == LUA_TTABLE)
|
||||
{
|
||||
lua_setfield(L, LUA_REGISTRYINDEX, "_CLIBS");
|
||||
lua_pop(L, 1);
|
||||
break;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
static GPtrArray *scripts;
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
|
@ -26,7 +26,7 @@ if get_option('with-lua') != 'false'
|
||||
subdir('lua')
|
||||
endif
|
||||
|
||||
if get_option('with-perl')
|
||||
if get_option('with-perl') != 'false'
|
||||
subdir('perl')
|
||||
endif
|
||||
|
||||
|
@ -51,7 +51,7 @@ sub get_context;
|
||||
sub HexChat::Internal::context_info;
|
||||
sub HexChat::Internal::print;
|
||||
|
||||
#keep compability with Xchat scripts
|
||||
#keep compatibility with Xchat scripts
|
||||
sub EAT_XCHAT ();
|
||||
BEGIN {
|
||||
*Xchat:: = *HexChat::;
|
||||
|
@ -13,19 +13,24 @@ hexchat_perl_module = custom_target('hexchat-perl-header',
|
||||
command: [generate_perl_header, '@OUTPUT@', '@INPUT@']
|
||||
)
|
||||
|
||||
irc_perl_module = custom_target('irc-perl-header',
|
||||
input: 'lib/IRC.pm',
|
||||
output: 'irc.pm.h',
|
||||
command: [generate_perl_header, '@OUTPUT@', '@INPUT@']
|
||||
)
|
||||
perl_cflags = []
|
||||
irc_perl_module = []
|
||||
|
||||
perl = find_program('perl')
|
||||
if get_option('with-perl-legacy-api')
|
||||
irc_perl_module = custom_target('irc-perl-header',
|
||||
input: 'lib/IRC.pm',
|
||||
output: 'irc.pm.h',
|
||||
command: [generate_perl_header, '@OUTPUT@', '@INPUT@']
|
||||
)
|
||||
perl_cflags += '-DOLD_PERL'
|
||||
endif
|
||||
|
||||
perl = find_program(get_option('with-perl'))
|
||||
|
||||
ret = run_command([perl, '-MExtUtils::Embed', '-e', 'ccopts'])
|
||||
if ret.returncode() != 0
|
||||
error('perl: Failed to get cflags')
|
||||
endif
|
||||
perl_cflags = []
|
||||
foreach flag : ret.stdout().strip().split(' ')
|
||||
if flag.startswith('-I') or flag.startswith('-D')
|
||||
perl_cflags += flag
|
||||
@ -71,11 +76,14 @@ int main(void) {
|
||||
error('found perl not suitable for plugin')
|
||||
endif
|
||||
|
||||
perl_dep = declare_dependency(
|
||||
compile_args: perl_cflags,
|
||||
link_args: perl_ldflags
|
||||
)
|
||||
|
||||
shared_module('perl',
|
||||
sources: ['perl.c', hexchat_perl_module, irc_perl_module],
|
||||
dependencies: [libgio_dep, hexchat_plugin_dep],
|
||||
c_args: perl_cflags,
|
||||
link_args: perl_ldflags,
|
||||
dependencies: [libgio_dep, hexchat_plugin_dep, perl_dep],
|
||||
install: true,
|
||||
install_dir: plugindir,
|
||||
install_rpath: perl_rpath,
|
||||
|
@ -283,6 +283,8 @@ list_item_to_sv ( hexchat_list *list, const char *const *fields )
|
||||
return sv_2mortal (newRV_noinc ((SV *) hash));
|
||||
}
|
||||
|
||||
#define WORD_ARRAY_LEN 32
|
||||
|
||||
static AV *
|
||||
array2av (char *array[])
|
||||
{
|
||||
@ -293,7 +295,7 @@ array2av (char *array[])
|
||||
|
||||
for (
|
||||
count = 1;
|
||||
count < 32 && array[count] != NULL && array[count][0] != 0;
|
||||
count < WORD_ARRAY_LEN && array[count] != NULL && array[count][0] != 0;
|
||||
count++
|
||||
) {
|
||||
temp = newSVpv (array[count], 0);
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
|
386
plugins/python/_hexchat.py
Normal file
386
plugins/python/_hexchat.py
Normal file
@ -0,0 +1,386 @@
|
||||
import inspect
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
||||
from _hexchat_embedded import ffi, lib
|
||||
|
||||
__all__ = [
|
||||
'EAT_ALL', 'EAT_HEXCHAT', 'EAT_NONE', 'EAT_PLUGIN', 'EAT_XCHAT',
|
||||
'PRI_HIGH', 'PRI_HIGHEST', 'PRI_LOW', 'PRI_LOWEST', 'PRI_NORM',
|
||||
'__doc__', '__version__', 'command', 'del_pluginpref', 'emit_print',
|
||||
'find_context', 'get_context', 'get_info',
|
||||
'get_list', 'get_lists', 'get_pluginpref', 'get_prefs', 'hook_command',
|
||||
'hook_print', 'hook_print_attrs', 'hook_server', 'hook_server_attrs',
|
||||
'hook_timer', 'hook_unload', 'list_pluginpref', 'nickcmp', 'prnt',
|
||||
'set_pluginpref', 'strip', 'unhook',
|
||||
]
|
||||
|
||||
__doc__ = 'HexChat Scripting Interface'
|
||||
__version__ = (2, 0)
|
||||
__license__ = 'GPL-2.0+'
|
||||
|
||||
EAT_NONE = 0
|
||||
EAT_HEXCHAT = 1
|
||||
EAT_XCHAT = EAT_HEXCHAT
|
||||
EAT_PLUGIN = 2
|
||||
EAT_ALL = EAT_HEXCHAT | EAT_PLUGIN
|
||||
|
||||
PRI_LOWEST = -128
|
||||
PRI_LOW = -64
|
||||
PRI_NORM = 0
|
||||
PRI_HIGH = 64
|
||||
PRI_HIGHEST = 127
|
||||
|
||||
|
||||
# We need each module to be able to reference their parent plugin
|
||||
# which is a bit tricky since they all share the exact same module.
|
||||
# Simply navigating up to what module called it seems to actually
|
||||
# be a fairly reliable and simple method of doing so if ugly.
|
||||
def __get_current_plugin():
|
||||
frame = inspect.stack()[1][0]
|
||||
while '__plugin' not in frame.f_globals:
|
||||
frame = frame.f_back
|
||||
assert frame is not None
|
||||
|
||||
return frame.f_globals['__plugin']
|
||||
|
||||
|
||||
# Keeping API compat
|
||||
if sys.version_info[0] == 2:
|
||||
def __decode(string):
|
||||
return string
|
||||
|
||||
else:
|
||||
def __decode(string):
|
||||
return string.decode()
|
||||
|
||||
|
||||
# ------------ API ------------
|
||||
def prnt(string):
|
||||
lib.hexchat_print(lib.ph, string.encode())
|
||||
|
||||
|
||||
def emit_print(event_name, *args, **kwargs):
|
||||
time = kwargs.pop('time', 0) # For py2 compat
|
||||
cargs = []
|
||||
for i in range(4):
|
||||
arg = args[i].encode() if len(args) > i else b''
|
||||
cstring = ffi.new('char[]', arg)
|
||||
cargs.append(cstring)
|
||||
|
||||
if time == 0:
|
||||
return lib.hexchat_emit_print(lib.ph, event_name.encode(), *cargs)
|
||||
|
||||
attrs = lib.hexchat_event_attrs_create(lib.ph)
|
||||
attrs.server_time_utc = time
|
||||
ret = lib.hexchat_emit_print_attrs(lib.ph, attrs, event_name.encode(), *cargs)
|
||||
lib.hexchat_event_attrs_free(lib.ph, attrs)
|
||||
return ret
|
||||
|
||||
|
||||
# TODO: this shadows itself. command should be changed to cmd
|
||||
def command(command):
|
||||
lib.hexchat_command(lib.ph, command.encode())
|
||||
|
||||
|
||||
def nickcmp(string1, string2):
|
||||
return lib.hexchat_nickcmp(lib.ph, string1.encode(), string2.encode())
|
||||
|
||||
|
||||
def strip(text, length=-1, flags=3):
|
||||
stripped = lib.hexchat_strip(lib.ph, text.encode(), length, flags)
|
||||
ret = __decode(ffi.string(stripped))
|
||||
lib.hexchat_free(lib.ph, stripped)
|
||||
return ret
|
||||
|
||||
|
||||
def get_info(name):
|
||||
ret = lib.hexchat_get_info(lib.ph, name.encode())
|
||||
if ret == ffi.NULL:
|
||||
return None
|
||||
if name in ('gtkwin_ptr', 'win_ptr'):
|
||||
# Surely there is a less dumb way?
|
||||
ptr = repr(ret).rsplit(' ', 1)[1][:-1]
|
||||
return ptr
|
||||
|
||||
return __decode(ffi.string(ret))
|
||||
|
||||
|
||||
def get_prefs(name):
|
||||
string_out = ffi.new('char**')
|
||||
int_out = ffi.new('int*')
|
||||
_type = lib.hexchat_get_prefs(lib.ph, name.encode(), string_out, int_out)
|
||||
if _type == 0:
|
||||
return None
|
||||
|
||||
if _type == 1:
|
||||
return __decode(ffi.string(string_out[0]))
|
||||
|
||||
if _type in (2, 3): # XXX: 3 should be a bool, but keeps API
|
||||
return int_out[0]
|
||||
|
||||
raise AssertionError('Out of bounds pref storage')
|
||||
|
||||
|
||||
def __cstrarray_to_list(arr):
|
||||
i = 0
|
||||
ret = []
|
||||
while arr[i] != ffi.NULL:
|
||||
ret.append(ffi.string(arr[i]))
|
||||
i += 1
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
__FIELD_CACHE = {}
|
||||
|
||||
|
||||
def __get_fields(name):
|
||||
return __FIELD_CACHE.setdefault(name, __cstrarray_to_list(lib.hexchat_list_fields(lib.ph, name)))
|
||||
|
||||
|
||||
__FIELD_PROPERTY_CACHE = {}
|
||||
|
||||
|
||||
def __cached_decoded_str(string):
|
||||
return __FIELD_PROPERTY_CACHE.setdefault(string, __decode(string))
|
||||
|
||||
|
||||
def get_lists():
|
||||
return [__cached_decoded_str(field) for field in __get_fields(b'lists')]
|
||||
|
||||
|
||||
class ListItem:
|
||||
def __init__(self, name):
|
||||
self._listname = name
|
||||
|
||||
def __repr__(self):
|
||||
return '<{} list item at {}>'.format(self._listname, id(self))
|
||||
|
||||
|
||||
# done this way for speed
|
||||
if sys.version_info[0] == 2:
|
||||
def get_getter(name):
|
||||
return ord(name[0])
|
||||
|
||||
else:
|
||||
def get_getter(name):
|
||||
return name[0]
|
||||
|
||||
|
||||
def get_list(name):
|
||||
# XXX: This function is extremely inefficient and could be interators and
|
||||
# lazily loaded properties, but for API compat we stay slow
|
||||
orig_name = name
|
||||
name = name.encode()
|
||||
|
||||
if name not in __get_fields(b'lists'):
|
||||
raise KeyError('list not available')
|
||||
|
||||
list_ = lib.hexchat_list_get(lib.ph, name)
|
||||
if list_ == ffi.NULL:
|
||||
return None
|
||||
|
||||
ret = []
|
||||
fields = __get_fields(name)
|
||||
|
||||
def string_getter(field):
|
||||
string = lib.hexchat_list_str(lib.ph, list_, field)
|
||||
if string != ffi.NULL:
|
||||
return __decode(ffi.string(string))
|
||||
|
||||
return ''
|
||||
|
||||
def ptr_getter(field):
|
||||
if field == b'context':
|
||||
ptr = lib.hexchat_list_str(lib.ph, list_, field)
|
||||
ctx = ffi.cast('hexchat_context*', ptr)
|
||||
return Context(ctx)
|
||||
|
||||
return None
|
||||
|
||||
getters = {
|
||||
ord('s'): string_getter,
|
||||
ord('i'): lambda field: lib.hexchat_list_int(lib.ph, list_, field),
|
||||
ord('t'): lambda field: lib.hexchat_list_time(lib.ph, list_, field),
|
||||
ord('p'): ptr_getter,
|
||||
}
|
||||
|
||||
while lib.hexchat_list_next(lib.ph, list_) == 1:
|
||||
item = ListItem(orig_name)
|
||||
for _field in fields:
|
||||
getter = getters.get(get_getter(_field))
|
||||
if getter is not None:
|
||||
field_name = _field[1:]
|
||||
setattr(item, __cached_decoded_str(field_name), getter(field_name))
|
||||
|
||||
ret.append(item)
|
||||
|
||||
lib.hexchat_list_free(lib.ph, list_)
|
||||
return ret
|
||||
|
||||
|
||||
# TODO: 'command' here shadows command above, and should be renamed to cmd
|
||||
def hook_command(command, callback, userdata=None, priority=PRI_NORM, help=None):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
handle = lib.hexchat_hook_command(lib.ph, command.encode(), priority, lib._on_command_hook,
|
||||
help.encode() if help is not None else ffi.NULL, hook.handle)
|
||||
|
||||
hook.hexchat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_print(name, callback, userdata=None, priority=PRI_NORM):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
handle = lib.hexchat_hook_print(lib.ph, name.encode(), priority, lib._on_print_hook, hook.handle)
|
||||
hook.hexchat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_print_attrs(name, callback, userdata=None, priority=PRI_NORM):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
handle = lib.hexchat_hook_print_attrs(lib.ph, name.encode(), priority, lib._on_print_attrs_hook, hook.handle)
|
||||
hook.hexchat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_server(name, callback, userdata=None, priority=PRI_NORM):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
handle = lib.hexchat_hook_server(lib.ph, name.encode(), priority, lib._on_server_hook, hook.handle)
|
||||
hook.hexchat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_server_attrs(name, callback, userdata=None, priority=PRI_NORM):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
handle = lib.hexchat_hook_server_attrs(lib.ph, name.encode(), priority, lib._on_server_attrs_hook, hook.handle)
|
||||
hook.hexchat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_timer(timeout, callback, userdata=None):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata)
|
||||
handle = lib.hexchat_hook_timer(lib.ph, timeout, lib._on_timer_hook, hook.handle)
|
||||
hook.hexchat_hook = handle
|
||||
return id(hook)
|
||||
|
||||
|
||||
def hook_unload(callback, userdata=None):
|
||||
plugin = __get_current_plugin()
|
||||
hook = plugin.add_hook(callback, userdata, is_unload=True)
|
||||
return id(hook)
|
||||
|
||||
|
||||
def unhook(handle):
|
||||
plugin = __get_current_plugin()
|
||||
return plugin.remove_hook(handle)
|
||||
|
||||
|
||||
def set_pluginpref(name, value):
|
||||
if isinstance(value, str):
|
||||
return bool(lib.hexchat_pluginpref_set_str(lib.ph, name.encode(), value.encode()))
|
||||
|
||||
if isinstance(value, int):
|
||||
return bool(lib.hexchat_pluginpref_set_int(lib.ph, name.encode(), value))
|
||||
|
||||
# XXX: This should probably raise but this keeps API
|
||||
return False
|
||||
|
||||
|
||||
def get_pluginpref(name):
|
||||
name = name.encode()
|
||||
string_out = ffi.new('char[512]')
|
||||
if lib.hexchat_pluginpref_get_str(lib.ph, name, string_out) != 1:
|
||||
return None
|
||||
|
||||
string = ffi.string(string_out)
|
||||
# This API stores everything as a string so we have to figure out what
|
||||
# its actual type was supposed to be.
|
||||
if len(string) > 12: # Can't be a number
|
||||
return __decode(string)
|
||||
|
||||
number = lib.hexchat_pluginpref_get_int(lib.ph, name)
|
||||
if number == -1 and string != b'-1':
|
||||
return __decode(string)
|
||||
|
||||
return number
|
||||
|
||||
|
||||
def del_pluginpref(name):
|
||||
return bool(lib.hexchat_pluginpref_delete(lib.ph, name.encode()))
|
||||
|
||||
|
||||
def list_pluginpref():
|
||||
prefs_str = ffi.new('char[4096]')
|
||||
if lib.hexchat_pluginpref_list(lib.ph, prefs_str) == 1:
|
||||
return __decode(ffi.string(prefs_str)).split(',')
|
||||
|
||||
return []
|
||||
|
||||
|
||||
class Context:
|
||||
def __init__(self, ctx):
|
||||
self._ctx = ctx
|
||||
|
||||
def __eq__(self, value):
|
||||
if not isinstance(value, Context):
|
||||
return False
|
||||
|
||||
return self._ctx == value._ctx
|
||||
|
||||
@contextmanager
|
||||
def __change_context(self):
|
||||
old_ctx = lib.hexchat_get_context(lib.ph)
|
||||
if not self.set():
|
||||
# XXX: Behavior change, previously used wrong context
|
||||
lib.hexchat_print(lib.ph, b'Context object refers to closed context, ignoring call')
|
||||
return
|
||||
|
||||
yield
|
||||
lib.hexchat_set_context(lib.ph, old_ctx)
|
||||
|
||||
def set(self):
|
||||
# XXX: API addition, C plugin silently ignored failure
|
||||
return bool(lib.hexchat_set_context(lib.ph, self._ctx))
|
||||
|
||||
def prnt(self, string):
|
||||
with self.__change_context():
|
||||
prnt(string)
|
||||
|
||||
def emit_print(self, event_name, *args, **kwargs):
|
||||
time = kwargs.pop('time', 0) # For py2 compat
|
||||
with self.__change_context():
|
||||
return emit_print(event_name, *args, time=time)
|
||||
|
||||
def command(self, string):
|
||||
with self.__change_context():
|
||||
command(string)
|
||||
|
||||
def get_info(self, name):
|
||||
with self.__change_context():
|
||||
return get_info(name)
|
||||
|
||||
def get_list(self, name):
|
||||
with self.__change_context():
|
||||
return get_list(name)
|
||||
|
||||
|
||||
def get_context():
|
||||
ctx = lib.hexchat_get_context(lib.ph)
|
||||
return Context(ctx)
|
||||
|
||||
|
||||
def find_context(server=None, channel=None):
|
||||
server = server.encode() if server is not None else ffi.NULL
|
||||
channel = channel.encode() if channel is not None else ffi.NULL
|
||||
ctx = lib.hexchat_find_context(lib.ph, server, channel)
|
||||
if ctx == ffi.NULL:
|
||||
return None
|
||||
|
||||
return Context(ctx)
|
89
plugins/python/generate_plugin.py
Executable file
89
plugins/python/generate_plugin.py
Executable file
@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
import cffi
|
||||
|
||||
builder = cffi.FFI()
|
||||
|
||||
# hexchat-plugin.h
|
||||
with open(sys.argv[1]) as f:
|
||||
output = []
|
||||
eat_until_endif = 0
|
||||
# This is very specific to hexchat-plugin.h, it is not a cpp
|
||||
for line in f:
|
||||
if line.startswith('#define'):
|
||||
continue
|
||||
elif line.endswith('HEXCHAT_PLUGIN_H\n'):
|
||||
continue
|
||||
elif 'time.h' in line:
|
||||
output.append('typedef int... time_t;')
|
||||
elif line.startswith('#if'):
|
||||
eat_until_endif += 1
|
||||
elif line.startswith('#endif'):
|
||||
eat_until_endif -= 1
|
||||
elif eat_until_endif and '_hexchat_context' not in line:
|
||||
continue
|
||||
else:
|
||||
output.append(line)
|
||||
builder.cdef(''.join(output))
|
||||
|
||||
builder.embedding_api('''
|
||||
extern "Python" int _on_py_command(char **, char **, void *);
|
||||
extern "Python" int _on_load_command(char **, char **, void *);
|
||||
extern "Python" int _on_unload_command(char **, char **, void *);
|
||||
extern "Python" int _on_reload_command(char **, char **, void *);
|
||||
extern "Python" int _on_say_command(char **, char **, void *);
|
||||
|
||||
extern "Python" int _on_command_hook(char **, char **, void *);
|
||||
extern "Python" int _on_print_hook(char **, void *);
|
||||
extern "Python" int _on_print_attrs_hook(char **, hexchat_event_attrs *, void *);
|
||||
extern "Python" int _on_server_hook(char **, char **, void *);
|
||||
extern "Python" int _on_server_attrs_hook(char **, char **, hexchat_event_attrs *, void *);
|
||||
extern "Python" int _on_timer_hook(void *);
|
||||
|
||||
extern "Python" int _on_plugin_init(char **, char **, char **, char *, char *);
|
||||
extern "Python" int _on_plugin_deinit(void);
|
||||
|
||||
static hexchat_plugin *ph;
|
||||
''')
|
||||
|
||||
builder.set_source('_hexchat_embedded', '''
|
||||
/* Python's header defines these.. */
|
||||
#undef HAVE_MEMRCHR
|
||||
#undef HAVE_STRINGS_H
|
||||
|
||||
#include "config.h"
|
||||
#include "hexchat-plugin.h"
|
||||
|
||||
static hexchat_plugin *ph;
|
||||
CFFI_DLLEXPORT int _on_plugin_init(char **, char **, char **, char *, char *);
|
||||
CFFI_DLLEXPORT int _on_plugin_deinit(void);
|
||||
|
||||
int hexchat_plugin_init(hexchat_plugin *plugin_handle,
|
||||
char **name_out, char **description_out,
|
||||
char **version_out, char *arg)
|
||||
{
|
||||
if (ph != NULL)
|
||||
{
|
||||
puts ("Python plugin already loaded\\n");
|
||||
return 0; /* Prevent loading twice */
|
||||
}
|
||||
|
||||
ph = plugin_handle;
|
||||
return _on_plugin_init(name_out, description_out, version_out, arg, HEXCHATLIBDIR);
|
||||
}
|
||||
|
||||
int hexchat_plugin_deinit(void)
|
||||
{
|
||||
int ret = _on_plugin_deinit();
|
||||
ph = NULL;
|
||||
return ret;
|
||||
}
|
||||
''')
|
||||
|
||||
# python.py
|
||||
with open(sys.argv[2]) as f:
|
||||
builder.embedding_init_code(f.read())
|
||||
|
||||
# python.c
|
||||
builder.emit_c_code(sys.argv[3])
|
1
plugins/python/hexchat.py
Normal file
1
plugins/python/hexchat.py
Normal file
@ -0,0 +1 @@
|
||||
from _hexchat import *
|
@ -1,12 +1,30 @@
|
||||
python_opt = get_option('with-python')
|
||||
if python_opt.startswith('python3')
|
||||
python_dep = dependency(python_opt, version: '>= 3.3')
|
||||
# Python 3.8 introduced a new -embed variant
|
||||
if not python_opt.endswith('-embed')
|
||||
python_dep = dependency(python_opt + '-embed', version: '>= 3.3', required: false)
|
||||
if not python_dep.found()
|
||||
python_dep = dependency(python_opt, version: '>= 3.3')
|
||||
endif
|
||||
else
|
||||
python_dep = dependency(python_opt, version: '>= 3.3')
|
||||
endif
|
||||
else
|
||||
python_dep = dependency(python_opt, version: '>= 2.7')
|
||||
endif
|
||||
|
||||
shared_module('python', 'python.c',
|
||||
dependencies: [libgio_dep, hexchat_plugin_dep, python_dep],
|
||||
python3_source = custom_target('python-bindings',
|
||||
input: ['../../src/common/hexchat-plugin.h', 'python.py'],
|
||||
output: 'python.c',
|
||||
command: [find_program('generate_plugin.py'), '@INPUT@', '@OUTPUT@']
|
||||
)
|
||||
|
||||
install_data(['_hexchat.py', 'hexchat.py', 'xchat.py'],
|
||||
install_dir: join_paths(get_option('libdir'), 'hexchat/python')
|
||||
)
|
||||
|
||||
shared_module('python', python3_source,
|
||||
dependencies: [hexchat_plugin_dep, python_dep],
|
||||
install: true,
|
||||
install_dir: plugindir,
|
||||
name_prefix: '',
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
||||
EXPORTS
|
||||
hexchat_plugin_init
|
||||
hexchat_plugin_deinit
|
||||
hexchat_plugin_get_info
|
||||
|
554
plugins/python/python.py
Normal file
554
plugins/python/python.py
Normal file
@ -0,0 +1,554 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import pydoc
|
||||
import signal
|
||||
import sys
|
||||
import traceback
|
||||
import weakref
|
||||
from contextlib import contextmanager
|
||||
|
||||
from _hexchat_embedded import ffi, lib
|
||||
|
||||
if sys.version_info < (3, 0):
|
||||
from io import BytesIO as HelpEater
|
||||
else:
|
||||
from io import StringIO as HelpEater
|
||||
|
||||
if not hasattr(sys, 'argv'):
|
||||
sys.argv = ['<hexchat>']
|
||||
|
||||
VERSION = b'2.0' # Sync with hexchat.__version__
|
||||
PLUGIN_NAME = ffi.new('char[]', b'Python')
|
||||
PLUGIN_DESC = ffi.new('char[]', b'Python %d.%d scripting interface' % (sys.version_info[0], sys.version_info[1]))
|
||||
PLUGIN_VERSION = ffi.new('char[]', VERSION)
|
||||
|
||||
# TODO: Constants should be screaming snake case
|
||||
hexchat = None
|
||||
local_interp = None
|
||||
hexchat_stdout = None
|
||||
plugins = set()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def redirected_stdout():
|
||||
sys.stdout = sys.__stdout__
|
||||
sys.stderr = sys.__stderr__
|
||||
yield
|
||||
sys.stdout = hexchat_stdout
|
||||
sys.stderr = hexchat_stdout
|
||||
|
||||
|
||||
if os.getenv('HEXCHAT_LOG_PYTHON'):
|
||||
def log(*args):
|
||||
with redirected_stdout():
|
||||
print(*args)
|
||||
|
||||
else:
|
||||
def log(*args):
|
||||
pass
|
||||
|
||||
|
||||
class Stdout:
|
||||
def __init__(self):
|
||||
self.buffer = bytearray()
|
||||
|
||||
def write(self, string):
|
||||
string = string.encode()
|
||||
idx = string.rfind(b'\n')
|
||||
if idx != -1:
|
||||
self.buffer += string[:idx]
|
||||
lib.hexchat_print(lib.ph, bytes(self.buffer))
|
||||
self.buffer = bytearray(string[idx + 1:])
|
||||
else:
|
||||
self.buffer += string
|
||||
|
||||
def isatty(self):
|
||||
return False
|
||||
|
||||
|
||||
class Attribute:
|
||||
def __init__(self):
|
||||
self.time = 0
|
||||
|
||||
def __repr__(self):
|
||||
return '<Attribute object at {}>'.format(id(self))
|
||||
|
||||
|
||||
class Hook:
|
||||
def __init__(self, plugin, callback, userdata, is_unload):
|
||||
self.is_unload = is_unload
|
||||
self.plugin = weakref.proxy(plugin)
|
||||
self.callback = callback
|
||||
self.userdata = userdata
|
||||
self.hexchat_hook = None
|
||||
self.handle = ffi.new_handle(weakref.proxy(self))
|
||||
|
||||
def __del__(self):
|
||||
log('Removing hook', id(self))
|
||||
if self.is_unload is False:
|
||||
assert self.hexchat_hook is not None
|
||||
lib.hexchat_unhook(lib.ph, self.hexchat_hook)
|
||||
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
def compile_file(data, filename):
|
||||
return compile(data, filename, 'exec', dont_inherit=True)
|
||||
|
||||
|
||||
def compile_line(string):
|
||||
try:
|
||||
return compile(string, '<string>', 'eval', dont_inherit=True)
|
||||
|
||||
except SyntaxError:
|
||||
# For some reason `print` is invalid for eval
|
||||
# This will hide any return value though
|
||||
return compile(string, '<string>', 'exec', dont_inherit=True)
|
||||
else:
|
||||
def compile_file(data, filename):
|
||||
return compile(data, filename, 'exec', optimize=2, dont_inherit=True)
|
||||
|
||||
|
||||
def compile_line(string):
|
||||
# newline appended to solve unexpected EOF issues
|
||||
return compile(string + '\n', '<string>', 'single', optimize=2, dont_inherit=True)
|
||||
|
||||
|
||||
class Plugin:
|
||||
def __init__(self):
|
||||
self.ph = None
|
||||
self.name = ''
|
||||
self.filename = ''
|
||||
self.version = ''
|
||||
self.description = ''
|
||||
self.hooks = set()
|
||||
self.globals = {
|
||||
'__plugin': weakref.proxy(self),
|
||||
'__name__': '__main__',
|
||||
}
|
||||
|
||||
def add_hook(self, callback, userdata, is_unload=False):
|
||||
hook = Hook(self, callback, userdata, is_unload=is_unload)
|
||||
self.hooks.add(hook)
|
||||
return hook
|
||||
|
||||
def remove_hook(self, hook):
|
||||
for h in self.hooks:
|
||||
if id(h) == hook:
|
||||
ud = h.userdata
|
||||
self.hooks.remove(h)
|
||||
return ud
|
||||
|
||||
log('Hook not found')
|
||||
return None
|
||||
|
||||
def loadfile(self, filename):
|
||||
try:
|
||||
self.filename = filename
|
||||
with open(filename) as f:
|
||||
data = f.read()
|
||||
compiled = compile_file(data, filename)
|
||||
exec(compiled, self.globals)
|
||||
|
||||
try:
|
||||
self.name = self.globals['__module_name__']
|
||||
|
||||
except KeyError:
|
||||
lib.hexchat_print(lib.ph, b'Failed to load module: __module_name__ must be set')
|
||||
|
||||
return False
|
||||
|
||||
self.version = self.globals.get('__module_version__', '')
|
||||
self.description = self.globals.get('__module_description__', '')
|
||||
self.ph = lib.hexchat_plugingui_add(lib.ph, filename.encode(), self.name.encode(),
|
||||
self.description.encode(), self.version.encode(), ffi.NULL)
|
||||
|
||||
except Exception as e:
|
||||
lib.hexchat_print(lib.ph, 'Failed to load module: {}'.format(e).encode())
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __del__(self):
|
||||
log('unloading', self.filename)
|
||||
for hook in self.hooks:
|
||||
if hook.is_unload is True:
|
||||
try:
|
||||
hook.callback(hook.userdata)
|
||||
|
||||
except Exception as e:
|
||||
log('Failed to run hook:', e)
|
||||
traceback.print_exc()
|
||||
|
||||
del self.hooks
|
||||
if self.ph is not None:
|
||||
lib.hexchat_plugingui_remove(lib.ph, self.ph)
|
||||
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
def __decode(string):
|
||||
return string
|
||||
|
||||
else:
|
||||
def __decode(string):
|
||||
return string.decode()
|
||||
|
||||
|
||||
# There can be empty entries between non-empty ones so find the actual last value
|
||||
def wordlist_len(words):
|
||||
for i in range(31, 0, -1):
|
||||
if ffi.string(words[i]):
|
||||
return i
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def create_wordlist(words):
|
||||
size = wordlist_len(words)
|
||||
return [__decode(ffi.string(words[i])) for i in range(1, size + 1)]
|
||||
|
||||
|
||||
# This function only exists for compat reasons with the C plugin
|
||||
# It turns the word list from print hooks into a word_eol list
|
||||
# This makes no sense to do...
|
||||
def create_wordeollist(words):
|
||||
words = reversed(words)
|
||||
accum = None
|
||||
ret = []
|
||||
for word in words:
|
||||
if accum is None:
|
||||
accum = word
|
||||
|
||||
elif word:
|
||||
last = accum
|
||||
accum = ' '.join((word, last))
|
||||
|
||||
ret.insert(0, accum)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def to_cb_ret(value):
|
||||
if value is None:
|
||||
return 0
|
||||
|
||||
return int(value)
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_command_hook(word, word_eol, userdata):
|
||||
hook = ffi.from_handle(userdata)
|
||||
word = create_wordlist(word)
|
||||
word_eol = create_wordlist(word_eol)
|
||||
return to_cb_ret(hook.callback(word, word_eol, hook.userdata))
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_print_hook(word, userdata):
|
||||
hook = ffi.from_handle(userdata)
|
||||
word = create_wordlist(word)
|
||||
word_eol = create_wordeollist(word)
|
||||
return to_cb_ret(hook.callback(word, word_eol, hook.userdata))
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_print_attrs_hook(word, attrs, userdata):
|
||||
hook = ffi.from_handle(userdata)
|
||||
word = create_wordlist(word)
|
||||
word_eol = create_wordeollist(word)
|
||||
attr = Attribute()
|
||||
attr.time = attrs.server_time_utc
|
||||
return to_cb_ret(hook.callback(word, word_eol, hook.userdata, attr))
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_server_hook(word, word_eol, userdata):
|
||||
hook = ffi.from_handle(userdata)
|
||||
word = create_wordlist(word)
|
||||
word_eol = create_wordlist(word_eol)
|
||||
return to_cb_ret(hook.callback(word, word_eol, hook.userdata))
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_server_attrs_hook(word, word_eol, attrs, userdata):
|
||||
hook = ffi.from_handle(userdata)
|
||||
word = create_wordlist(word)
|
||||
word_eol = create_wordlist(word_eol)
|
||||
attr = Attribute()
|
||||
attr.time = attrs.server_time_utc
|
||||
return to_cb_ret(hook.callback(word, word_eol, hook.userdata, attr))
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_timer_hook(userdata):
|
||||
hook = ffi.from_handle(userdata)
|
||||
if hook.callback(hook.userdata) is True:
|
||||
return 1
|
||||
|
||||
hook.is_unload = True # Don't unhook
|
||||
for h in hook.plugin.hooks:
|
||||
if h == hook:
|
||||
hook.plugin.hooks.remove(h)
|
||||
break
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@ffi.def_extern(error=3)
|
||||
def _on_say_command(word, word_eol, userdata):
|
||||
channel = ffi.string(lib.hexchat_get_info(lib.ph, b'channel'))
|
||||
if channel == b'>>python<<':
|
||||
python = ffi.string(word_eol[1])
|
||||
lib.hexchat_print(lib.ph, b'>>> ' + python)
|
||||
exec_in_interp(__decode(python))
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def load_filename(filename):
|
||||
filename = os.path.expanduser(filename)
|
||||
if not os.path.isabs(filename):
|
||||
configdir = __decode(ffi.string(lib.hexchat_get_info(lib.ph, b'configdir')))
|
||||
|
||||
filename = os.path.join(configdir, 'addons', filename)
|
||||
|
||||
if filename and not any(plugin.filename == filename for plugin in plugins):
|
||||
plugin = Plugin()
|
||||
if plugin.loadfile(filename):
|
||||
plugins.add(plugin)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def unload_name(name):
|
||||
if name:
|
||||
for plugin in plugins:
|
||||
if name in (plugin.name, plugin.filename, os.path.basename(plugin.filename)):
|
||||
plugins.remove(plugin)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def reload_name(name):
|
||||
if name:
|
||||
for plugin in plugins:
|
||||
if name in (plugin.name, plugin.filename, os.path.basename(plugin.filename)):
|
||||
filename = plugin.filename
|
||||
plugins.remove(plugin)
|
||||
return load_filename(filename)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@contextmanager
|
||||
def change_cwd(path):
|
||||
old_cwd = os.getcwd()
|
||||
os.chdir(path)
|
||||
yield
|
||||
os.chdir(old_cwd)
|
||||
|
||||
|
||||
def autoload():
|
||||
configdir = __decode(ffi.string(lib.hexchat_get_info(lib.ph, b'configdir')))
|
||||
addondir = os.path.join(configdir, 'addons')
|
||||
try:
|
||||
with change_cwd(addondir): # Maintaining old behavior
|
||||
for f in os.listdir(addondir):
|
||||
if f.endswith('.py'):
|
||||
log('Autoloading', f)
|
||||
# TODO: Set cwd
|
||||
load_filename(os.path.join(addondir, f))
|
||||
|
||||
except FileNotFoundError as e:
|
||||
log('Autoload failed', e)
|
||||
|
||||
|
||||
def list_plugins():
|
||||
if not plugins:
|
||||
lib.hexchat_print(lib.ph, b'No python modules loaded')
|
||||
return
|
||||
|
||||
tbl_headers = [b'Name', b'Version', b'Filename', b'Description']
|
||||
tbl = [
|
||||
tbl_headers,
|
||||
[(b'-' * len(s)) for s in tbl_headers]
|
||||
]
|
||||
|
||||
for plugin in plugins:
|
||||
basename = os.path.basename(plugin.filename).encode()
|
||||
name = plugin.name.encode()
|
||||
version = plugin.version.encode() if plugin.version else b'<none>'
|
||||
description = plugin.description.encode() if plugin.description else b'<none>'
|
||||
tbl.append((name, version, basename, description))
|
||||
|
||||
column_sizes = [
|
||||
max(len(item) for item in column)
|
||||
for column in zip(*tbl)
|
||||
]
|
||||
|
||||
for row in tbl:
|
||||
lib.hexchat_print(lib.ph, b' '.join(item.ljust(column_sizes[i])
|
||||
for i, item in enumerate(row)))
|
||||
lib.hexchat_print(lib.ph, b'')
|
||||
|
||||
|
||||
def exec_in_interp(python):
|
||||
global local_interp
|
||||
|
||||
if not python:
|
||||
return
|
||||
|
||||
if local_interp is None:
|
||||
local_interp = Plugin()
|
||||
local_interp.locals = {}
|
||||
local_interp.globals['hexchat'] = hexchat
|
||||
|
||||
code = compile_line(python)
|
||||
try:
|
||||
ret = eval(code, local_interp.globals, local_interp.locals)
|
||||
if ret is not None:
|
||||
lib.hexchat_print(lib.ph, '{}'.format(ret).encode())
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc(file=hexchat_stdout)
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_load_command(word, word_eol, userdata):
|
||||
filename = ffi.string(word[2])
|
||||
if filename.endswith(b'.py'):
|
||||
load_filename(__decode(filename))
|
||||
return 3
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_unload_command(word, word_eol, userdata):
|
||||
filename = ffi.string(word[2])
|
||||
if filename.endswith(b'.py'):
|
||||
unload_name(__decode(filename))
|
||||
return 3
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_reload_command(word, word_eol, userdata):
|
||||
filename = ffi.string(word[2])
|
||||
if filename.endswith(b'.py'):
|
||||
reload_name(__decode(filename))
|
||||
return 3
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@ffi.def_extern(error=3)
|
||||
def _on_py_command(word, word_eol, userdata):
|
||||
subcmd = __decode(ffi.string(word[2])).lower()
|
||||
|
||||
if subcmd == 'exec':
|
||||
python = __decode(ffi.string(word_eol[3]))
|
||||
exec_in_interp(python)
|
||||
|
||||
elif subcmd == 'load':
|
||||
filename = __decode(ffi.string(word[3]))
|
||||
load_filename(filename)
|
||||
|
||||
elif subcmd == 'unload':
|
||||
name = __decode(ffi.string(word[3]))
|
||||
if not unload_name(name):
|
||||
lib.hexchat_print(lib.ph, b'Can\'t find a python plugin with that name')
|
||||
|
||||
elif subcmd == 'reload':
|
||||
name = __decode(ffi.string(word[3]))
|
||||
if not reload_name(name):
|
||||
lib.hexchat_print(lib.ph, b'Can\'t find a python plugin with that name')
|
||||
|
||||
elif subcmd == 'console':
|
||||
lib.hexchat_command(lib.ph, b'QUERY >>python<<')
|
||||
|
||||
elif subcmd == 'list':
|
||||
list_plugins()
|
||||
|
||||
elif subcmd == 'about':
|
||||
lib.hexchat_print(lib.ph, b'HexChat Python interface version ' + VERSION)
|
||||
|
||||
else:
|
||||
lib.hexchat_command(lib.ph, b'HELP PY')
|
||||
|
||||
return 3
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_plugin_init(plugin_name, plugin_desc, plugin_version, arg, libdir):
|
||||
global hexchat
|
||||
global hexchat_stdout
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
plugin_name[0] = PLUGIN_NAME
|
||||
plugin_desc[0] = PLUGIN_DESC
|
||||
plugin_version[0] = PLUGIN_VERSION
|
||||
|
||||
try:
|
||||
libdir = __decode(ffi.string(libdir))
|
||||
modpath = os.path.join(libdir, '..', 'python')
|
||||
sys.path.append(os.path.abspath(modpath))
|
||||
hexchat = importlib.import_module('hexchat')
|
||||
|
||||
except (UnicodeDecodeError, ImportError) as e:
|
||||
lib.hexchat_print(lib.ph, b'Failed to import module: ' + repr(e).encode())
|
||||
|
||||
return 0
|
||||
|
||||
hexchat_stdout = Stdout()
|
||||
sys.stdout = hexchat_stdout
|
||||
sys.stderr = hexchat_stdout
|
||||
pydoc.help = pydoc.Helper(HelpEater(), HelpEater())
|
||||
|
||||
lib.hexchat_hook_command(lib.ph, b'', 0, lib._on_say_command, ffi.NULL, ffi.NULL)
|
||||
lib.hexchat_hook_command(lib.ph, b'LOAD', 0, lib._on_load_command, ffi.NULL, ffi.NULL)
|
||||
lib.hexchat_hook_command(lib.ph, b'UNLOAD', 0, lib._on_unload_command, ffi.NULL, ffi.NULL)
|
||||
lib.hexchat_hook_command(lib.ph, b'RELOAD', 0, lib._on_reload_command, ffi.NULL, ffi.NULL)
|
||||
lib.hexchat_hook_command(lib.ph, b'PY', 0, lib._on_py_command, b'''Usage: /PY LOAD <filename>
|
||||
UNLOAD <filename|name>
|
||||
RELOAD <filename|name>
|
||||
LIST
|
||||
EXEC <command>
|
||||
CONSOLE
|
||||
ABOUT''', ffi.NULL)
|
||||
|
||||
lib.hexchat_print(lib.ph, b'Python interface loaded')
|
||||
autoload()
|
||||
return 1
|
||||
|
||||
|
||||
@ffi.def_extern()
|
||||
def _on_plugin_deinit():
|
||||
global local_interp
|
||||
global hexchat
|
||||
global hexchat_stdout
|
||||
global plugins
|
||||
|
||||
plugins = set()
|
||||
local_interp = None
|
||||
hexchat = None
|
||||
hexchat_stdout = None
|
||||
sys.stdout = sys.__stdout__
|
||||
sys.stderr = sys.__stderr__
|
||||
pydoc.help = pydoc.Helper()
|
||||
|
||||
for mod in ('_hexchat', 'hexchat', 'xchat', '_hexchat_embedded'):
|
||||
try:
|
||||
del sys.modules[mod]
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return 1
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
@ -37,6 +37,9 @@
|
||||
<AdditionalDependencies>"$(Python2Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python2Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>"$(Python3Path)\python.exe" generate_plugin.py ..\..\src\common\hexchat-plugin.h python.py "$(IntDir)python.c"</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
@ -48,12 +51,15 @@
|
||||
<AdditionalDependencies>"$(Python2Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python2Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>"$(Python3Path)\python.exe" generate_plugin.py ..\..\src\common\hexchat-plugin.h python.py "$(IntDir)python.c"</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<None Include="python.def" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="python.c" />
|
||||
<ClCompile Include="$(IntDir)python.c" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
@ -37,6 +37,9 @@
|
||||
<AdditionalDependencies>"$(Python3Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python3Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>"$(Python3Path)\python.exe" generate_plugin.py ..\..\src\common\hexchat-plugin.h python.py "$(IntDir)python.c"</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
@ -48,12 +51,20 @@
|
||||
<AdditionalDependencies>"$(Python3Lib).lib";$(DepLibs);%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalLibraryDirectories>$(DepsRoot)\lib;$(Python3Path)\libs;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<Command>"$(Python3Path)\python.exe" generate_plugin.py ..\..\src\common\hexchat-plugin.h python.py "$(IntDir)python.c"</Command>
|
||||
</PreBuildEvent>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<None Include="generate_plugin.py" />
|
||||
<None Include="hexchat.py" />
|
||||
<None Include="python.def" />
|
||||
<None Include="python.py" />
|
||||
<None Include="xchat.py" />
|
||||
<None Include="_hexchat.py" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="python.c" />
|
||||
<ClCompile Include="$(IntDir)python.c" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
</Project>
|
@ -9,13 +9,26 @@
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="python.c">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="$(IntDir)python.c" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="python.def">
|
||||
<Filter>Resource Files</Filter>
|
||||
</None>
|
||||
<None Include="_hexchat.py">
|
||||
<Filter>Source Files</Filter>
|
||||
</None>
|
||||
<None Include="generate_plugin.py">
|
||||
<Filter>Source Files</Filter>
|
||||
</None>
|
||||
<None Include="hexchat.py">
|
||||
<Filter>Source Files</Filter>
|
||||
</None>
|
||||
<None Include="python.py">
|
||||
<Filter>Source Files</Filter>
|
||||
</None>
|
||||
<None Include="xchat.py">
|
||||
<Filter>Source Files</Filter>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
26
plugins/python/python_style_guide.md
Normal file
26
plugins/python/python_style_guide.md
Normal file
@ -0,0 +1,26 @@
|
||||
# HexChat Python Module Style Guide
|
||||
|
||||
(This is a work in progress).
|
||||
|
||||
## General rules
|
||||
|
||||
- PEP8 as general fallback recommendations
|
||||
- Max line length: 120
|
||||
- Avoid overcomplex compound statements. i.e. dont do this: `somevar = x if x == y else z if a == b and c == b else x`
|
||||
|
||||
## Indentation style
|
||||
|
||||
### Multi-line functions
|
||||
|
||||
```python
|
||||
foo(really_long_arg_1,
|
||||
really_long_arg_2)
|
||||
```
|
||||
|
||||
### Mutli-line lists/dicts
|
||||
|
||||
```python
|
||||
foo = {
|
||||
'bar': 'baz',
|
||||
}
|
||||
```
|
1
plugins/python/xchat.py
Normal file
1
plugins/python/xchat.py
Normal file
@ -0,0 +1 @@
|
||||
from _hexchat import *
|
@ -13,13 +13,13 @@ sysinfo_includes = []
|
||||
sysinfo_cargs = []
|
||||
|
||||
system = host_machine.system()
|
||||
if system == 'linux' or system == 'darwin'
|
||||
if system == 'linux' or system == 'gnu' or system.startswith('gnu/') or system == 'darwin' or system == 'freebsd'
|
||||
sysinfo_includes += 'shared'
|
||||
sysinfo_sources += [
|
||||
'shared/df.c'
|
||||
]
|
||||
|
||||
if system == 'linux'
|
||||
if system == 'linux' or system == 'gnu' or system.startswith('gnu/') or system == 'freebsd'
|
||||
libpci = dependency('libpci', required: false, method: 'pkg-config')
|
||||
if libpci.found()
|
||||
sysinfo_deps += libpci
|
||||
|
@ -26,7 +26,7 @@ int xs_parse_df(gint64 *out_total, gint64 *out_free)
|
||||
FILE *pipe;
|
||||
char buffer[bsize];
|
||||
|
||||
pipe = popen("df -k -l -P", "r");
|
||||
pipe = popen("df -k -l -P --exclude-type=squashfs --exclude-type=devtmpfs --exclude-type=tmpfs", "r");
|
||||
if(pipe==NULL)
|
||||
return 1;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
|
@ -269,6 +269,16 @@ int xs_parse_meminfo(unsigned long long *mem_tot, unsigned long long *mem_free,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void strip_quotes(char *string)
|
||||
{
|
||||
size_t len = strlen(string);
|
||||
if (string[len - 1] == '"')
|
||||
string[--len] = '\0';
|
||||
|
||||
if (string[0] == '"')
|
||||
memmove(string, string + 1, len);
|
||||
}
|
||||
|
||||
int xs_parse_distro(char *name)
|
||||
{
|
||||
FILE *fp = NULL;
|
||||
@ -320,6 +330,20 @@ int xs_parse_distro(char *name)
|
||||
else
|
||||
g_snprintf(buffer, bsize, "Gentoo Linux %s", keywords);
|
||||
}
|
||||
else if((fp = fopen("/etc/os-release", "r")) != NULL)
|
||||
{
|
||||
char name[bsize], version[bsize];
|
||||
strcpy(name, "?");
|
||||
strcpy(version, "?");
|
||||
while(fgets(buffer, bsize, fp) != NULL)
|
||||
{
|
||||
find_match_char(buffer, "NAME=", name);
|
||||
find_match_char(buffer, "VERSION=", version);
|
||||
}
|
||||
strip_quotes(name);
|
||||
strip_quotes(version);
|
||||
g_snprintf(buffer, bsize, "%s %s", name, version);
|
||||
}
|
||||
else
|
||||
g_snprintf(buffer, bsize, "Unknown Distro");
|
||||
if(fp != NULL)
|
||||
|
@ -142,7 +142,7 @@ void pci_find_fullname(char *fullname, char *vendor, char *device)
|
||||
{
|
||||
position = strstr(buffer, vendor);
|
||||
position += 6;
|
||||
strncpy(vendorname, position, bsize/2);
|
||||
g_strlcpy(vendorname, position, sizeof (vendorname));
|
||||
position = strstr(vendorname, "\n");
|
||||
*(position) = '\0';
|
||||
break;
|
||||
@ -154,7 +154,7 @@ void pci_find_fullname(char *fullname, char *vendor, char *device)
|
||||
{
|
||||
position = strstr(buffer, device);
|
||||
position += 6;
|
||||
strncpy(devicename, position, bsize/2);
|
||||
g_strlcpy(devicename, position, sizeof (devicename));
|
||||
position = strstr(devicename, " (");
|
||||
if (position == NULL)
|
||||
position = strstr(devicename, "\n");
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Label="Configuration">
|
||||
<PlatformToolset>v140</PlatformToolset>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
|
636
po/en_GB.po
636
po/en_GB.po
File diff suppressed because it is too large
Load Diff
684
po/ja_JP.po
684
po/ja_JP.po
File diff suppressed because it is too large
Load Diff
@ -1 +1,9 @@
|
||||
i18n.gettext('hexchat', preset: 'glib')
|
||||
|
||||
validate_translations = find_program('validate-textevent-translations')
|
||||
test('Validate translations', validate_translations,
|
||||
args: [
|
||||
join_paths(meson.current_source_dir(), 'LINGUAS'),
|
||||
meson.current_source_dir(),
|
||||
]
|
||||
)
|
||||
|
839
po/pt_BR.po
839
po/pt_BR.po
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user