-
Notifications
You must be signed in to change notification settings - Fork 11
/
serverutil.py
163 lines (131 loc) · 5.54 KB
/
serverutil.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""Utility functions for the server.
This includes the interface from the server implementation to the
payment channel and lightning network APIs.
requires_auth -- decorator which makes a view function require authentication
authenticate_before_request -- a before_request callback for auth
api_factory -- returns a flask Blueprint or equivalent, along with a decorator
making functions availiable as RPCs, and a base class for
SQLAlchemy Declarative database models.
Signals:
WALLET_NOTIFY: sent when bitcoind tells us it has a transaction.
- tx = txid
BLOCK_NOTIFY: send when bitcoind tells us it has a block
- block = block hash
"""
import os.path
from functools import wraps
from flask import Flask, current_app, Response, request, Blueprint
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.types import TypeDecorator
from blinker import Namespace
from sqlalchemy import LargeBinary, Text
from jsonrpc.backend.flask import JSONRPCAPI
import bitcoin.core.serialize
from jsonrpcproxy import SmartDispatcher
app = Flask(__name__)
database = SQLAlchemy(app)
SIGNALS = Namespace()
WALLET_NOTIFY = SIGNALS.signal('WALLET_NOTIFY')
BLOCK_NOTIFY = SIGNALS.signal('BLOCK_NOTIFY')
# Copied from http://flask.pocoo.org/snippets/8/
def check_auth(username, password):
"""This function is called to check if a username /
password combination is valid.
"""
return (username == current_app.config['rpcuser'] and
password == current_app.config['rpcpassword'])
def authenticate():
"""Sends a 401 response that enables basic auth"""
return Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'})
def requires_auth(view):
"""Require basic authentication on requests to this view.
Also only accept requests from localhost.
"""
@wraps(view)
def decorated(*args, **kwargs):
"""Decorated version of view that checks authentication."""
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
if request.remote_addr != "127.0.0.1":
return Response("Access outside 127.0.0.1 forbidden", 403)
return view(*args, **kwargs)
return decorated
def authenticate_before_request():
"""before_request callback to perform authentication."""
return requires_auth(lambda: None)()
def api_factory(name):
"""Construct a Blueprint and a REMOTE decorator to set up an API.
RPC calls are availiable at the url /name/
"""
api = Blueprint(name, __name__, url_prefix='/'+name)
# set up the database
def setup_bind(state):
"""Add the database to the config."""
database_path = os.path.join(state.app.config['datadir'], name + '.dat')
state.app.config['SQLALCHEMY_BINDS'][name] = 'sqlite:///' + database_path
api.record_once(setup_bind)
def initialize_database():
"""Create the database."""
database.create_all(name)
api.before_app_first_request(initialize_database)
# create a base class for models
class BoundMeta(type(database.Model)):
"""Metaclass for Model which allows __abstract__ base classes."""
def __init__(self, cls_name, bases, attrs):
assert '__bind_key__' not in attrs
if not attrs.get('__abstract__', False):
attrs['__bind_key__'] = name
super(BoundMeta, self).__init__(cls_name, bases, attrs)
class BoundModel(database.Model, metaclass=BoundMeta):
"""Base class for models which have __bind_key__ set automatically."""
__abstract__ = True
query = object.__getattribute__(database.Model, 'query')
def __init__(self, *args, **kwargs):
super(BoundModel, self).__init__(*args, **kwargs)
# create a JSON-RPC API endpoint
rpc_api = JSONRPCAPI(SmartDispatcher())
assert type(rpc_api.dispatcher == SmartDispatcher)
api.add_url_rule('/', 'rpc', rpc_api.as_view(), methods=['POST'])
return api, rpc_api.dispatcher.add_method, BoundModel
class ImmutableSerializableType(TypeDecorator):
"""Converts bitcoin-lib ImmutableSerializable instances for the DB."""
impl = LargeBinary
def __init__(self, subtype=bitcoin.core.serialize.ImmutableSerializable):
self.subtype = subtype
super(ImmutableSerializableType, self).__init__()
@property
def python_type(self):
return self.subtype
def process_bind_param(self, value, dialect):
if value is not None:
value = value.serialize()
return value
def process_result_value(self, value, dialect):
if value is not None:
value = self.subtype.deserialize(value)
return value
def process_literal_param(self, value, dialect):
raise NotImplementedError()
class Base58DataType(TypeDecorator):
"""Converts bitcoin-lib Base58Data instances for the DB."""
impl = Text
def __init__(self, subtype=bitcoin.base58.CBase58Data):
self.subtype = subtype
super(Base58DataType, self).__init__()
@property
def python_type(self):
return self.subtype
def process_bind_param(self, value, dummy_dialect):
if value is not None:
value = str(value)
return value
def process_result_value(self, value, dummy_dialect):
if value is not None:
value = self.subtype(value)
return value
def process_literal_param(self, value, dialect):
raise NotImplementedError()