]> git.frykholm.com Git - friends.git/blame_incremental - friends/server.py
Add salmon support (WIP)
[friends.git] / friends / server.py
... / ...
CommitLineData
1#!/usr/bin/python3
2import tornado.ioloop
3import tornado.web
4import os
5import os.path
6import tornado.httpserver
7import tornado.httpclient as httpclient
8import salmoning
9import sqlite3
10import arrow
11import datetime
12from rd import RD, Link
13import hmac
14from tornado.options import options, define
15import logging
16db = None
17# insert into user (name,email) values('mikael','mikael@frykholm.com');
18# insert into entry (userid,text) values (1,'My thoughts on ostatus');
19
20
21settings = {
22 "static_path": os.path.join(os.path.dirname(__file__), "static"),
23 "cookie_secret": "supersecret123",
24 "login_url": "/login",
25 "xsrf_cookies": False,
26 "domain":"https://ronin.frykholm.com",
27
28}
29
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"
31
32class PushHandler(tornado.web.RequestHandler):
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','')
40 hub_secret = self.get_argument('hub.secret','')
41 hub_verify_token = self.get_argument('hub.verify_token','')
42 print(self.request.body)
43 if hub_mode == 'unsubscribe':
44 pass #FIXME
45 path = hub_topic.split(self.settings['domain'])[1]
46 user = path.split('user/')[1]
47 row = db.execute("select id from author where name=?",(user,)).fetchone()
48 expire = datetime.datetime.utcnow() + datetime.timedelta(seconds=int(hub_lease_seconds))
49 if row:
50 db.execute("INSERT into subscriptions (author, expires, callback, secret, verified) "
51 "values (?,?,?,?,?)",(row['id'],expire,hub_callback,hub_secret,False))
52 db.commit()
53 self.set_status(202)
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()
66 #TODO add secret to outgoing feeds with hmac
67
68class XrdHandler(tornado.web.RequestHandler):
69 def get(self):
70 self.render("templates/xrd.xml", hostname="ronin.frykholm.com", url=self.settings['domain'])
71
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
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('@')
89 row = db.execute("select id,salmon_pubkey from author where author.name=?",(user,)).fetchone()
90 if not row:
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)
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)))
117 self.write(rd.to_json())
118
119class UserHandler(tornado.web.RequestHandler):
120 def get(self, user):
121 entries = db.execute("select entry.id,text,ts from author,entry where author.id=entry.author and author.name=?",(user,))
122 # import pdb;pdb.set_trace()
123 self.set_header("Content-Type", 'application/atom+xml')
124 out = self.render("templates/feed.xml",
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 )
130 #digest = hmac.new()
131
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()
143 # Notify subscribers
144 subscribers = db.execute("select callback, secret from subscriptions, author where author.id=subscriptions.author and author.name=?",(user,))
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
152application = tornado.web.Application([
153 (r"/.well-known/host-meta", XrdHandler),
154 (r"/.well-known/webfinger", FingerHandler),
155 (r"/salmon/(.+)", SalmonHandler),
156 (r"/user/(.+)", UserHandler),
157 (r"/hub", PushHandler),
158 ],debug=True,**settings)
159srv = tornado.httpserver.HTTPServer(application, )
160
161def setup_db(path):
162 gen_log = logging.getLogger("tornado.general")
163 gen_log.warn("No db found, creating in {}".format(path))
164 con = sqlite3.connect(path)
165 con.execute("""create table author (id integer primary key,
166 uri varchar,
167 name varchar,
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,
176 ts timestamp default current_timestamp,
177 FOREIGN KEY(author) REFERENCES author(id));""")
178 con.execute("""
179 create table subscriptions (id integer primary key,
180 author integer,
181 expires datetime,
182 callback varchar,
183 secret varchar,
184 verified bool,
185 FOREIGN KEY(author) REFERENCES author(id));""")
186 con.commit()
187
188
189options.define("config_file", default="/etc/friends/friends.conf", type=str)
190options.define("webroot", default="/srv/friends/", type=str)
191
192if __name__ == "__main__":
193 dbPath = 'friends.db'
194# options.log_file_prefix="/tmp/friends"
195 tornado.options.parse_config_file(options.config_file)
196 tornado.options.parse_command_line()
197 gen_log = logging.getLogger("tornado.general")
198 gen_log.info("Reading config from: %s", options.config_file,)
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
203 srv.listen(80)
204 tornado.ioloop.IOLoop.instance().start()
205