Add salmon support (WIP)
[friends.git] / friends / server.py
1 #!/usr/bin/python3
2 import tornado.ioloop
3 import tornado.web
4 import os
5 import os.path
6 import tornado.httpserver
7 import tornado.httpclient as httpclient
8 import salmoning
9 import sqlite3
10 import arrow
11 import datetime
12 from rd import RD, Link
13 import hmac
14 from tornado.options import options, define
15 import logging
16 db = 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
21 settings = {
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
32 class 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
68 class XrdHandler(tornado.web.RequestHandler):
69 def get(self):
70 self.render("templates/xrd.xml", hostname="ronin.frykholm.com", url=self.settings['domain'])
71
72 class 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
77 class 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
84 class 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
119 class 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
152 application = 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)
159 srv = tornado.httpserver.HTTPServer(application, )
160
161 def 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
189 options.define("config_file", default="/etc/friends/friends.conf", type=str)
190 options.define("webroot", default="/srv/friends/", type=str)
191
192 if __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