root/lang/ruby/weblogin/weblogin.rb @ 1589

Revision 248, 10.1 kB (checked in by cho45, 7 years ago)

lang/ruby/weblogin,
lang/ruby/weblogin/weblogin.rb,
lang/ruby/weblogin/readme.txt:

cgi/session とウェブサービスのログインAPI使うやつ。
名前がダサい

Line 
1require "cgi"
2require "cgi/session"
3class WebLogin
4        class LoginError < StandardError; end
5        class NoActionError < LoginError; end
6        class LoginFailed < LoginError; end
7
8        attr_reader :id, :user_name, :profile_uri, :icon, :full_name
9
10        def service
11                if @service && !@service.empty?
12                        @service
13                else
14                        nil
15                end
16        end
17       
18        def self.open(cgi, api_keys, session_opt={})
19                this = new(cgi, api_keys, session_opt)
20                if block_given?
21                        begin
22                                yield this
23                        ensure
24                                this.finish
25                        end
26                else
27                        this
28                end
29        end
30       
31        def initialize(cgi, api_keys, session_opt={})
32                @cgi = cgi
33                @session = CGI::Session.new(@cgi, {
34                        "prefix" => "_login_session",
35                        "session_path" => "/",
36                        "session_key" => "_login_session_id",
37                        "session_expires" => Time.now + 30 * 24 * 60 * 60
38                }.update(session_opt))
39                @api_keys = api_keys
40
41                [:service, :id, :user_name, :profile_uri, :icon, :full_name].each do |i|
42                        self.instance_eval("@#{i.to_s} = @session['#{i.to_s}']")
43                end
44        end
45
46        def finish
47                @session.close
48        end
49        alias close finish
50
51        def auth(debug=false)
52                @debug = debug
53                _, service_name, action = (@cgi.path_info || "").split("/")
54                case
55                when @cgi.query_string == "logout"
56                        # logout
57                        logout
58                        :logout
59
60                when action
61                        # call back
62                        callback(service_name)
63                        :login
64
65                when service_name
66                        @session["return_path"] = @cgi["return_path"]
67                        get_service(service_name).auth
68                        :redirect
69
70                else
71                        #puts @cgi.header("type" => "text/plain")
72                        #puts @api_keys.keys.join("\n")
73                        raise NoActionError
74                end
75        end
76
77        def services
78                ret = []
79                @api_keys.keys.each do |s|
80                        ret << s if WebLogin.const_defined?(s)
81                end
82                ret
83        end
84
85        private
86
87        def callback(service_name)
88                get_service(service_name).callback.each do |k,v|
89                        @session[k] = v
90                        self.instance_eval("@#{k.to_s} = v")
91                end
92                @service = @session["service"] = service_name
93                return if @debug
94                puts @cgi.header({
95                        "status" => "REDIRECT",
96                        "Location" => @session["return_path"],
97                        "type" => "text/plain"
98                })
99                puts "return #{@session["return_path"]}"
100        end
101
102        def logout
103                [:service, :id, :user_name, :profile_uri, :icon, :full_name].each do |i|
104                        @session[i.to_s] = nil
105                end
106                @session.close
107                @session.delete
108                puts @cgi.header({
109                        "status" => "REDIRECT",
110                        "Location" => @cgi.referer,
111                        "type" => "text/plain"
112                })
113        end
114
115
116
117        def get_service(service_name)
118                ret = nil
119                if @api_keys.key?(service_name)
120                        ret = WebLogin.const_get(service_name).new(@cgi, @session, @api_keys[service_name])
121                else
122                        raise NameError.new("", service_name)
123                end
124                ret
125                #       rescue NameError => e
126                #               raise NameError.new("Unknown Service `#{e.name}'.", e.name)
127        end
128
129        require "digest/md5"
130        require "xmlrpc/client"
131        require "rexml/document"
132        class Flickr
133                def initialize(cgi, session, api_key)
134                        @cgi = cgi
135                        @session = session
136                        @api_key = api_key["api_key"]
137                        @secret  = api_key["secret"]
138                        @server  = XMLRPC::Client.new("www.flickr.com", "/services/xmlrpc/", 80)
139
140                end
141
142                def auth
143                        api_sig = Digest::MD5.hexdigest("#{@secret}api_key#{@api_key}permsread")
144                        login_url = "http://flickr.com/services/auth/?api_key=#{@api_key}&perms=read&api_sig=#{api_sig}"
145                        puts @cgi.header({
146                                "status" => "REDIRECT",
147                                "Location" => login_url,
148                                "type" => "text/plain"
149                        })
150
151                end
152
153                def callback
154                        frob = @cgi["frob"]
155
156                        xml = call("flickr.auth.getToken", {
157                                "api_key" => @api_key,
158                                "frob" => frob,
159                        })
160                        doc = REXML::Document.new(xml)
161                        euser = doc.root.elements["/auth/user"]
162                        nsid = euser.attributes["nsid"]
163                        user = euser.attributes["username"]
164                        full = euser.attributes["fullname"]
165
166                        xml = call("flickr.people.getInfo", {
167                                "api_key" => @api_key,
168                                "user_id" => nsid
169                        })
170                        doc = REXML::Document.new(xml)
171                        iconserver = doc.root.attributes["iconserver"]
172
173                        {
174                                "id" => nsid,
175                                "user_name" => user,
176                                "profile_uri" => "http://www.flickr.com/photos/#{nsid}/",
177                                "icon" => "http://static.flickr.com/#{iconserver}/buddyicons/#{nsid}.jpg",
178                                "full_name" => full
179                        }
180
181                rescue XMLRPC::FaultException => e
182                        puts "Content-type: text/plain"
183                        puts
184                        puts "Error:"
185                        puts e.faultCode
186                        puts e.faultString
187                end
188
189                private
190               
191                def call(method, params)
192                        sig = @secret.dup
193                        params.keys.sort.each do |k|
194                                sig << k << params[k]
195                        end
196                        sig = Digest::MD5.hexdigest(sig)
197
198                        params["api_sig"] = sig
199                        @server.call(method, params)
200                end
201        end
202
203
204        require "openssl"
205        class TypeKey
206                class VerifyFailed < LoginFailed; end
207
208                def initialize(cgi, session, token)
209                        @cgi = cgi
210                        @session = session
211                        @token = token
212                end
213
214                def auth
215                        return_url = "http://"
216                        return_url << @cgi.host
217                        return_url << @cgi.script_name
218                        return_url << "/TypeKey/callback"
219
220                        login_url = "https://www.typekey.com/t/typekey/login?"
221                        login_url << "t=#{@token};"
222                        login_url << "_return=#{return_url};"
223                        login_url << "v=1.1"
224                        puts @cgi.header({
225                                "status" => "REDIRECT",
226                                "Location" => login_url,
227                                "type" => "text/plain"
228                        })
229                end
230
231                def callback
232                        email, name, nick = %w|email name nick|.map {|i| String.new @cgi[i] }
233
234                        if verify(email, name, nick, @cgi["ts"], @cgi["sig"])
235
236                                profile = Net::HTTP.get("profile.typekey.com", "/#{name}/")
237                                icon = profile[/<div class="photo">\s*<img src="([^"]+)/, 1]
238
239                                {
240                                        "id" => name,
241                                        "user_name" => nick,
242                                        "profile_uri" => "http://profile.typekey.com/#{name}/",
243                                        "icon" => icon,
244                                        "full_name" => nick
245                                }
246                        else
247                                raise VerifyFailed
248                        end
249                end
250
251                private
252
253                def verify(email, name, nick, ts, sig)
254                        key = Net::HTTP.get("www.typekey.com", "/extras/regkeys.txt").chomp
255                        key = Hash[*key.split(/ |=/)]
256
257                        data = [email, name, nick, ts, @token].join("::")
258
259                        sig.gsub!(/ /, "+")
260                        r_sig, s_sig = sig.split(':').collect {|i| i.unpack("m")[0].unpack("H*")[0].hex}
261
262                        sign = OpenSSL::ASN1::Sequence.new([OpenSSL::ASN1::Integer.new(r_sig), OpenSSL::ASN1::Integer.new(s_sig)]).to_der
263
264                        dsa = OpenSSL::PKey::DSA.new
265                        dsa.p, dsa.q, dsa.g = key["p"].to_i, key["q"].to_i, key["g"].to_i
266                        dsa.pub_key = key["pub_key"].to_i
267                        dsa.verify(OpenSSL::Digest::DSS1.new, sign, data)
268
269                end
270        end
271       
272        class Hatena
273
274                def initialize(cgi, session, api_key)
275                        @cgi = cgi
276                        @session = session
277                        @api_key = api_key["api_key"]
278                        @secret  = api_key["secret"]
279                end
280
281                def auth
282                        api_sig = Digest::MD5.hexdigest("#{@secret}api_key#{@api_key}")
283                        login_url = "http://auth.hatena.ne.jp/auth?api_key=#{@api_key}&api_sig=#{api_sig}"
284                        puts @cgi.header({
285                                "status" => "REDIRECT",
286                                "Location" => login_url,
287                                "type" => "text/plain"
288                        })
289
290                end
291
292                def callback
293                        res = call({
294                                "api_key" => @api_key,
295                                "cert" => @cgi["cert"]
296                        })
297                       
298                        doc = REXML::Document.new(res.body)
299                        if doc.root.elements["/response/has_error"].text == "true"
300                                raise doc.root.elements["/response/error/message"].text
301                        end
302                        name = doc.root.elements["/response/user/name"].text
303                        image = doc.root.elements["/response/user/image_url"].text
304
305                        {
306                                "id" => name,
307                                "user_name" => name,
308                                "profile_uri" => "http://d.hatena.ne.jp/#{name}/",
309                                "icon" => image,
310                                "full_name" => name
311                        }
312                end
313
314                private
315               
316                def call(params)
317                        sig = @secret.dup
318                        params.keys.sort.each do |k|
319                                sig << k << params[k]
320                        end
321                        sig = Digest::MD5.hexdigest(sig)
322
323                        params["api_sig"] = sig
324                        query = params.collect {|k,v| "#{URI.escape(k)}=#{URI.escape(v)}"}.join("&")
325                       
326                        uri = URI("http://auth.hatena.ne.jp/api/auth.xml?#{query}")
327                        ret = nil
328                        Net::HTTP.start(uri.host, uri.port) do |http|
329                                ret = http.get(uri.request_uri)
330                        end
331                        ret
332                end
333        end
334       
335        require "digest/sha1"
336        require "time"
337        class JugemKey
338                  def initialize(cgi, session, api_key)
339                        @cgi, @session = cgi, session
340                        @api_key = api_key["api_key"]
341                        @secret  = api_key["secret"]
342                end
343
344                def auth
345                        # https://secure.jugemkey.jp/?mode=auth_issue_frob&api_key={api_key}&perms={permission}&callback_url={callback_url}&api_sig={api_sig}
346                       
347                       
348                        callback_url = "http://"
349                        callback_url << @cgi.host
350                        callback_url << @cgi.script_name
351                        callback_url << "/JugemKey/callback"
352                       
353                        api_sig = hmac_sha1(@secret, "#{@api_key}#{callback_url}auth")
354                       
355                        puts @cgi.header({
356                                "status" => "REDIRECT",
357                                "Location" => "https://secure.jugemkey.jp/?mode=auth_issue_frob&api_key=#{@api_key}&perms=auth&callback_url=#{callback_url}&api_sig=#{api_sig}"
358                        })
359                end
360
361                def callback
362                        frob = @cgi["frob"]
363                        time = Time.now.xmlschema
364                       
365                        uri = URI("http://api.jugemkey.jp/api/auth/token")
366                        res = nil
367                        Net::HTTP.start(uri.host, uri.port) do |http|
368                                res = http.get(uri.request_uri, {
369                                        "X-JUGEMKEY-API-CREATED" => time,
370                                        "X-JUGEMKEY-API-KEY" => @api_key,
371                                        "X-JUGEMKEY-API-FROB" => frob,
372                                        "X-JUGEMKEY-API-SIG" => hmac_sha1(@secret, "#{@api_key}#{time}#{frob}")
373                                })
374                        end
375                       
376                        doc = REXML::Document.new(res.body)
377                        if doc.root.elements["/error"]
378                                raise doc.root.elements["/error"].text
379                        end
380                       
381                        user_name = doc.root.elements["/entry/title"].text
382                        token     = doc.root.elements["/entry/auth:token"].text
383                       
384                        {
385                                "id" => user_name,
386                                "user_name" => user_name,
387                                "profile_uri" => "",
388                                "icon" => "",
389                                "full_name" => user_name
390                        }
391                end
392
393                private
394                def hmac_sha1(key, str)
395                        key = Digest::SHA1.digest(key) if key.length > 64
396                        key << "\0" * (64 - key.length)
397                        ipad = "\x36" * 64
398                        opad = "\x5C" * 64
399                        (key.size - 1).times do |i|
400                                ipad[i] ^= key[i]
401                                opad[i] ^= key[i]
402                        end
403                       
404                        sha1 = Digest::SHA1.new
405                        sha1.update(ipad)
406                        sha1.update(str)
407                        str = sha1.digest
408                       
409                        sha1 = Digest::SHA1.new
410                        sha1.update(opad)
411                        sha1.update(str)
412                       
413                        sha1.hexdigest
414                end
415        end
416end
417
418
419# sample
420if ENV["SCRIPT_FILENAME"] == __FILE__
421        api_keys = {
422                "Flickr" => {
423                        "api_key" => "Flickr70c55b82e10021ffaaapi_key6",
424                        "secret"  => "Flickrdf5secret3"
425                },
426
427                "TypeKey" => "fsakfsTypeKeyafasAPI"
428        }
429
430
431        @cgi = CGI.new
432        #puts @cgi.header("type" => "text/plain")
433        WebLogin.open(@cgi, api_keys) do |login|
434                case login.auth
435                when :login
436                        require "YAML"
437                        File.open("login.yaml", "r+") do |f|
438                                f.flock(File::LOCK_EX)
439
440                                users = YAML.load(f) || []
441                               
442                                user = {}
443                                %w|service id user_name full_name profile_uri icon|.each do |s|
444                                        user[s] = login.send(s)
445                                end
446                                users.reject! {|x| (x["service"] == user["service"]) && (x["id"] == user["id"]) }
447
448                                users << user
449
450                                f.rewind
451                                f.puts(users.to_yaml)
452                                f.truncate(f.tell)
453                        end
454                when :logout
455                end
456        end
457
458end
Note: See TracBrowser for help on using the browser.