Commit 6b12e964 authored by ilv's avatar ilv
Browse files

RESTful API

parent 676ee289
Loading
Loading
Loading
Loading

gettor/http.py

0 → 100644
+493 −0
Original line number Diff line number Diff line
# -*- coding: utf-8 -*-
#
# This file is part of GetTor.
#
# :authors: Israel Leiva <ilv@torproject.org>
#           see also AUTHORS file
#
# :copyright:   (c) 2008-2015, The Tor Project, Inc.
#               (c) 2015, Israel Leiva
#
# :license: This is Free Software. See LICENSE for license information.

import os
import re
import json
import codecs
import urllib2
import ConfigParser

from time import gmtime, strftime

from flask import Flask
from flask_restful import Api, Resource, reqparse

import core
import utils

"""GetTor RESTful API"""

# currently supported locales for Tor Browser
LC = ['ar', 'de', 'en-US', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl',
      'pt-PT', 'ru', 'tr', 'vi', 'zh-CN']

# https://gitweb.tpo/tor-browser-spec.git/tree/processes/VersionNumbers
# does not say anything about operating systems, so it's possible the
# notation might change in the future. We should always use the same three
# strings though: linux, windows, osx.
OS = {
    'Linux': 'linux',
    'Windows': 'windows',
    'MacOS': 'osx'
}

# based on
# https://gitweb.tpo.org/tor-browser-spec.git/tree/processes/VersionNumbers
# except for the first one, which is based on current RecommendedTBBVersions
RE = {
    'os': '(.*)-(\w+)',
    'alpha': '\d\.\d(\.\d)*a\d+',
    'beta': '\d\.\d(\.\d)*b\d+',
    'stable': '\d\.\d(\.\d)*'
}

# strings to build names of packages depending on OS.
PKG = {
    'windows': 'torbrowser-install-%s_%s.exe',
    'linux': 'tor-browser-linux%s-%s_%s.tar.xz',
    'osx': 'TorBrowser-%s-osx64_%s.dmg'
}

# bin and asc are used to build the download links for each version, os and lc
URL = {
    'version': 'https://www.torproject.org/projects/torbrowser/RecommendedTBBVersions',
    'bin': 'https://www.torproject.org/dist/torbrowser/%s/%s',
    'asc': 'https://www.torproject.org/dist/torbrowser/%s/%s.asc'
}

# filters for API resources
parser = reqparse.RequestParser()
parser.add_argument('os', type=str, help='Operating System')
parser.add_argument('lc', type=str, help='Locale')


class ConfigError(Exception):
    pass


class InternalError(Exception):
    pass


class HTTP(object):
    """ Provide useful resources via RESTful API. """
    def __init__(self, cfg=None):
        """ Create new object by reading a configuration file.

        :param: cfg (string) path of the configuration file.

        """
        default_cfg = 'http.cfg'
        config = ConfigParser.ConfigParser()

        if cfg is None or not os.path.isfile(cfg):
            cfg = default_cfg

        try:
            with open(cfg) as f:
                config.readfp(f)
        except IOError:
            raise ConfigError("File %s not found!" % cfg)

        try:
            # server that provides the RESTful API
            self.server = config.get('general', 'server')
            # path to the links files
            self.links_path = config.get('general', 'links')
            # path to mirrors in json
            self.mirrors_path = config.get('general', 'mirrors')

            # we will ask gettor.core for the links
            core_cfg = config.get('general', 'core')
            self.core = core.Core(core_cfg)

        except ConfigParser.Error as e:
            raise ConfigError("Configuration error: %s" % str(e))
        except core.ConfigError as e:
            raise InternalError("HTTP error: %s" % str(e))

    def _is_json(self, my_json):
        """ Check if json generated is valid.

        :param: my_json (string) data to ve verified.

        :return: (bool) true if data is json-valid, false otherwise.

        """
        try:
            json_object = json.loads(my_json)
        except ValueError, e:
            return False
        return True

    def _get_provider_name(self, p):
        """ Return simplified version of provider's name.

        :param: p (string) provider's name.

        :return: (string) provider's name in lowercase and without spaces.

        """
        p = p.replace(' ', '-')
        return p.lower()

    def _add_links(self, lv, release, version, os):
        """ Add link for all locales in LC depending on given OS.

        :param: lv (dict) latest version data structure.
        :param: release (string) release to which add the links.
        :param: version (string) version obtained from tpo.
        :param: os (string) operating system.

        """
        for lc in LC:
            if os == 'linux':
                pkg32 = PKG['linux'] % ('32', version, lc)
                link_bin32 = URL['bin'] % (version, pkg32)
                link_asc32 = URL['asc'] % (version, pkg32)

                pkg64 = PKG['linux'] % ('64', version, lc)
                link_bin64 = URL['bin'] % (version, pkg64)
                link_asc64 = URL['asc'] % (version, pkg64)

                lv[release]['downloads'][os][lc] = {
                    'binary32': link_bin32,
                    'signature32': link_asc32,
                    'binary64': link_bin64,
                    'signature64': link_asc64,
                }
            else:
                if os == 'windows':
                    pkg = PKG['windows'] % (version, lc)

                elif os == 'osx':
                    pkg = PKG['osx'] % (version, lc)

                else:
                    continue

                link_bin = URL['bin'] % (version, pkg)
                link_asc = URL['asc'] % (version, pkg)
                lv[release]['downloads'][os][lc] = {
                    'binary': link_bin,
                    'signature': link_asc
                }

    def _load_latest_version(self):
        """ Load latest version data. """
        response = urllib2.urlopen(URL['version'])
        json_response = json.load(response)

        lv = {
            'stable': {
                'latest_version': '',
                'downloads': {}
            },
            'alpha': {
                'latest_version': '',
                'downloads': {}
            },
            'beta': {
                'latest_version': '',
                'downloads': {}
            }
        }

        self.releases = {
            'alpha': '%s/latest/alpha' % self.server,
            'beta': '%s/latest/beta' % self.server,
            'stable': '%s/latest/stable' % self.server,
            'updated_at': strftime("%Y-%m-%d %H:%M:%S", gmtime())
        }

        # one iteration to find the latest version for each release
        for v in json_response:
            # latest version for each release
            if not re.match(RE['os'], v):
                if re.match(RE['alpha'], v):
                    if v > lv['alpha']['latest_version']:
                        # we'll use the latest one
                        lv['alpha']['latest_version'] = v

                elif re.match(RE['beta'], v):
                    if v > lv['beta']['latest_version']:
                        # we'll use the latest one
                        lv['beta']['latest_version'] = v

                elif re.match(RE['stable'], v):
                    if v > lv['stable']['latest_version']:
                        # we'll use the latest one
                        lv['stable']['latest_version'] = v

        latest_alpha = lv['alpha']['latest_version']
        latest_beta = lv['beta']['latest_version']
        latest_stable = lv['stable']['latest_version']

        # another iteration to add the links
        for v in json_response:
            # based on current RecommendedTBBVersions scheme
            # for each release and for each os we build links for all locales
            if re.match(RE['os'], v):
                m = re.match(RE['os'], v)
                version = m.group(1)
                osys = m.group(2)

                if osys in OS:
                    if latest_alpha and version == latest_alpha \
                            and re.match(RE['alpha'], version):
                        lv['alpha']['downloads'][OS[osys]] = {}
                        self._add_links(lv, 'alpha', version, OS[osys])

                    elif latest_beta and version == latest_beta \
                            and re.match(RE['beta'], version):
                        lv['beta']['downloads'][OS[osys]] = {}
                        self._add_links(lv, 'beta', version, OS[osys])

                    elif latest_stable and version == latest_stable \
                            and re.match(RE['stable'], version):
                        lv['stable']['downloads'][OS[osys]] = {}
                        self._add_links(lv, 'stable', version, OS[osys])

        lv['updated_at'] = strftime("%Y-%m-%d %H:%M:%S", gmtime())
        self.lv = lv

    def _load_links(self):
        """ Load links and providers data. """
        links_files = []

        # look for files ending with .links in links_path
        p = re.compile('.*\.links$')
        for name in os.listdir(self.links_path):
            path = os.path.abspath(os.path.join(self.links_path, name))
            if os.path.isfile(path) and p.match(path):
                links_files.append(path)

        links = {}
        providers = {}
        supported_os = self.core.get_supported_os()
        supported_lc = self.core.get_supported_lc()

        for name in links_files:
            config = ConfigParser.ConfigParser()
            try:
                with open(name) as f:
                    config.readfp(f)
            except IOError:
                raise InternalError("File %s not found!" % name)

            try:
                pname = config.get('provider', 'name')
                pname = self._get_provider_name(pname)

                # build providers dict
                providers[pname] = '%s/providers/%s' % (self.server, pname)
                providers['updated_at'] = strftime(
                    "%Y-%m-%d %H:%M:%S", gmtime()
                )

                self.providers = providers
                links[pname] = {}

                # build links data.
                for osys in supported_os:
                    links[pname][osys] = {}
                    for lc in supported_lc:
                        links[pname][osys][lc] = {}

                for osys in supported_os:
                    for lc in supported_lc:
                        l_str = config.get(osys, lc)

                        # linux has 32 and 64 bit packages
                        if osys == 'linux':
                            l32_str, l64_str = l_str.split(',')

                            link32, sig32, sha32 = [
                                l for l in l32_str.split("$") if l
                            ]

                            link64, sig64, sha64 = [
                                l for l in l64_str.split("$") if l
                            ]
                            link64 = link64.lstrip()

                            links[pname][osys][lc]['binary32'] = link32
                            links[pname][osys][lc]['signature32'] = sig32
                            links[pname][osys][lc]['sha256-32'] = sha32
                            links[pname][osys][lc]['binary64'] = link64
                            links[pname][osys][lc]['signature64'] = sig64
                            links[pname][osys][lc]['sha256-64'] = sha64

                        else:
                            link, sig, sha = [l for l in l_str.split("$") if l]
                            links[pname][osys][lc]['binary'] = link
                            links[pname][osys][lc]['signature'] = sig
                            links[pname][osys][lc]['sha256'] = sha

            except ConfigParser.Error as e:
                raise InternalError("%s" % str(e))

        links['updated_at'] = strftime("%Y-%m-%d %H:%M:%S", gmtime())
        self.links = links

    def _load_mirrors(self):
        """ Load mirrors data. """
        mirrors = []

        # json of mirrors should be obtained from get_mirrors.py
        json_data = open(self.mirrors_path).read()
        mirrors = json.loads(json_data)

        self.mirrors = mirrors

    def _load_resources(self):
        """ Load available resources data. """

        self.resources = {
            'providers': '%s/providers' % self.server,
            'mirrors': '%s/mirrors' % self.server,
            'latest_version': '%s/latest' % self.server,
            'updated_at': strftime("%Y-%m-%d %H:%M:%S", gmtime())
        }

    def load_data(self):
        """ Load all data.

        Since data is not frequently updated, we load all data before
        running the RESTful API. Every time the links/mirrors/version
        data is updated we should restart the API.

        """
        self._load_links()
        self._load_mirrors()
        self._load_resources()
        self._load_latest_version()

    def run(self):
        """ Run RESTful API. """
        app = Flask(__name__)
        api = Api(app)

        api.add_resource(
            AvailableResources,
            '/',
            resource_class_kwargs={
                'resources': self.resources
            }
        )

        api.add_resource(
            Providers,
            '/providers',
            '/providers/<string:provider>',
            resource_class_kwargs={
                'links': self.links,
                'providers': self.providers
            }
        )

        api.add_resource(
            LatestVersion,
            '/latest',
            '/latest/<string:release>',
            resource_class_kwargs={
                'latest_version': self.lv,
                'releases': self.releases
            }
        )

        api.add_resource(
            Mirrors,
            '/mirrors',
            resource_class_kwargs={
                'mirrors': self.mirrors
            }
        )

        app.run(debug=True)


class AvailableResources(Resource):
    def __init__(self, resources):
        """ Set initial data. """
        self.resources = resources

    def get(self):
        """ Return available resources on the API. """
        return self.resources


class Providers(Resource):
    def __init__(self, providers, links):
        """ Set initial data. """
        self.providers = providers
        self.links = links

    def get(self, provider=None):
        """ Return providers and links data. """

        # we use arg to filter results by os and lc (in that order)
        arg = parser.parse_args()

        if provider:
            if arg['os']:
                if arg['lc']:
                    # links by provider, os, and lc (in that order)
                    return self.links[provider][arg['os']][arg['lc']]
                else:
                    # links by provider and os (in that order)
                    return self.links[provider][arg['os']]
            else:
                # links by provider
                return self.links[provider]
        else:
            # list of providers
            return self.providers


class LatestVersion(Resource):
    def __init__(self, latest_version, releases):
        """ Set initial data. """
        self.lv = latest_version
        self.releases = releases

    def get(self, release=None):
        """ Return latest version data. """

        # we use arg to filter results by os and lc (in that order)
        arg = parser.parse_args()

        if release:
            if arg['os']:
                if arg['lc']:
                    # tpo links by release, os and lc (in that order)
                    return self.lv[release]['downloads'][arg['os']][arg['lc']]
                else:
                    # tpo links by release and os (in that order)
                    return self.lv[release]['downloads'][arg['os']]
            else:
                # version and tpo links by release
                return self.lv[release]
        else:
            # list of releases
            return self.releases


class Mirrors(Resource):
    def __init__(self, mirrors):
        """ Set initial data. """
        self.mirrors = mirrors

    def get(self):
        """ Return mirrors data. """
        return self.mirrors

process_http.py

0 → 100644
+13 −0
Original line number Diff line number Diff line
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import gettor.http


def main():
    api = gettor.http.HTTP('http.cfg')
    api.load_data()
    api.run()

if __name__ == '__main__':
    main()