Skip to content
GitLab
Explore
Sign in
Register
Primary navigation
Search or go to…
Project
G
gajim-plugins
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Model registry
Operate
Environments
Monitor
Incidents
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
gajim
gajim-plugins
Commits
9064e4a8
Commit
9064e4a8
authored
5 years ago
by
Philipp Hörist
Browse files
Options
Downloads
Patches
Plain Diff
[omemo] Refactor file downloads
- Use Gajim FileTransferProgress Dialog - Use libsoup - Fixes
#467
,
#419
parent
4f70e9b4
No related branches found
Branches containing commit
No related tags found
Tags containing commit
No related merge requests found
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
omemo/file_crypto.py
+148
-174
148 additions, 174 deletions
omemo/file_crypto.py
with
148 additions
and
174 deletions
omemo/file_crypto.py
+
148
−
174
View file @
9064e4a8
...
@@ -14,136 +14,145 @@
...
@@ -14,136 +14,145 @@
# You should have received a copy of the GNU General Public License
# You should have received a copy of the GNU General Public License
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
# along with OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import
os
import
sys
import
hashlib
import
hashlib
import
logging
import
logging
import
socket
import
threading
import
binascii
import
binascii
import
ssl
from
pathlib
import
Path
from
urllib.request
import
urlopen
from
urllib.parse
import
urlparse
,
unquote
from
urllib.error
import
URLError
from
urllib.parse
import
urlparse
,
urldefrag
from
io
import
BufferedWriter
,
FileIO
,
BytesIO
from
gi.repository
import
GLib
from
gi.repository
import
GLib
from
gi.repository
import
Soup
from
gajim.common
import
app
from
gajim.common
import
app
from
gajim.common
import
configpaths
from
gajim.common
import
configpaths
from
gajim.common
import
helpers
from
gajim.common.helpers
import
write_file_async
from
gajim.common.helpers
import
open_file
from
gajim.common.const
import
URIType
from
gajim.common.const
import
URIType
from
gajim.common.const
import
FTState
from
gajim.common.filetransfer
import
FileTransfer
from
gajim.plugins.plugins_i18n
import
_
from
gajim.plugins.plugins_i18n
import
_
from
gajim.gtk.dialogs
import
ErrorDialog
from
gajim.gtk.dialogs
import
DialogButton
from
gajim.gtk.dialogs
import
DialogButton
from
gajim.gtk.dialogs
import
NewConfirmationDialog
from
gajim.gtk.dialogs
import
NewConfirmationDialog
from
omemo.gtk.progress
import
ProgressWindow
from
omemo.backend.aes
import
aes_decrypt_file
from
omemo.backend.aes
import
aes_decrypt_file
if
sys
.
platform
in
(
'
win32
'
,
'
darwin
'
):
import
certifi
log
=
logging
.
getLogger
(
'
gajim.p.omemo.filedecryption
'
)
log
=
logging
.
getLogger
(
'
gajim.p.omemo.filedecryption
'
)
DIRECTORY
=
os
.
path
.
join
(
configpaths
.
get
(
'
MY_DATA
'
),
'
downloads
'
)
DIRECTORY
=
Path
(
configpaths
.
get
(
'
MY_DATA
'
))
/
'
downloads
'
ERROR
=
False
try
:
if
not
os
.
path
.
exists
(
DIRECTORY
):
os
.
makedirs
(
DIRECTORY
)
except
Exception
:
ERROR
=
True
log
.
exception
(
'
Error
'
)
class
File
:
def
__init__
(
self
,
url
,
account
):
self
.
account
=
account
self
.
url
,
self
.
fragment
=
urldefrag
(
url
)
self
.
key
=
None
self
.
iv
=
None
self
.
filepath
=
None
self
.
filename
=
None
class
FileDecryption
:
class
FileDecryption
:
def
__init__
(
self
,
plugin
):
def
__init__
(
self
,
plugin
):
self
.
plugin
=
plugin
self
.
plugin
=
plugin
self
.
window
=
None
self
.
window
=
None
self
.
_session
=
Soup
.
Session
()
def
hyperlink_handler
(
self
,
uri
,
instance
,
window
):
def
hyperlink_handler
(
self
,
uri
,
instance
,
window
):
if
ERROR
or
uri
.
type
!=
URIType
.
WEB
:
if
uri
.
type
!=
URIType
.
WEB
:
return
return
self
.
window
=
window
self
.
window
=
window
urlparts
=
urlparse
(
uri
.
data
)
file
=
File
(
urlparts
.
geturl
(),
instance
.
account
)
if
urlparts
.
scheme
not
in
[
'
https
'
,
'
aesgcm
'
]
or
not
urlparts
.
netloc
:
urlparts
=
urlparse
(
unquote
(
uri
.
data
))
log
.
info
(
"
Not accepting URL for decryption: %s
"
,
uri
.
data
)
if
urlparts
.
scheme
!=
'
aesgcm
'
:
log
.
info
(
'
URL not encrypted: %s
'
,
uri
.
data
)
return
return
if
urlparts
.
scheme
==
'
aesgcm
'
:
try
:
log
.
debug
(
'
aesgcm scheme detected
'
)
key
,
iv
=
self
.
_parse_fragment
(
urlparts
.
fragment
)
file
.
url
=
'
https://
'
+
file
.
url
[
9
:]
except
ValueError
:
if
not
self
.
is_encrypted
(
file
):
log
.
info
(
'
URL not encrypted: %s
'
,
uri
.
data
)
log
.
info
(
'
URL not encrypted: %s
'
,
uri
.
data
)
return
return
self
.
create_paths
(
file
)
file_path
=
self
.
_get_file_path
(
uri
.
data
,
urlparts
)
if
file_path
.
exists
():
if
os
.
path
.
exists
(
file
.
filepath
):
instance
.
plugin_modified
=
True
instance
.
plugin_modified
=
True
self
.
finished
(
file
)
self
.
_show_file_open_dialog
(
file_path
)
return
return
event
=
threading
.
Event
()
file_path
.
parent
.
mkdir
(
mode
=
0o700
,
exist_ok
=
True
)
progressbar
=
ProgressWindow
(
self
.
plugin
,
self
.
window
,
event
)
thread
=
threading
.
Thread
(
target
=
Download
,
transfer
=
OMEMODownload
(
instance
.
account
,
args
=
(
file
,
progressbar
,
self
.
window
,
self
.
_cancel_download
,
event
,
self
))
urlparts
,
thread
.
daemon
=
True
file_path
,
thread
.
start
()
key
,
iv
)
app
.
interface
.
show_httpupload_progress
(
transfer
)
self
.
_download_content
(
transfer
)
instance
.
plugin_modified
=
True
instance
.
plugin_modified
=
True
def
is_encrypted
(
self
,
file
):
def
_download_content
(
self
,
transfer
):
if
file
.
fragment
:
log
.
info
(
'
Start downloading: %s
'
,
transfer
.
request_uri
)
try
:
transfer
.
set_started
()
fragment
=
binascii
.
unhexlify
(
file
.
fragment
)
message
=
transfer
.
get_soup_message
()
file
.
key
=
fragment
[
16
:]
message
.
connect
(
'
got-headers
'
,
self
.
_on_got_headers
,
transfer
)
file
.
iv
=
fragment
[:
16
]
message
.
connect
(
'
got-chunk
'
,
self
.
_on_got_chunk
,
transfer
)
if
len
(
file
.
key
)
==
32
and
len
(
file
.
iv
)
==
16
:
return
True
self
.
_session
.
queue_message
(
message
,
self
.
_on_finished
,
transfer
)
file
.
key
=
fragment
[
12
:]
def
_cancel_download
(
self
,
transfer
):
file
.
iv
=
fragment
[:
12
]
message
=
transfer
.
get_soup_message
()
if
len
(
file
.
key
)
==
32
and
len
(
file
.
iv
)
==
12
:
self
.
_session
.
cancel_message
(
message
,
Soup
.
Status
.
CANCELLED
)
return
True
except
:
@staticmethod
return
False
def
_on_got_headers
(
message
,
transfer
):
return
False
transfer
.
set_in_progress
()
size
=
message
.
props
.
response_headers
.
get_content_length
()
def
create_paths
(
self
,
file
):
transfer
.
size
=
size
file
.
filename
=
os
.
path
.
basename
(
file
.
url
)
ext
=
os
.
path
.
splitext
(
file
.
filename
)[
1
]
def
_on_got_chunk
(
self
,
message
,
chunk
,
transfer
):
name
=
os
.
path
.
splitext
(
file
.
filename
)[
0
]
transfer
.
set_chunk
(
chunk
.
get_data
())
urlhash
=
hashlib
.
sha1
(
file
.
url
.
encode
(
'
utf-8
'
)).
hexdigest
()
transfer
.
update_progress
()
newfilename
=
name
+
'
_
'
+
urlhash
[:
10
]
+
ext
file
.
filepath
=
os
.
path
.
join
(
DIRECTORY
,
newfilename
)
self
.
_session
.
pause_message
(
message
)
GLib
.
idle_add
(
self
.
_session
.
unpause_message
,
message
)
def
finished
(
self
,
file
):
def
_on_finished
(
self
,
_session
,
message
,
transfer
):
if
message
.
props
.
status_code
==
Soup
.
Status
.
CANCELLED
:
log
.
info
(
'
Download cancelled
'
)
return
if
message
.
status_code
!=
Soup
.
Status
.
OK
:
log
.
warning
(
'
Download failed: %s
'
,
transfer
.
request_uri
)
log
.
warning
(
Soup
.
Status
.
get_phrase
(
message
.
status_code
))
return
data
=
message
.
props
.
response_body_data
.
get_data
()
if
data
is
None
:
return
decrypted_data
=
aes_decrypt_file
(
transfer
.
key
,
transfer
.
iv
,
data
)
write_file_async
(
transfer
.
path
,
decrypted_data
,
self
.
_on_decrypted
,
transfer
)
transfer
.
set_decrypting
()
def
_on_decrypted
(
self
,
_result
,
error
,
transfer
):
if
error
is
not
None
:
log
.
error
(
'
%s: %s
'
,
transfer
.
path
,
error
)
return
transfer
.
set_finished
()
self
.
_show_file_open_dialog
(
transfer
.
path
)
def
_show_file_open_dialog
(
self
,
file_path
):
def
_open_file
():
def
_open_file
():
helpers
.
open_file
(
file
.
file
path
)
open_file
(
file
_
path
)
def
_open_folder
():
def
_open_folder
():
directory
=
os
.
path
.
dirname
(
file
.
filepath
)
open_file
(
file_path
.
parent
)
helpers
.
open_file
(
directory
)
NewConfirmationDialog
(
NewConfirmationDialog
(
_
(
'
Open File
'
),
_
(
'
Open File
'
),
_
(
'
Open File?
'
),
_
(
'
Open File?
'
),
_
(
'
Do you want to open %s?
'
)
%
file
.
file
name
,
_
(
'
Do you want to open %s?
'
)
%
file
_path
.
name
,
[
DialogButton
.
make
(
'
Cancel
'
,
[
DialogButton
.
make
(
'
Cancel
'
,
text
=
_
(
'
_No
'
)),
text
=
_
(
'
_No
'
)),
DialogButton
.
make
(
'
OK
'
,
DialogButton
.
make
(
'
OK
'
,
...
@@ -154,104 +163,69 @@ class FileDecryption:
...
@@ -154,104 +163,69 @@ class FileDecryption:
callback
=
_open_file
)],
callback
=
_open_file
)],
transient_for
=
self
.
window
).
show
()
transient_for
=
self
.
window
).
show
()
return
False
@staticmethod
def
_parse_fragment
(
fragment
):
if
not
fragment
:
raise
ValueError
(
'
Invalid fragment
'
)
fragment
=
binascii
.
unhexlify
(
fragment
)
key
=
fragment
[
16
:]
iv
=
fragment
[:
16
]
if
len
(
key
)
!=
32
or
len
(
iv
)
!=
16
:
raise
ValueError
(
'
Invalid fragment
'
)
return
key
,
iv
class
Download
:
@staticmethod
def
__init__
(
self
,
file
,
progressbar
,
window
,
event
,
base
):
def
_get_file_path
(
uri
,
urlparts
):
self
.
file
=
file
path
=
Path
(
urlparts
.
path
)
self
.
progressbar
=
progressbar
stem
=
path
.
stem
self
.
window
=
window
extension
=
path
.
suffix
self
.
event
=
event
self
.
base
=
base
self
.
download
()
def
download
(
self
):
GLib
.
idle_add
(
self
.
progressbar
.
set_text
,
_
(
'
Downloading...
'
))
data
=
self
.
load_url
()
if
isinstance
(
data
,
str
):
GLib
.
idle_add
(
self
.
progressbar
.
close_dialog
)
GLib
.
idle_add
(
self
.
error
,
data
)
return
GLib
.
idle_add
(
self
.
progressbar
.
set_text
,
_
(
'
Decrypting...
'
))
if
len
(
stem
)
>
90
:
# Many Filesystems have a limit on filename length
# Most have 255, some encrypted ones only 143
# We add around 50 chars for the hash,
# so the filename should not exceed 90
stem
=
stem
[:
90
]
decrypted_data
=
aes_decrypt_file
(
self
.
file
.
key
,
name_hash
=
hashlib
.
sha1
(
str
(
uri
).
encode
()).
hexdigest
()
self
.
file
.
iv
,
data
.
getvalue
())
GLib
.
idle_add
(
hash_filename
=
'
%s_%s%s
'
%
(
stem
,
name_hash
,
extension
)
self
.
progressbar
.
set_text
,
_
(
'
Writing file to harddisk...
'
))
self
.
write_file
(
decrypted_data
)
GLib
.
idle_add
(
self
.
progressbar
.
close_dialog
)
file_path
=
DIRECTORY
/
hash_filename
return
file_path
GLib
.
idle_add
(
self
.
base
.
finished
,
self
.
file
)
def
load_url
(
self
):
class
OMEMODownload
(
FileTransfer
):
try
:
stream
=
BytesIO
()
_state_descriptions
=
{
if
not
app
.
config
.
get_per
(
'
accounts
'
,
FTState
.
DECRYPTING
:
_
(
'
Decrypting file…
'
),
self
.
file
.
account
,
FTState
.
STARTED
:
_
(
'
Downloading…
'
),
'
httpupload_verify
'
):
}
context
=
ssl
.
create_default_context
()
context
.
check_hostname
=
False
def
__init__
(
self
,
account
,
cancel_func
,
urlparts
,
path
,
key
,
iv
):
context
.
verify_mode
=
ssl
.
CERT_NONE
FileTransfer
.
__init__
(
self
,
account
,
cancel_func
=
cancel_func
)
log
.
warning
(
'
CERT Verification disabled
'
)
get_request
=
urlopen
(
self
.
file
.
url
,
timeout
=
30
,
context
=
context
)
self
.
_urlparts
=
urlparts
else
:
self
.
path
=
path
cafile
=
None
self
.
iv
=
iv
if
sys
.
platform
in
(
'
win32
'
,
'
darwin
'
):
self
.
key
=
key
cafile
=
certifi
.
where
()
context
=
ssl
.
create_default_context
(
cafile
=
cafile
)
self
.
_message
=
None
get_request
=
urlopen
(
self
.
file
.
url
,
timeout
=
30
,
context
=
context
)
@property
size
=
get_request
.
info
()[
'
Content-Length
'
]
def
request_uri
(
self
):
if
not
size
:
urlparts
=
self
.
_urlparts
.
_replace
(
scheme
=
'
https
'
,
fragment
=
''
)
errormsg
=
'
Content-Length not found in header
'
return
urlparts
.
geturl
()
log
.
error
(
errormsg
)
return
errormsg
while
True
:
try
:
if
self
.
event
.
isSet
():
raise
DownloadAbortedException
temp
=
get_request
.
read
(
10000
)
GLib
.
idle_add
(
self
.
progressbar
.
update_progress
,
len
(
temp
),
size
)
except
socket
.
timeout
:
errormsg
=
'
Request timeout
'
log
.
error
(
errormsg
)
return
errormsg
if
temp
:
stream
.
write
(
temp
)
else
:
return
stream
except
DownloadAbortedException
as
error
:
log
.
info
(
'
Download Aborted
'
)
errormsg
=
error
except
URLError
as
error
:
log
.
exception
(
'
URLError
'
)
errormsg
=
error
.
reason
except
Exception
as
error
:
log
.
exception
(
'
Error
'
)
errormsg
=
error
stream
.
close
()
return
str
(
errormsg
)
def
write_file
(
self
,
data
):
log
.
info
(
'
Writing data to %s
'
,
self
.
file
.
filepath
)
try
:
with
BufferedWriter
(
FileIO
(
self
.
file
.
filepath
,
"
wb
"
))
as
output
:
output
.
write
(
data
)
output
.
close
()
except
Exception
:
log
.
exception
(
'
Failed to write file
'
)
def
error
(
self
,
error
):
@property
ErrorDialog
(
_
(
'
Error
'
),
error
,
transient_for
=
self
.
window
)
def
filename
(
self
):
return
Fals
e
return
Path
(
self
.
_urlparts
.
path
).
nam
e
def
set_chunk
(
self
,
bytes_
):
self
.
_seen
+=
len
(
bytes_
)
class
DownloadAbortedException
(
Exception
):
def
get_soup_message
(
self
):
def
__str__
(
self
):
if
self
.
_message
is
None
:
return
_
(
'
Download Aborted
'
)
self
.
_message
=
Soup
.
Message
.
new
(
'
GET
'
,
self
.
request_uri
)
return
self
.
_message
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment