Add salmon support (WIP)
[friends.git] / friends / server.py
CommitLineData
5d2af2f4 1#!/usr/bin/python3
b3102128
MF
2import tornado.ioloop
3import tornado.web
5d2af2f4
MF
4import os
5import os.path
b3102128 6import tornado.httpserver
ca027dea 7import tornado.httpclient as httpclient
2af4a5fc 8import salmoning
ddf4123d
MF
9import sqlite3
10import arrow
11import datetime
12from rd import RD, Link
ca027dea 13import hmac
5d2af2f4
MF
14from tornado.options import options, define
15import logging
ddf4123d 16db = None
5d2af2f4
MF
17# insert into user (name,email) values('mikael','mikael@frykholm.com');
18# insert into entry (userid,text) values (1,'My thoughts on ostatus');
19
ddf4123d
MF
20
21settings = {
22 "static_path": os.path.join(os.path.dirname(__file__), "static"),
5d2af2f4 23 "cookie_secret": "supersecret123",
ddf4123d
MF
24 "login_url": "/login",
25 "xsrf_cookies": False,
26 "domain":"https://ronin.frykholm.com",
27
28}
5d2af2f4 29
ddf4123d 30#curl -v -k "https://ronin.frykholm.com/hub" -d "hub.callback=a" -d "hub.mode=b" -d "hub.topic=c" -d "hub.verify=d"
5d2af2f4
MF
31
32class PushHandler(tornado.web.RequestHandler):
ddf4123d
MF
33 def post(self):
34 """ Someone wants to subscribe to hub_topic feed"""
35 hub_callback = self.get_argument('hub.callback')
36 hub_mode = self.get_argument('hub.mode')
37 hub_topic = self.get_argument('hub.topic')
38 hub_verify = self.get_argument('hub.verify')
39 hub_lease_seconds = self.get_argument('hub.lease_seconds','')
ca027dea 40 hub_secret = self.get_argument('hub.secret','')
ddf4123d 41 hub_verify_token = self.get_argument('hub.verify_token','')
511b6ecb 42 print(self.request.body)
ddf4123d
MF
43 if hub_mode == 'unsubscribe':
44 pass #FIXME
45 path = hub_topic.split(self.settings['domain'])[1]
46 user = path.split('user/')[1]
6fd5e534 47 row = db.execute("select id from author where name=?",(user,)).fetchone()
ca027dea
MF
48 expire = datetime.datetime.utcnow() + datetime.timedelta(seconds=int(hub_lease_seconds))
49 if row:
6fd5e534 50 db.execute("INSERT into subscriptions (author, expires, callback, secret, verified) "
ca027dea 51 "values (?,?,?,?,?)",(row['id'],expire,hub_callback,hub_secret,False))
ddf4123d
MF
52 db.commit()
53 self.set_status(202)
ca027dea
MF
54 http_client = httpclient.HTTPClient()
55 try:
56 response = http_client.fetch(hub_callback+"?hub.mode={}&hub.topic={}&hub.secret".format(hub_mode,hub_topic,hub_secret))
57 print(response.body)
58 except httpclient.HTTPError as e:
59 # HTTPError is raised for non-200 responses; the response
60 # can be found in e.response.
61 print("Error: " + str(e))
62 except Exception as e:
63 # Other errors are possible, such as IOError.
64 print("Error: " + str(e))
65 http_client.close()
087a104b
MF
66 #TODO add secret to outgoing feeds with hmac
67
b3102128
MF
68class XrdHandler(tornado.web.RequestHandler):
69 def get(self):
ddf4123d
MF
70 self.render("templates/xrd.xml", hostname="ronin.frykholm.com", url=self.settings['domain'])
71
6fd5e534
MF
72class apa():
73 def LookupPublicKey(self, signer_uri=None):
74 return """RSA.jj2_lJ348aNh_9s3eCHlJlbMQdnHVm9svdU2ESW86TvV-4wZId-z3M029pjPvco0UEvlUUnJytXwoTLd70pzfZ8Cu5MMwGbvm9asI9-PKUDSNFgr5T_B017qUXOG5UH1ZNI_fVA2mSAkxxfEksv4HXg43dBvEIW94JpyAtqggHM=.AQAB.Bzz_LcnoLCu7RfDa3sMizROnq0YwzaY362UZLkA0X84KspVLhhzDI15SCLR4BdlvVhK2pa9SlH7Uku9quc2ZGNyr5mEdqjO7YTbQA9UCgbobEq2ImqV_j7Y4IfjPc8prDPCKb_mO9DUlS_ZUxJYfsOuc-SVlGmPZ93uEl8i9OjE="""
75
76
77class SalmonHandler(tornado.web.RequestHandler):
78 def post(self, user):
79 sp = salmoning.SalmonProtocol()
80 sp.key_retriever = apa()
81 data = sp.ParseSalmon(self.request.body)
82 pass
83
ddf4123d
MF
84class FingerHandler(tornado.web.RequestHandler):
85 def get(self):
86 user = self.get_argument('resource')
87 user = user.split('acct:')[1]
88 (user,domain) = user.split('@')
6fd5e534
MF
89 row = db.execute("select id,salmon_pubkey from author where author.name=?",(user,)).fetchone()
90 if not row:
ddf4123d
MF
91 self.set_status(404)
92 self.write("Not found")
93 self.finish()
94 return
95 lnk = Link(rel='http://spec.example.net/photo/1.0',
96 type='image/jpeg',
97 href='{}/static/{}.jpg'.format(self.settings['domain'],user))
98 lnk.titles.append(('User Photo', 'en'))
99 lnk.titles.append(('Benutzerfoto', 'de'))
100 lnk.properties.append(('http://spec.example.net/created/1.0', '1970-01-01'))
101 lnk2 = Link(rel='http://schemas.google.com/g/2010#updates-from',
102 type='application/atom+xml',
103 href='{}/user/{}'.format(self.settings['domain'],user))
104
105 rd = RD(subject='{}/{}'.format(self.settings['domain'],user))
106 rd.properties.append('http://spec.example.net/type/person')
107 rd.links.append(lnk)
108 rd.links.append(lnk2)
6fd5e534
MF
109 rd.links.append(Link(rel="magic-public-key",
110 href="data:application/magic-public-key,RSA."+row['salmon_pubkey']))
111 rd.links.append(Link(rel="salmon",
112 href="{}/salmon/{}".format(self.settings['domain'],user)))
113 rd.links.append(Link(rel="http://salmon-protocol.org/ns/salmon-replies",
114 href="{}/salmon/{}".format(self.settings['domain'],user)))
115 rd.links.append(Link(rel="http://salmon-protocol.org/ns/salmon-mention",
116 href="{}/salmon/{}".format(self.settings['domain'],user)))
ddf4123d 117 self.write(rd.to_json())
b3102128
MF
118
119class UserHandler(tornado.web.RequestHandler):
120 def get(self, user):
6fd5e534 121 entries = db.execute("select entry.id,text,ts from author,entry where author.id=entry.author and author.name=?",(user,))
5d2af2f4 122 # import pdb;pdb.set_trace()
ddf4123d 123 self.set_header("Content-Type", 'application/atom+xml')
ca027dea 124 out = self.render("templates/feed.xml",
ddf4123d
MF
125 user=user,
126 feed_url="{}/user/{}".format(self.settings['domain'], user),
127 hub_url="{}/hub".format(self.settings['domain']),
128 entries=entries,
129 arrow=arrow )
ca027dea 130 #digest = hmac.new()
b3102128 131
86f2d7fc
MF
132 def post(self, user):
133 entries = db.execute("select entry.id,text,ts from user,entry where user.id=entry.userid and user.name=?",(user,))
134
135 self.set_header("Content-Type", 'application/atom+xml')
136 out = self.render_string("templates/feed.xml",
137 user=user,
138 feed_url="{}/user/{}".format(self.settings['domain'], user),
139 hub_url="{}/hub".format(self.settings['domain']),
140 entries=entries,
141 arrow=arrow)
142 #import pdb;pdb.set_trace()
6fd5e534
MF
143 # Notify subscribers
144 subscribers = db.execute("select callback, secret from subscriptions, author where author.id=subscriptions.author and author.name=?",(user,))
86f2d7fc
MF
145 for url,secret in subscribers:
146 digest = hmac.new(secret.encode('utf8'), out, digestmod='sha1').hexdigest()
147
148 req = httpclient.HTTPRequest(url=url, allow_nonstandard_methods=True,method='POST', body=out, headers={"X-Hub-Signature":"sha1={}".format(digest),"Content-Type": 'application/atom+xml',"Content-Length":len(out)})
149 apa = httpclient.HTTPClient()
150 apa.fetch(req)
151
b3102128
MF
152application = tornado.web.Application([
153 (r"/.well-known/host-meta", XrdHandler),
ddf4123d 154 (r"/.well-known/webfinger", FingerHandler),
2af4a5fc 155 (r"/salmon/(.+)", SalmonHandler),
b3102128 156 (r"/user/(.+)", UserHandler),
ddf4123d
MF
157 (r"/hub", PushHandler),
158 ],debug=True,**settings)
ca027dea 159srv = tornado.httpserver.HTTPServer(application, )
5d2af2f4 160
ddf4123d 161def setup_db(path):
5d2af2f4
MF
162 gen_log = logging.getLogger("tornado.general")
163 gen_log.warn("No db found, creating in {}".format(path))
ddf4123d 164 con = sqlite3.connect(path)
6fd5e534
MF
165 con.execute("""create table author (id integer primary key,
166 uri varchar,
ddf4123d 167 name varchar,
6fd5e534
MF
168 email varchar,
169 salmon_pubkey varchar, -- base64_urlencoded public modulus +.+ base64_urlencoded public exponent
170 salmon_privkey varchar -- base64_urlencoded private exponent
171 );""")
172 con.execute(""" create table entry (id integer primary key,
173 author INTEGER,
174 text varchar, -- xml atom <entry>
175 verb varchar,
ddf4123d 176 ts timestamp default current_timestamp,
6fd5e534
MF
177 FOREIGN KEY(author) REFERENCES author(id));""")
178 con.execute("""
ddf4123d 179 create table subscriptions (id integer primary key,
6fd5e534 180 author integer,
ddf4123d
MF
181 expires datetime,
182 callback varchar,
087a104b 183 secret varchar,
ddf4123d 184 verified bool,
6fd5e534 185 FOREIGN KEY(author) REFERENCES author(id));""")
ddf4123d 186 con.commit()
b3102128 187
5d2af2f4
MF
188
189options.define("config_file", default="/etc/friends/friends.conf", type=str)
190options.define("webroot", default="/srv/friends/", type=str)
191
b3102128 192if __name__ == "__main__":
ddf4123d 193 dbPath = 'friends.db'
5d2af2f4
MF
194# options.log_file_prefix="/tmp/friends"
195 tornado.options.parse_config_file(options.config_file)
ddf4123d 196 tornado.options.parse_command_line()
5d2af2f4
MF
197 gen_log = logging.getLogger("tornado.general")
198 gen_log.info("Reading config from: %s", options.config_file,)
ddf4123d
MF
199 if not os.path.exists(dbPath):
200 setup_db(dbPath)
201 db = sqlite3.connect(dbPath, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
202 db.row_factory = sqlite3.Row
5d2af2f4 203 srv.listen(80)
3ffcd3f8 204 tornado.ioloop.IOLoop.instance().start()
86f2d7fc 205