2
0
mirror of https://github.com/koreader/koreader synced 2024-11-10 01:10:34 +00:00
koreader/frontend/ui/widget/networksetting.lua
Wim de With 17a4aa962f
Fix connection bug with non-ASCII SSIDs in wpa_supplicant (#11089)
* Bump base

includes:

koreader/koreader-base#1691
koreader/koreader-base#1692
koreader/koreader-base#1689
koreader/koreader-base#1690
koreader/koreader-base#1693

* Integrate decoding of SSIDs within wpa_supplicant

The UTF-8 decoding of SSIDs is specific to wpa_supplicant. In this
patch, we move all of this decoding logic to the wpa_supplicant module.
We expose the raw bytes of the SSID to the NetworkMgr code, and make
sure to always fix bad UTF-8 before we display the SSID to the user.

Within the wpa_supplicant module, we replace the call to the
wpa_passphrase binary to get the PSK with a direct function call to
OpenSSL. This allows us to calculate the PSK over any arbitrary bytes,
including UTF-8. In the same vein, we use the hex-encoded SSID to
communicate with wpa_supplicant when setting up the network to support
arbitrary bytes in the SSID.

Unfortunately, we also remove the tests, as there is no way to unit test
local functions.
2023-11-09 21:08:26 +01:00

520 lines
16 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--[[--
Network setting widget.
Example:
local network_list = {
{
ssid = "foo",
signal_level = -58,
flags = "[WPA2-PSK-CCMP][ESS]",
signal_quality = 84,
password = "123abc",
connected = true,
},
{
ssid = "bar",
signal_level = -258,
signal_quality = 44,
flags = "[WEP][ESS]",
},
}
UIManager:show(require("ui/widget/networksetting"):new{
network_list = network_list,
connect_callback = function()
-- connect_callback will be called when a *connect* (NOT disconnect)
-- attempt has been successful.
-- You can update UI widgets in the callback.
end,
disconnect_callback = function()
-- This one will fire unconditionally after a disconnect attempt.
end,
})
]]
local BD = require("ui/bidi")
local bit = require("bit")
local Blitbuffer = require("ffi/blitbuffer")
local CenterContainer = require("ui/widget/container/centercontainer")
local Device = require("device")
local Font = require("ui/font")
local Geom = require("ui/geometry")
local FrameContainer = require("ui/widget/container/framecontainer")
local GestureRange = require("ui/gesturerange")
local HorizontalGroup = require("ui/widget/horizontalgroup")
local HorizontalSpan = require("ui/widget/horizontalspan")
local IconWidget = require("ui/widget/iconwidget")
local InfoMessage = require("ui/widget/infomessage")
local InputContainer = require("ui/widget/container/inputcontainer")
local InputDialog = require("ui/widget/inputdialog")
local LeftContainer = require("ui/widget/container/leftcontainer")
local ListView = require("ui/widget/listview")
local RightContainer = require("ui/widget/container/rightcontainer")
local NetworkMgr = require("ui/network/manager")
local OverlapGroup = require("ui/widget/overlapgroup")
local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
local UIManager = require("ui/uimanager")
local util = require("util")
local VerticalGroup = require("ui/widget/verticalgroup")
local Widget = require("ui/widget/widget")
local _ = require("gettext")
local T = require("ffi/util").template
local Screen = Device.screen
local band = bit.band
local function obtainIP()
--- @todo check for DHCP result
local info = InfoMessage:new{text = _("Obtaining IP address…")}
UIManager:show(info)
UIManager:forceRePaint()
NetworkMgr:obtainIP()
UIManager:close(info)
end
local MinimalPaginator = Widget:extend{
width = nil,
height = nil,
progress = nil,
}
function MinimalPaginator:getSize()
return Geom:new{w = self.width, h = self.height}
end
function MinimalPaginator:paintTo(bb, x, y)
self.dimen = self:getSize()
self.dimen.x, self.dimen.y = x, y
-- paint background
bb:paintRoundedRect(x, y,
self.dimen.w, self.dimen.h,
Blitbuffer.COLOR_LIGHT_GRAY)
-- paint percentage infill
bb:paintRect(x, y,
math.ceil(self.dimen.w*self.progress), self.dimen.h,
Blitbuffer.COLOR_DARK_GRAY)
end
function MinimalPaginator:setProgress(progress) self.progress = progress end
local NetworkItem = InputContainer:extend{
dimen = nil,
height = Screen:scaleBySize(44),
icon_size = Screen:scaleBySize(32),
width = nil,
info = nil,
display_ssid = nil,
background = Blitbuffer.COLOR_WHITE,
}
function NetworkItem:init()
self.dimen = Geom:new{x = 0, y = 0, w = self.width, h = self.height}
if not self.info.ssid then
self.info.ssid = "[hidden]"
end
self.display_ssid = util.fixUtf8(self.info.ssid, "<EFBFBD>")
local wifi_icon
if string.find(self.info.flags, "WPA") then
wifi_icon = "wifi.secure.%d"
else
wifi_icon = "wifi.open.%d"
end
-- Based on NetworkManager's nmc_wifi_strength_bars
-- c.f., https://github.com/NetworkManager/NetworkManager/blob/2fa8ef9fb9c7fe0cc2d9523eed6c5a3749b05175/clients/common/nm-client-utils.c#L585-L612
if self.info.signal_quality > 80 then
wifi_icon = string.format(wifi_icon, 100)
elseif self.info.signal_quality > 55 then
wifi_icon = string.format(wifi_icon, 75)
elseif self.info.signal_quality > 30 then
wifi_icon = string.format(wifi_icon, 50)
elseif self.info.signal_quality > 5 then
wifi_icon = string.format(wifi_icon, 25)
else
wifi_icon = string.format(wifi_icon, 0)
end
local horizontal_space = HorizontalSpan:new{width = Size.span.horizontal_default}
self.content_container = OverlapGroup:new{
dimen = self.dimen:copy(),
LeftContainer:new{
dimen = self.dimen:copy(),
HorizontalGroup:new{
horizontal_space,
IconWidget:new{
icon = wifi_icon,
width = self.icon_size,
height = self.icon_size,
},
horizontal_space,
TextWidget:new{
text = self.display_ssid,
face = Font:getFace("cfont"),
},
},
}
}
self.btn_disconnect = nil
self.btn_edit_nw = nil
if self.info.connected then
self.btn_disconnect = FrameContainer:new{
bordersize = 0,
padding = 0,
TextWidget:new{
text = _("disconnect"),
face = Font:getFace("cfont"),
}
}
table.insert(self.content_container, RightContainer:new{
dimen = self.dimen:copy(),
HorizontalGroup:new{
self.btn_disconnect,
horizontal_space,
}
})
self.setting_ui:setConnectedItem(self)
elseif self.info.password then
self.btn_edit_nw = FrameContainer:new{
bordersize = 0,
padding = 0,
TextWidget:new{
text = _("edit"),
face = Font:getFace("cfont"),
}
}
table.insert(self.content_container, RightContainer:new{
dimen = self.dimen:copy(),
HorizontalGroup:new{
self.btn_edit_nw,
horizontal_space,
}
})
end
self[1] = FrameContainer:new{
padding = 0,
margin = 0,
background = self.background,
bordersize = 0,
width = self.width,
self.content_container,
}
if Device:isTouchDevice() then
self.ges_events.TapSelect = {
GestureRange:new{
ges = "tap",
range = self.dimen,
}
}
end
end
function NetworkItem:refresh()
self:init()
UIManager:setDirty(self.setting_ui, function() return "ui", self.dimen end)
end
function NetworkItem:connect()
local connected_item = self.setting_ui:getConnectedItem()
if connected_item then connected_item:disconnect() end
local success, err_msg = NetworkMgr:authenticateNetwork(self.info)
local text
if success then
obtainIP()
self.info.connected = true
self.setting_ui:setConnectedItem(self)
text = _("Connected.")
else
text = err_msg
end
-- Do what it says on the tin, and only trigger the connect_callback on a *successful* connect.
-- NOTE: This callback comes from NetworkManager, where it's named complete_callback.
if success and self.setting_ui.connect_callback then
self.setting_ui.connect_callback()
end
self:refresh()
UIManager:show(InfoMessage:new{text = text, timeout = 3})
end
function NetworkItem:disconnect()
local info = InfoMessage:new{text = _("Disconnecting…")}
UIManager:show(info)
UIManager:forceRePaint()
NetworkMgr:disconnectNetwork(self.info)
NetworkMgr:releaseIP()
UIManager:close(info)
self.info.connected = nil
self:refresh()
self.setting_ui:setConnectedItem(nil)
if self.setting_ui.disconnect_callback then
self.setting_ui.disconnect_callback()
end
end
function NetworkItem:saveAndConnectToNetwork(password_input)
local new_passwd = password_input:getInputText()
-- Dont set a empty password if WPA encryption, go through if its an open AP
if (new_passwd == nil or #new_passwd == 0) and string.find(self.info.flags, "WPA") then
UIManager:show(InfoMessage:new{
text = _("Password cannot be empty."),
})
else
if new_passwd ~= self.info.password then
self.info.password = new_passwd
self.info.psk = nil
NetworkMgr:saveNetwork(self.info)
end
self:connect()
end
UIManager:close(password_input)
end
function NetworkItem:onEditNetwork()
local password_input
password_input = InputDialog:new{
title = self.display_ssid,
input = self.info.password,
input_hint = _("password (leave empty for open networks)"),
input_type = "text",
text_type = "password",
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(password_input)
end,
},
{
text = _("Forget"),
callback = function()
NetworkMgr:deleteNetwork(self.info)
self.info.password = nil
-- remove edit button
table.remove(self.content_container, 2)
UIManager:close(password_input)
self:refresh()
end,
},
{
text = _("Connect"),
is_enter_default = true,
callback = function()
self:saveAndConnectToNetwork(password_input)
end,
},
},
},
}
UIManager:show(password_input)
password_input:onShowKeyboard()
return true
end
function NetworkItem:onAddNetwork()
local password_input
password_input = InputDialog:new{
title = self.display_ssid,
input = "",
input_hint = _("password (leave empty for open networks)"),
input_type = "text",
text_type = "password",
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(password_input)
end,
},
{
text = _("Connect"),
is_enter_default = true,
callback = function()
self:saveAndConnectToNetwork(password_input)
end,
},
},
},
}
UIManager:show(password_input)
password_input:onShowKeyboard()
return true
end
function NetworkItem:onTapSelect(arg, ges_ev)
-- Open AP dont have specific flag so we cant include them alongside WPA
-- so we exclude WEP instead (more encryption to exclude? not really future proof)
if string.find(self.info.flags, "WEP") then
UIManager:show(InfoMessage:new{
text = _("Networks with WEP encryption are not supported.")
})
return
end
if self.btn_disconnect then
-- noop if touch is not on disconnect button
if ges_ev.pos:intersectWith(self.btn_disconnect.dimen) then
self:disconnect()
end
elseif self.info.password then
if self.btn_edit_nw and ges_ev.pos:intersectWith(self.btn_edit_nw.dimen) then
self:onEditNetwork()
else
self:connect()
end
else
self:onAddNetwork()
end
return true
end
local NetworkSetting = InputContainer:extend{
width = nil,
height = nil,
-- sample network_list entry: {
-- bssid = "any",
-- ssid = "foo",
-- signal_level = -58,
-- signal_quality = 84,
-- frequency = 5660,
-- flags = "[WPA2-PSK-CCMP][ESS]",
-- }
network_list = nil,
connect_callback = nil,
disconnect_callback = nil,
}
function NetworkSetting:init()
self.width = self.width or Screen:getWidth() - Screen:scaleBySize(50)
self.width = math.min(self.width, Screen:scaleBySize(600))
local gray_bg = Blitbuffer.COLOR_GRAY_E
local items = {}
table.sort(self.network_list,
function(l, r) return l.signal_quality > r.signal_quality end)
for idx, network in ipairs(self.network_list) do
local bg
if band(idx, 1) == 0 then
bg = gray_bg
else
bg = Blitbuffer.COLOR_WHITE
end
table.insert(items, NetworkItem:new{
width = self.width,
info = network,
background = bg,
setting_ui = self,
})
end
self.status_text = TextWidget:new{
text = "",
face = Font:getFace("ffont"),
}
self.page_text = TextWidget:new{
text = "",
face = Font:getFace("ffont"),
}
self.pagination = MinimalPaginator:new{
width = self.width,
height = Screen:scaleBySize(8),
percentage = 0,
progress = 0,
}
self.height = self.height or math.min(Screen:getHeight()*3/4,
Screen:scaleBySize(800))
self.popup = FrameContainer:new{
background = Blitbuffer.COLOR_WHITE,
padding = 0,
bordersize = Size.border.window,
VerticalGroup:new{
align = "left",
self.pagination,
ListView:new{
padding = 0,
items = items,
width = self.width,
height = self.height-self.pagination:getSize().h,
page_update_cb = function(curr_page, total_pages)
self.pagination:setProgress(curr_page/total_pages)
-- self.page_text:setText(curr_page .. "/" .. total_pages)
UIManager:setDirty(self, function()
return "ui", self.dimen
end)
end
},
},
}
self[1] = CenterContainer:new{
dimen = {w = Screen:getWidth(), h = Screen:getHeight()},
self.popup,
}
if Device:isTouchDevice() then
self.ges_events.TapClose = {
GestureRange:new{
ges = "tap",
range = Geom:new{
x = 0, y = 0,
w = Screen:getWidth(),
h = Screen:getHeight(),
}
}
}
end
UIManager:nextTick(function()
local connected_item = self:getConnectedItem()
if connected_item ~= nil then
obtainIP()
if G_reader_settings:nilOrTrue("auto_dismiss_wifi_scan") then
UIManager:close(self, "ui", self.dimen)
end
UIManager:show(InfoMessage:new{
text = T(_("Connected to network %1"), BD.wrap(connected_item.display_ssid)),
timeout = 3,
})
if self.connect_callback then
self.connect_callback()
end
end
end)
end
function NetworkSetting:setConnectedItem(item)
self.connected_item = item
end
function NetworkSetting:getConnectedItem()
return self.connected_item
end
function NetworkSetting:onTapClose(arg, ges_ev)
if ges_ev.pos:notIntersectWith(self.popup.dimen) then
UIManager:close(self)
return true
end
end
return NetworkSetting