| 1 | require "cgi" |
|---|
| 2 | require "cgi/session" |
|---|
| 3 | class 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 |
|---|
| 416 | end |
|---|
| 417 | |
|---|
| 418 | |
|---|
| 419 | # sample |
|---|
| 420 | if 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 | |
|---|
| 458 | end |
|---|