Skip to content

Commit 6e6da0d

Browse files
authored
Merge pull request #7 from slaporte/master
add backend
2 parents 62c48ec + ec2dd2e commit 6e6da0d

5 files changed

Lines changed: 280 additions & 2 deletions

File tree

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@ node_modules
22
app
33

44
.vscode
5-
jsconfig.json
5+
jsconfig.json
6+
7+
config.local.yaml
8+
config.labs.yaml
9+
*.*~

README.md

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,65 @@
1-
### monumental
1+
# Monumental
2+
3+
Reasonator for monuments
4+
5+
## Server
6+
7+
A small python server providing authorization for edit actions on Wikidata.
8+
9+
### Local setup
10+
11+
1. Install python requirements
12+
13+
```bash
14+
pip install -r requirements.txt
15+
```
16+
17+
2. Setup config.yaml
18+
19+
Copy config.default.yaml to config.local.yaml. You may need to add oauth consumer info, which you can apply for [here](https://meta.wikimedia.org/wiki/Special:OAuthConsumerRegistration/propose). If you need a set of keys for testing purposes (running on localhost:5000), you can email me at <stephen.laporte@gmail.com>.
20+
21+
3. Run the dev server
22+
23+
```bash
24+
python monumental/server.py
25+
```
26+
27+
Test it out:
28+
29+
- Login: http://localhost:5000/login
30+
- A simple Wikidata API query: http://localhost:5000/api?action=query&list=random&rnnamespace=0&rnlimit=10
31+
- Get an edit token (with authorization): http://localhost:5000/api?action=query&meta=tokens&use_auth=true
32+
33+
See [here](https://www.wikidata.org/w/api.php) for full Wikidata API docs.
34+
35+
### Licnese
36+
37+
Copyright (c) 2017, Stephen LaPorte
38+
39+
Redistribution and use in source and binary forms, with or without
40+
modification, are permitted provided that the following conditions are
41+
met:
42+
43+
* Redistributions of source code must retain the above copyright
44+
notice, this list of conditions and the following disclaimer.
45+
46+
* Redistributions in binary form must reproduce the above
47+
copyright notice, this list of conditions and the following
48+
disclaimer in the documentation and/or other materials provided
49+
with the distribution.
50+
51+
* The names of the contributors may not be used to endorse or
52+
promote products derived from this software without specific
53+
prior written permission.
54+
55+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
56+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
57+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
58+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
59+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
60+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
61+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
62+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
63+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
64+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
65+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

config.default.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
oauth_secret_token: "see README"
3+
oauth_consumer_token: "see README"
4+
cookie_secret: "ReplaceThisWithSomethingSomewhatSecret"
5+
...

monumental/server.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import os
4+
import json
5+
import urllib2
6+
7+
from urllib import urlencode
8+
9+
import yaml
10+
import clastic
11+
import requests
12+
13+
from clastic import Application, redirect
14+
from clastic.render import render_basic
15+
from clastic.middleware.cookie import SignedCookieMiddleware, NEVER
16+
17+
from mwoauth import Handshaker, RequestToken, ConsumerToken
18+
from requests_oauthlib import OAuth1
19+
20+
21+
DEFAULT_WIKI_API_URL = 'https://www.wikidata.org/w/api.php'
22+
WIKI_OAUTH_URL = 'https://meta.wikimedia.org/w/index.php'
23+
CUR_PATH = os.path.dirname(os.path.abspath(__file__))
24+
25+
26+
def home(cookie, request):
27+
headers = dict([(k, v) for k, v in
28+
request.environ.items() if k.startswith('HTTP_')])
29+
30+
return {'cookies': dict(cookie), # For debugging
31+
'headers': headers}
32+
33+
34+
def login(request, consumer_token, cookie, root_path):
35+
handshaker = Handshaker(WIKI_OAUTH_URL, consumer_token)
36+
37+
redirect_url, request_token = handshaker.initiate()
38+
39+
cookie['request_token_key'] = request_token.key
40+
cookie['request_token_secret'] = request_token.secret
41+
42+
cookie['return_to_url'] = request.args.get('next', root_path)
43+
44+
return redirect(redirect_url)
45+
46+
47+
def logout(request, cookie, root_path):
48+
cookie.pop('userid', None)
49+
cookie.pop('username', None)
50+
cookie.pop('oauth_access_key', None)
51+
cookie.pop('oauth_access_secret', None)
52+
cookie.pop('request_token_secret', None)
53+
cookie.pop('request_token_key', None)
54+
55+
return_to_url = request.args.get('next', root_path)
56+
57+
return redirect(return_to_url)
58+
59+
60+
def complete_login(request, consumer_token, cookie):
61+
handshaker = Handshaker(WIKI_OAUTH_URL, consumer_token)
62+
63+
req_token = RequestToken(cookie['request_token_key'],
64+
cookie['request_token_secret'])
65+
66+
access_token = handshaker.complete(req_token,
67+
request.query_string)
68+
69+
identity = handshaker.identify(access_token)
70+
71+
userid = identity['sub']
72+
username = identity['username']
73+
74+
cookie['userid'] = userid
75+
cookie['username'] = username
76+
# Is this OK to put in a cookie?
77+
cookie['oauth_access_key'] = access_token.key
78+
cookie['oauth_access_secret'] = access_token.secret
79+
80+
return_to_url = cookie.get('return_to_url', '/')
81+
82+
return redirect(return_to_url)
83+
84+
85+
def get_wd_token(request, cookie, consumer_token, token_type=None):
86+
params = {'action': 'query',
87+
'meta': 'tokens',
88+
'format': 'json'}
89+
90+
auth = OAuth1(consumer_token.key,
91+
client_secret=consumer_token.secret,
92+
resource_owner_key=cookie['oauth_access_key'],
93+
resource_owner_secret=cookie['oauth_access_secret'])
94+
95+
if token_type:
96+
# by default, gets a csrf token (for editing)
97+
params['type'] = token_type
98+
else:
99+
params['type'] = 'csrf'
100+
101+
raw_resp = requests.get(DEFAULT_WIKI_API_URL,
102+
params=params,
103+
auth=auth)
104+
105+
resp = raw_resp.json()
106+
token_name = params['type'] + 'token'
107+
token = resp['query']['tokens'][token_name]
108+
109+
return token
110+
111+
112+
def send_to_wd_api(request, cookie, consumer_token):
113+
"""Sends GET or POST variables to the Wikidata API at
114+
http://wikidata.org/w/api.php.
115+
116+
Add ?use_auth=true for actions that require logging in and an edit
117+
token.
118+
"""
119+
120+
auth = False
121+
api_args = {k: v for k, v in request.values.items()}
122+
123+
if api_args.get('use_auth'):
124+
125+
if not cookie.get('oauth_access_key'):
126+
resp_dict = {'status': 'exception',
127+
'exception': 'not logged in'}
128+
return resp_dict
129+
130+
api_args.pop('use_auth')
131+
token = get_wd_token(request, cookie, consumer_token)
132+
api_args['token'] = token
133+
auth = OAuth1(consumer_token.key,
134+
client_secret=consumer_token.secret,
135+
resource_owner_key=cookie['oauth_access_key'],
136+
resource_owner_secret=cookie['oauth_access_secret'])
137+
138+
if not api_args.get('format'):
139+
api_args['format'] = 'json'
140+
141+
method = request.method
142+
143+
if method == 'GET':
144+
resp = requests.get(DEFAULT_WIKI_API_URL, api_args, auth=auth)
145+
elif method == 'POST':
146+
resp = requests.post(DEFAULT_WIKI_API_URL, api_args, auth=auth)
147+
148+
try:
149+
resp_dict = resp.json()
150+
except ValueError:
151+
# For debugging
152+
resp_dict = {'status': 'exception',
153+
'exception': resp.text,
154+
'api_args': api_args}
155+
return resp_dict
156+
157+
158+
def create_app():
159+
routes = [('/', home, render_basic),
160+
('/login', login),
161+
('/logout', logout),
162+
('/complete_login', complete_login),
163+
('/api', send_to_wd_api, render_basic)]
164+
165+
config_file_name = 'config.local.yaml'
166+
config_file_path = os.path.join(os.path.dirname(CUR_PATH), config_file_name)
167+
168+
config = yaml.load(open(config_file_path))
169+
170+
cookie_secret = config['cookie_secret']
171+
172+
root_path = config.get('root_path', '/')
173+
174+
scm_mw = SignedCookieMiddleware(secret_key=cookie_secret,
175+
path=root_path)
176+
scm_mw.data_expiry = NEVER
177+
178+
consumer_token = ConsumerToken(config['oauth_consumer_token'],
179+
config['oauth_secret_token'])
180+
181+
resources = {'config': config,
182+
'consumer_token': consumer_token,
183+
'root_path': root_path}
184+
185+
app = Application(routes, resources, middlewares=[scm_mw])
186+
187+
return app
188+
189+
190+
app = create_app()
191+
192+
193+
if __name__ == '__main__':
194+
app.serve()

requirements.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
PyJWT==1.4.2
2+
PyYAML==3.12
3+
Werkzeug==0.9.4
4+
argparse==1.4.0
5+
clastic==0.4.3
6+
mwoauth==0.2.8
7+
oauthlib==2.0.1
8+
requests==2.12.4
9+
requests-oauthlib==0.7.0
10+
six==1.10.0
11+
wsgiref==0.1.2

0 commit comments

Comments
 (0)