Skip to content
This repository has been archived by the owner on Dec 12, 2018. It is now read-only.

Commit

Permalink
Initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
Schnouki committed Feb 10, 2014
0 parents commit cf17314
Show file tree
Hide file tree
Showing 10 changed files with 1,364 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
build
dist
*.egg-info
*.pyc
674 changes: 674 additions & 0 deletions COPYING

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include COPYING
include README.md
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
hubiC remote for git-annex
==========================

This lets you store your data managed with git-annex to [hubiC](https://hubic.com/).

Warning: this project is very recent. It may cause data loss. Don't trust it.


Installation
------------

1. Install Python2 and setuptools (on Arch Linux: `pacman -S python2-setuptools`;
on Debian/Ubuntu: `apt-get install python-setuptools`).

2. Clone this repository:

git clone git://github.com/Schnouki/git-annex-remote-hubic.git

3. Install the package:

python2 setup.py --user install
# Use python on outdated distros such as Debian or Ubuntu

4. Log into your hubiC account, go to "my account", and then to "your
applications" ([short link](https://hubic.com/home/browser/developers/)

5. Click "Add an application". Enter "git-annex-remote-hubic" in the Name field,
and "http://localhost:18181/" in the Redirection domain field, then click
Ok. You will now see your client ID and client secret, which you will need
soon.

6. Go to a repository you manage with git-annex, and initialize your new remote
using the following commands as a starting point:

export HUBIC_CLIENT_ID=api_hubic_*****
export HUBIC_CLIENT_SECRET=*****
git annex initremote my-hubic-remote type=external externaltype=hubic encryption=shared hubic_path=annex/dirname

(`HUBIC_CLIENT_ID` and `HUBIC_CLIENT_SECRET` are the application ID and
secret you got in the previous step, `my-hubic-remote` is the name of your
remote, `hubic_path` is the directory in your hubiC account where your
annexed data will be stored. Adjust the value of the `encryption` variable
as you like.)

You can now use your new remote just like any normal git-annex special remote.

Enjoy, and in case of trouble don't hesitate to
[file an issue](github.com/Schnouki/git-annex-remote-hubic/issues) or to
[send me an e-mail](mailto:schnouki+garh@schnouki.net).


License
-------

This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with
this program. If not, see <http://www.gnu.org/licenses/>.
Empty file added hubic_remote/__init__.py
Empty file.
220 changes: 220 additions & 0 deletions hubic_remote/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# Copyright (c) 2014 Thomas Jost
#
# This file is part of git-annex-remote-hubic.
#
# git-annex-remote-hubic is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# git-annex-remote-hubic is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# git-annex-remote-hubic. If not, see <http://www.gnu.org/licenses/>.

"""hubiC authentication module"""

import BaseHTTPServer
import datetime
import os
import sys
import urlparse
import webbrowser

import dateutil.parser
import dateutil.tz
import rauth

REDIRECT_PORT = 18181
REDIRECT_URI = "http://localhost:%d/" % REDIRECT_PORT

DATETIME_MIN = datetime.datetime(2000, 1, 1, tzinfo=dateutil.tz.tzlocal())

def now():
"""Timezone-aware version of datetime.datetime.now"""
return datetime.datetime.now(dateutil.tz.tzlocal())


class HubicAuth(object):
"""Handle authentication using the hubiC API"""

access_token_url = "https://api.hubic.com/oauth/token"
authorize_url = "https://api.hubic.com/oauth/auth"
base_url = "https://api.hubic.com/1.0/"

def __init__(self, remote):
self.remote = remote

self.oauth_client_id = self.oauth_client_secret = None
self.service = None

self.refresh_token = self.access_token = None
self.access_token_expiration = DATETIME_MIN

self.swift_token = self.swift_endpoint = None
self.swift_token_expiration = DATETIME_MIN


def get_service(self):
"""Initialize the OAuth2 service"""
if self.service is not None:
return self.service
self.remote.debug("Initializing the OAuth service")

# Try to get client IDs from the config
self.oauth_client_id, self.oauth_client_secret = self.remote.get_credentials("oauth_client")

# If this is the initial login, get them from the environment instead
if self.oauth_client_id is None or self.oauth_client_secret is None:
self.remote.debug("Reading OAuth credentials from the environment")
self.oauth_client_id = os.environ.get("HUBIC_CLIENT_ID", None)
self.oauth_client_secret = os.environ.get("HUBIC_CLIENT_SECRET", None)
if self.oauth_client_id is None or self.oauth_client_secret is None:
self.remote.fatal("Could not read the HUBIC_CLIENT_ID and HUBIC_CLIENT_SECRET environment variables")

self.remote.set_credentials("oauth_client", self.oauth_client_id, self.oauth_client_secret)

# Create the OAuth service
self.service = rauth.OAuth2Service(
name="git-annex-remote",
client_id=self.oauth_client_id,
client_secret=self.oauth_client_secret,
access_token_url=self.access_token_url,
authorize_url=self.authorize_url,
base_url=self.base_url
)
return self.service


def initialize(self):
"""Perform a first-time OAuth2 authentication"""
self.remote.debug("Starting first-time OAuth2 authentication")
service = self.get_service()

# Is this enableremote or initremote? If enableremote, we already have our credentials...
_, self.refresh_token = self.remote.get_credentials("token")
if self.refresh_token is not None:
self.refresh_access_token()
self.remote.send("INITREMOTE-SUCCESS")
return

# First step: open the authorization URL in a browser
url = service.get_authorize_url(redirect_uri=REDIRECT_URI, response_type="code", scope="account.r,credentials.r")
print >>sys.stderr, "\nAn authentication tab should open in your browser. If it does not,"
print >>sys.stderr, "please go to the following URL:"
print >>sys.stderr, url
webbrowser.open_new_tab(url)

# Start a simple webserver that will handle the redirect and extract the
# request code
self.remote.debug("Starting the HTTP server to handle the redirection URL")
httpd = RedirectServer(("127.0.0.1", REDIRECT_PORT), RedirectHandler)
httpd.handle_request()
if "code" not in httpd.query:
self.remote.fatal("Something went wrong during the authentication: the request code is missing.")

request_code = httpd.query['code']

# Now get an access token and a refresh token
data = {
"code": request_code,
"redirect_uri": REDIRECT_URI,
"grant_type": "authorization_code",
}
tokens = service.get_raw_access_token(data=data).json()
self.refresh_token = tokens["refresh_token"]
self.access_token = tokens["access_token"]
self.access_token_expiration = now() + datetime.timedelta(seconds=tokens["expires_in"])
self.remote.debug("The current OAuth access token expires in %d seconds" % tokens["expires_in"])

# Finally get some informations about the account, just for fun
sess = self.get_session()
data = sess.get("account").json()
email = data['email']

# Store the credentials safely
self.remote.set_credentials("token", email, self.refresh_token)

# And tell that we're done
self.remote.send("INITREMOTE-SUCCESS")


def prepare(self):
"""Prepare for OAuth2 access"""
self.remote.debug("Preparing the remote")
_, self.refresh_token = self.remote.get_credentials("token")
if self.refresh_token is None:
self.remote.send("PREPARE-FAILURE No credentials found")

self.refresh_swift_token()
self.remote.send("PREPARE-SUCCESS")


def get_session(self):
"""Get an authenticated OAuth2 session"""
if self.access_token_expiration <= now():
self.refresh_access_token()
service = self.get_service()
return service.get_session(token=self.access_token)


def refresh_access_token(self):
"""Refresh the OAuth2 access token"""
data = {
"refresh_token": self.refresh_token,
"grant_type": "refresh_token"
}
service = self.get_service()
self.remote.debug("Refreshing the OAuth access token")
tokens = service.get_raw_access_token(data=data).json()
self.access_token = tokens["access_token"]
self.access_token_expiration = now() + datetime.timedelta(seconds=tokens["expires_in"])
self.remote.debug("The current OAuth access token expires in %d seconds" % tokens["expires_in"])


def refresh_swift_token(self):
"""Refresh the OpenStack access token"""
self.remote.debug("Refreshing the OpenStack access token")
sess = self.get_session()
swift_creds = sess.get("account/credentials").json()
self.swift_token = swift_creds['token']
self.swift_endpoint = swift_creds['endpoint']
self.swift_token_expiration = dateutil.parser.parse(swift_creds['expires'])
delta = self.swift_token_expiration - now()
self.remote.debug("The current OpenStack access token expires in %d seconds" % delta.total_seconds())


def get_swift_credentials(self):
"""Get a valid OpenStack endpoint and access token"""
if self.swift_token_expiration <= now():
self.refresh_swift_token()
return (self.swift_endpoint, self.swift_token)


class RedirectServer(BaseHTTPServer.HTTPServer):
"""A basic HTTP server that handles a single request to the OAuth redirection URL"""
query = {}

class RedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"""A basic HTTP request handler that extracts relevant information from the OAuth redirection URL"""

def do_GET(request):
"""Extract query string parameters from a URL and return a generic response"""
query = request.path.split('?', 1)[-1]
query = dict(urlparse.parse_qsl(query))
request.server.query = query

request.send_response(200)
request.send_header("Content-Type", "text/html")
request.end_headers()
request.wfile.write("""<html>
<head><title>git-annex-remote-hubic authentication</title></head>
<body><p>Authentication completed, you can now close this window.</p></body>
</html>""")

def log_message(self, *args, **kwargs):
"""No-op log message handler"""
pass
27 changes: 27 additions & 0 deletions hubic_remote/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright (c) 2014 Thomas Jost
#
# This file is part of git-annex-remote-hubic.
#
# git-annex-remote-hubic is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# git-annex-remote-hubic is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# git-annex-remote-hubic. If not, see <http://www.gnu.org/licenses/>.

"""git-annex special remote for hubiC"""

import remote

def main():
rem = remote.Remote()
rem.run()

if __name__ == "__main__":
main()
Loading

0 comments on commit cf17314

Please sign in to comment.