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 @@
# 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/>.
import
os
import
sys
import
hashlib
import
logging
import
socket
import
threading
import
binascii
import
ssl
from
urllib.request
import
urlopen
from
urllib.error
import
URLError
from
urllib.parse
import
urlparse
,
urldefrag
from
io
import
BufferedWriter
,
FileIO
,
BytesIO
from
pathlib
import
Path
from
urllib.parse
import
urlparse
,
unquote
from
gi.repository
import
GLib
from
gi.repository
import
Soup
from
gajim.common
import
app
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
FTState
from
gajim.common.filetransfer
import
FileTransfer
from
gajim.plugins.plugins_i18n
import
_
from
gajim.gtk.dialogs
import
ErrorDialog
from
gajim.gtk.dialogs
import
DialogButton
from
gajim.gtk.dialogs
import
NewConfirmationDialog
from
omemo.gtk.progress
import
ProgressWindow
from
omemo.backend.aes
import
aes_decrypt_file
if
sys
.
platform
in
(
'
win32
'
,
'
darwin
'
):
import
certifi
log
=
logging
.
getLogger
(
'
gajim.p.omemo.filedecryption
'
)
DIRECTORY
=
os
.
path
.
join
(
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
DIRECTORY
=
Path
(
configpaths
.
get
(
'
MY_DATA
'
))
/
'
downloads
'
class
FileDecryption
:
def
__init__
(
self
,
plugin
):
self
.
plugin
=
plugin
self
.
window
=
None
self
.
_session
=
Soup
.
Session
()
def
hyperlink_handler
(
self
,
uri
,
instance
,
window
):
if
ERROR
or
uri
.
type
!=
URIType
.
WEB
:
if
uri
.
type
!=
URIType
.
WEB
:
return
self
.
window
=
window
urlparts
=
urlparse
(
uri
.
data
)
file
=
File
(
urlparts
.
geturl
(),
instance
.
account
)
if
urlparts
.
scheme
not
in
[
'
https
'
,
'
aesgcm
'
]
or
not
urlparts
.
netloc
:
log
.
info
(
"
Not accepting URL for decryption: %s
"
,
uri
.
data
)
urlparts
=
urlparse
(
unquote
(
uri
.
data
))
if
urlparts
.
scheme
!=
'
aesgcm
'
:
log
.
info
(
'
URL not encrypted: %s
'
,
uri
.
data
)
return
if
urlparts
.
scheme
==
'
aesgcm
'
:
log
.
debug
(
'
aesgcm scheme detected
'
)
file
.
url
=
'
https://
'
+
file
.
url
[
9
:]
if
not
self
.
is_encrypted
(
file
):
try
:
key
,
iv
=
self
.
_parse_fragment
(
urlparts
.
fragment
)
except
ValueError
:
log
.
info
(
'
URL not encrypted: %s
'
,
uri
.
data
)
return
self
.
create_paths
(
file
)
if
os
.
path
.
exists
(
file
.
filepath
):
file_path
=
self
.
_get_file_path
(
uri
.
data
,
urlparts
)
if
file_path
.
exists
():
instance
.
plugin_modified
=
True
self
.
finished
(
file
)
self
.
_show_file_open_dialog
(
file_path
)
return
event
=
threading
.
Event
()
progressbar
=
ProgressWindow
(
self
.
plugin
,
self
.
window
,
event
)
thread
=
threading
.
Thread
(
target
=
Download
,
args
=
(
file
,
progressbar
,
self
.
window
,
event
,
self
))
thread
.
daemon
=
True
thread
.
start
()
file_path
.
parent
.
mkdir
(
mode
=
0o700
,
exist_ok
=
True
)
transfer
=
OMEMODownload
(
instance
.
account
,
self
.
_cancel_download
,
urlparts
,
file_path
,
key
,
iv
)
app
.
interface
.
show_httpupload_progress
(
transfer
)
self
.
_download_content
(
transfer
)
instance
.
plugin_modified
=
True
def
is_encrypted
(
self
,
file
):
if
file
.
fragment
:
try
:
fragment
=
binascii
.
unhexlify
(
file
.
fragment
)
file
.
key
=
fragment
[
16
:]
file
.
iv
=
fragment
[:
16
]
if
len
(
file
.
key
)
==
32
and
len
(
file
.
iv
)
==
16
:
return
True
file
.
key
=
fragment
[
12
:]
file
.
iv
=
fragment
[:
12
]
if
len
(
file
.
key
)
==
32
and
len
(
file
.
iv
)
==
12
:
return
True
except
:
return
False
return
False
def
create_paths
(
self
,
file
):
file
.
filename
=
os
.
path
.
basename
(
file
.
url
)
ext
=
os
.
path
.
splitext
(
file
.
filename
)[
1
]
name
=
os
.
path
.
splitext
(
file
.
filename
)[
0
]
urlhash
=
hashlib
.
sha1
(
file
.
url
.
encode
(
'
utf-8
'
)).
hexdigest
()
newfilename
=
name
+
'
_
'
+
urlhash
[:
10
]
+
ext
file
.
filepath
=
os
.
path
.
join
(
DIRECTORY
,
newfilename
)
def
finished
(
self
,
file
):
def
_download_content
(
self
,
transfer
):
log
.
info
(
'
Start downloading: %s
'
,
transfer
.
request_uri
)
transfer
.
set_started
()
message
=
transfer
.
get_soup_message
()
message
.
connect
(
'
got-headers
'
,
self
.
_on_got_headers
,
transfer
)
message
.
connect
(
'
got-chunk
'
,
self
.
_on_got_chunk
,
transfer
)
self
.
_session
.
queue_message
(
message
,
self
.
_on_finished
,
transfer
)
def
_cancel_download
(
self
,
transfer
):
message
=
transfer
.
get_soup_message
()
self
.
_session
.
cancel_message
(
message
,
Soup
.
Status
.
CANCELLED
)
@staticmethod
def
_on_got_headers
(
message
,
transfer
):
transfer
.
set_in_progress
()
size
=
message
.
props
.
response_headers
.
get_content_length
()
transfer
.
size
=
size
def
_on_got_chunk
(
self
,
message
,
chunk
,
transfer
):
transfer
.
set_chunk
(
chunk
.
get_data
())
transfer
.
update_progress
()
self
.
_session
.
pause_message
(
message
)
GLib
.
idle_add
(
self
.
_session
.
unpause_message
,
message
)
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
():
helpers
.
open_file
(
file
.
file
path
)
open_file
(
file
_
path
)
def
_open_folder
():
directory
=
os
.
path
.
dirname
(
file
.
filepath
)
helpers
.
open_file
(
directory
)
open_file
(
file_path
.
parent
)
NewConfirmationDialog
(
_
(
'
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
'
,
text
=
_
(
'
_No
'
)),
DialogButton
.
make
(
'
OK
'
,
...
...
@@ -154,104 +163,69 @@ class FileDecryption:
callback
=
_open_file
)],
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
:
def
__init__
(
self
,
file
,
progressbar
,
window
,
event
,
base
):
self
.
file
=
file
self
.
progressbar
=
progressbar
self
.
window
=
window
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
@staticmethod
def
_get_file_path
(
uri
,
urlparts
):
path
=
Path
(
urlparts
.
path
)
stem
=
path
.
stem
extension
=
path
.
suffix
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
,
self
.
file
.
iv
,
data
.
getvalue
())
name_hash
=
hashlib
.
sha1
(
str
(
uri
).
encode
()).
hexdigest
()
GLib
.
idle_add
(
self
.
progressbar
.
set_text
,
_
(
'
Writing file to harddisk...
'
))
self
.
write_file
(
decrypted_data
)
hash_filename
=
'
%s_%s%s
'
%
(
stem
,
name_hash
,
extension
)
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
):
try
:
stream
=
BytesIO
()
if
not
app
.
config
.
get_per
(
'
accounts
'
,
self
.
file
.
account
,
'
httpupload_verify
'
):
context
=
ssl
.
create_default_context
()
context
.
check_hostname
=
False
context
.
verify_mode
=
ssl
.
CERT_NONE
log
.
warning
(
'
CERT Verification disabled
'
)
get_request
=
urlopen
(
self
.
file
.
url
,
timeout
=
30
,
context
=
context
)
else
:
cafile
=
None
if
sys
.
platform
in
(
'
win32
'
,
'
darwin
'
):
cafile
=
certifi
.
where
()
context
=
ssl
.
create_default_context
(
cafile
=
cafile
)
get_request
=
urlopen
(
self
.
file
.
url
,
timeout
=
30
,
context
=
context
)
size
=
get_request
.
info
()[
'
Content-Length
'
]
if
not
size
:
errormsg
=
'
Content-Length not found in header
'
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
'
)
class
OMEMODownload
(
FileTransfer
):
_state_descriptions
=
{
FTState
.
DECRYPTING
:
_
(
'
Decrypting file…
'
),
FTState
.
STARTED
:
_
(
'
Downloading…
'
),
}
def
__init__
(
self
,
account
,
cancel_func
,
urlparts
,
path
,
key
,
iv
):
FileTransfer
.
__init__
(
self
,
account
,
cancel_func
=
cancel_func
)
self
.
_urlparts
=
urlparts
self
.
path
=
path
self
.
iv
=
iv
self
.
key
=
key
self
.
_message
=
None
@property
def
request_uri
(
self
):
urlparts
=
self
.
_urlparts
.
_replace
(
scheme
=
'
https
'
,
fragment
=
''
)
return
urlparts
.
geturl
()
def
error
(
self
,
error
):
ErrorDialog
(
_
(
'
Error
'
),
error
,
transient_for
=
self
.
window
)
return
Fals
e
@property
def
filename
(
self
):
return
Path
(
self
.
_urlparts
.
path
).
nam
e
def
set_chunk
(
self
,
bytes_
):
self
.
_seen
+=
len
(
bytes_
)
class
DownloadAbortedException
(
Exception
):
def
__str__
(
self
):
return
_
(
'
Download Aborted
'
)
def
get_soup_message
(
self
):
if
self
.
_message
is
None
:
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