Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 37 additions & 33 deletions src/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,25 @@
"""

import base64
import ConfigParser
import errno
import hashlib
import httplib
import json
import random # nosec
import socket
import subprocess
import time
import xmlrpclib
from binascii import hexlify, unhexlify
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
from struct import pack

from six.moves import configparser, http_client, xmlrpc_server

import defaults
import helper_inbox
import helper_sent
import network.stats
import proofofwork
import queues
import shared
import shutdown

import state
from addresses import (
addBMIfNotPresent,
Expand All @@ -90,9 +87,21 @@
from bmconfigparser import BMConfigParser
from debug import logger
from helper_sql import SqlBulkExecute, sqlExecute, sqlQuery, sqlStoredProcedure, sql_ready
from inventory import Inventory

# TODO: solve the issue with next two imports and remove the cutout
# FIXME: perhaps this module shouldn't use the Inventory at all
try:
import shutdown
from inventory import Inventory
except ImportError:
Inventory = None

try:
import network.stats as network_stats
except ImportError:
network_stats = None

from network.threads import StoppableThread
from six.moves import queue
from version import softwareVersion

try: # TODO: write tests for XML vulnerabilities
Expand Down Expand Up @@ -162,7 +171,7 @@ def __new__(mcs, name, bases, namespace):
return result


class APIError(xmlrpclib.Fault):
class APIError(xmlrpc_server.Fault):
"""
APIError exception class

Expand Down Expand Up @@ -210,7 +219,7 @@ def run(self):
except AttributeError:
errno.WSAEADDRINUSE = errno.EADDRINUSE

RPCServerBase = SimpleXMLRPCServer
RPCServerBase = xmlrpc_server.SimpleXMLRPCServer
ct = 'text/xml'
if BMConfigParser().safeGet(
'bitmessagesettings', 'apivariant') == 'json':
Expand Down Expand Up @@ -351,7 +360,7 @@ def wrapper(*args):
# Modified by Jonathan Warren (Atheros).
# Further modified by the Bitmessage developers
# http://code.activestate.com/recipes/501148
class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler):
"""The main API handler"""

# pylint: disable=protected-access
Expand Down Expand Up @@ -392,7 +401,7 @@ def do_POST(self):
validuser = self.APIAuthenticateClient()
if not validuser:
time.sleep(2)
self.send_response(httplib.UNAUTHORIZED)
self.send_response(http_client.UNAUTHORIZED)
self.end_headers()
return
# "RPC Username or password incorrect or HTTP header"
Expand All @@ -409,11 +418,11 @@ def do_POST(self):
)
except Exception: # This should only happen if the module is buggy
# internal error, report as HTTP server error
self.send_response(httplib.INTERNAL_SERVER_ERROR)
self.send_response(http_client.INTERNAL_SERVER_ERROR)
self.end_headers()
else:
# got a valid XML RPC response
self.send_response(httplib.OK)
self.send_response(http_client.OK)
self.send_header("Content-type", self.server.content_type)
self.send_header("Content-length", str(len(response)))

Expand Down Expand Up @@ -858,7 +867,7 @@ def HandleLeaveChan(self, address):
' Use deleteAddress API call instead.')
try:
self.config.remove_section(address)
except ConfigParser.NoSectionError:
except configparser.NoSectionError:
raise APIError(
13, 'Could not find this address in your keys.dat file.')
self.config.save()
Expand All @@ -875,7 +884,7 @@ def HandleDeleteAddress(self, address):
address = addBMIfNotPresent(address)
try:
self.config.remove_section(address)
except ConfigParser.NoSectionError:
except configparser.NoSectionError:
raise APIError(
13, 'Could not find this address in your keys.dat file.')
self.config.save()
Expand Down Expand Up @@ -1266,6 +1275,8 @@ def HandleDisseminatePreEncryptedMsg(
requiredPayloadLengthExtraBytes):
"""Handle a request to disseminate an encrypted message"""

if Inventory is None:
raise APIError(21, 'Could not import Inventory.')
# The device issuing this command to PyBitmessage supplies a msg
# object that has already been encrypted but which still needs the POW
# to be done. PyBitmessage accepts this msg object and sends it out
Expand Down Expand Up @@ -1322,6 +1333,8 @@ def HandleTrashSentMessageByAckDAta(self, ackdata):
def HandleDissimatePubKey(self, payload):
"""Handle a request to disseminate a public key"""

if Inventory is None:
raise APIError(21, 'Could not import Inventory.')
# The device issuing this command to PyBitmessage supplies a pubkey
# object to be disseminated to the rest of the Bitmessage network.
# PyBitmessage accepts this pubkey object and sends it out to the rest
Expand Down Expand Up @@ -1411,7 +1424,10 @@ def HandleClientStatus(self):
or "connectedAndReceivingIncomingConnections".
"""

connections_num = len(network.stats.connectedHostsList())
try:
connections_num = len(network_stats.connectedHostsList())
except AttributeError:
raise APIError(21, 'Could not import network_stats.')
if connections_num == 0:
networkStatus = 'notConnected'
elif state.clientHasReceivedIncomingConnections:
Expand All @@ -1423,7 +1439,7 @@ def HandleClientStatus(self):
'numberOfMessagesProcessed': state.numberOfMessagesProcessed,
'numberOfBroadcastsProcessed': state.numberOfBroadcastsProcessed,
'numberOfPubkeysProcessed': state.numberOfPubkeysProcessed,
'pendingDownload': network.stats.pendingDownload(),
'pendingDownload': network_stats.pendingDownload(),
'networkStatus': networkStatus,
'softwareName': 'PyBitmessage',
'softwareVersion': softwareVersion
Expand All @@ -1439,25 +1455,11 @@ def HandleAdd(self, a, b):
"""Test two numeric params"""
return a + b

@testmode('clearUISignalQueue')
def HandleclearUISignalQueue(self):
"""clear UISignalQueue"""
queues.UISignalQueue.queue.clear()
return "success"

@command('statusBar')
def HandleStatusBar(self, message):
"""Update GUI statusbar message"""
queues.UISignalQueue.put(('updateStatusBar', message))

@testmode('getStatusBar')
def HandleGetStatusBar(self):
"""Get GUI statusbar message"""
try:
_, data = queues.UISignalQueue.get(block=False)
except queue.Empty:
return None
return data
return "success"

@testmode('undeleteMessage')
def HandleUndeleteMessage(self, msgid):
Expand All @@ -1475,6 +1477,8 @@ def HandleDeleteAndVacuum(self):
@command('shutdown')
def HandleShutdown(self):
"""Shutdown the bitmessage. Returns 'done'."""
if Inventory is None:
raise APIError(21, 'Could not import shutdown.')
# backward compatible trick because False == 0 is True
state.shutdown = False
return 'done'
Expand Down
14 changes: 1 addition & 13 deletions src/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import psutil

from .samples import (
sample_seed, sample_deterministic_addr3, sample_deterministic_addr4, sample_statusbar_msg,
sample_seed, sample_deterministic_addr3, sample_deterministic_addr4,
sample_inbox_msg_ids, sample_test_subscription_address, sample_subscription_name)

from .test_process import TestProcessProto
Expand Down Expand Up @@ -86,18 +86,6 @@ def test_invalid_method(self):
'API Error 0020: Invalid method: test'
)

def test_statusbar_method(self):
"""Test statusbar method"""
self.api.clearUISignalQueue()
self.assertEqual(
self.api.statusBar(sample_statusbar_msg),
'null'
)
self.assertEqual(
self.api.getStatusBar(),
sample_statusbar_msg
)

def test_message_inbox(self):
"""Test message inbox methods"""
self.assertEqual(
Expand Down
74 changes: 74 additions & 0 deletions src/tests/test_api_thread.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import time
import unittest

from six.moves import queue, xmlrpc_client

from pybitmessage import pathmagic

from .samples import sample_statusbar_msg # any


class TestAPIThread(unittest.TestCase):
"""Test case running the API thread"""

@classmethod
def setUpClass(cls):
pathmagic.setup() # need this because of import state in network ):

import helper_sql
import helper_startup
import queues
import state
from bmconfigparser import BMConfigParser

# pylint: disable=too-few-public-methods
class SqlReadyMock(object):
"""Mock helper_sql.sql_ready event with dummy class"""
@staticmethod
def wait():
"""Don't wait, return immediately"""
return

helper_sql.sql_ready = SqlReadyMock
cls.state = state
cls.queues = queues

helper_startup.loadConfig()
# helper_startup.fixSocket()
config = BMConfigParser()

config.set(
'bitmessagesettings', 'apiusername', 'username')
config.set(
'bitmessagesettings', 'apipassword', 'password')
config.save()

import api
cls.thread = api.singleAPI()
cls.thread.daemon = True
cls.thread.start()
time.sleep(3)
cls.api = xmlrpc_client.ServerProxy(
"http://username:password@127.0.0.1:8442/")

def test_connection(self):
"""API command 'helloWorld'"""
self.assertEqual(
self.api.helloWorld('hello', 'world'), 'hello-world')

def test_statusbar(self):
"""Check UISignalQueue after issuing the 'statusBar' command"""
self.queues.UISignalQueue.queue.clear()
self.assertEqual(
self.api.statusBar(sample_statusbar_msg), 'success')
try:
cmd, data = self.queues.UISignalQueue.get(block=False)
except queue.Empty:
self.fail('UISignalQueue is empty!')

self.assertEqual(cmd, 'updateStatusBar')
self.assertEqual(data, sample_statusbar_msg)

@classmethod
def tearDownClass(cls):
cls.state.shutdown = 1